
C言語の構造体|構造体を活用して保守性の高いプログラムを書こう
C言語では、構造体を活用することで保守性の高いプログラムを記述することが出来ます。構造体は、C++やJava等で使用されているクラスの基となったものです。
この記事では、構造体の基本的な使い方から、構造体を利用した便利な使い方を紹介したいと思います。
1.構造体の基礎
1-1.構造体とは
構造体とは、複数のデータをまとめたグループのことを言います。
配列も複数のデータをまとめることは可能ですが、構造体は配列を含めた複数のデータ型を1つにまとめることができる点で大きく異なります。
複数のデータを1つにまとめた構造体を、ユーザ作成のデータ型として使用する事で、構造体全体としてメモリにアクセスすることも、構造体の中にある各データ1つ1つにアクセスすることも可能です。
1-2.構造体の記述方法
構造体は、次の手順で記述します。
①構造体型を作成する
まず、必要なデータ型と名前を宣言した構造体型を作成します。
struct 構造体の名称(タグ名)
{
データ型 データ名称;
データ型 データ名称;
データ型 データ名称;
:
};
構造体の型を宣言するには、structという予約語を使用します。structの後に記述する構造体の名称は、通常の変数と同様に自由に決めることができます。構造体内部の各データのデータ型は、通常の変数と同様(int, char等)に自由に組み合わせられます。データ名称も構造体名称と同様に自由に決めることが出来ます。
以下に構造体定義の例を示します。
例1-1-1)
struct stCompany { char name[80]; // 会社名称 int capital; // 資本金額 int date; // 設立年月日 int val; // 社員数 };
例1-1-2)
typedef struct stCompany { char name[80]; // 会社名称 int capital; // 資本金額 int date; // 設立年月日 int val; // 社員数 } stCompany;
例1-1-1)と例1-1-2)の違いは、typedefという予約語を付けることです。structの後の構造体名称がなくなり、末尾のセミコロンの前に別名(stComp)で記述しています。
typedefという予約語は、別名で定義するための予約語です。構造体の場合、宣言する際にstructを付けると長くなりがちなので、typedefを使うことが多いです。
上記の例1-1-2)では、structの後に構造体名称を記述していますが、typedefを使う場合は省略できます。
②構造体の宣言
次に、構造体を宣言して実体を作ります。こちらは通常の変数と同様に宣言することで、定義した構造体を使うことが出来るようになります。
例1-2-1)
void main() { struct stCompany stComp; }
例1-2-2)
void main() { stCompany stComp; }
例1-1-1)のように定義した場合、例1-2-1)のように宣言します。例1-1-2)のように定義した場合、例1-2-2)のように宣言します。typedefを使用するか否かで、変数宣言が短くなることが分かります。上記の例ではauto変数としていますが、通常の変数と同様、グローバル変数で宣言することも可能です。
1-3.構造体の使い方(基礎編)
前章までで宣言の方法までを説明しました。本章では、構造体の使い方を説明します。
①構造体変数へのアクセス方法1
前章で用いた構造体を使って、構造体の中身であるメンバ変数へのアクセス方法を説明します。メンバ変数にアクセスするには、構造体変数とメンバ変数を『.』(ドット)でつなぎます。これだけで、通常の変数と同様に使うことが出来ます。
構造体変数.メンバ変数
例1-3-1)
void main() { stCompany stComp; memset(stComp.name, 0x00, sizeof(stComp.name); stComp.capital = 0; stComp.date = 0; stComp.val = 0; }
上記は単純に0で初期化するだけの処理ですが、アクセス方法は理解しやすいかと思います。ただし、上記の初期化処理であれば、1行で記述することも可能です。
例1-3-2)
void main() { stCompany comp; memset(comp, 0x00, sizeof(comp); }
上記の例では、構造体変数全体をmemsetを使用してゼロで初期化しています。構造体を使用すると、このようにプログラムを短くすることが出来ます。記述する量が多くなればなるほどバグを作りこみやすくなりますが、構造体を使用して記述量を減らす方法があることを理解していただければと思います。
②構造体変数へのアクセス方法2
構造体変数も単なる変数のため、通常の変数と同様、宣言すればメモリ上に配置されます。メモリに配置されるということは、配置されたアドレスも変数と同様に存在するということです。つまり、構造体変数においてもポインタの考え方はそのまま適用することができます。ここでは、ポインタ構造体変数へのアクセス方法を説明します。
ポインタ構造体変数にアクセスするには、構造体変数とメンバ変数を『->』(アロー)でつなぎます。
構造体変数->メンバ変数
例1-3-3)
void main() { stCompany stComp; memset(stComp, 0x00, sizeof(stComp); set_comp_data_all(&stComp, “株式会社ABC”, 100000000, 20020417, 500); } void set_comp_data_all(stCompany* pComp, char* name, int capital, int date, int val) { strcpy(pComp->name, name); pComp->capital = capital; pComp->date = date; pComp->val = val; }
自作の関数が追加されて少し長くなりましたが、最初から説明します。3行目までは構造体変数の宣言から初期化となります。5行目で構造体変数をポインタとして代入したい項目とともに関数に渡しています。10行目が構造体変数に渡されたデータを設定する関数であり、12行目から15行目でポインタ構造体変数にデータを設定しています。
『.』(ドット)が『->』(アロー)に変わっただけで、記述方法は変わらないことが分かります。
※自作関数についてはプロトタイプ宣言が必要となりますが、ここでは省略しています。
2.構造体の使い方
前章までで基本的な使い方を説明しましたが、本章では実際の使用方法や注意点を説明します。
2-1.構造体の使い方(応用編)
構造体を使う利点としては、データのまとまりを1つのデータかのように扱うことが出来るという点です。次の例では、構造体配列として扱ったプログラムを説明します。配列になっても、やることは通常の変数と同じである点に着目していただけると良いかと思います。
例2-1-1)
void main() { stCompany stComp[5]; memset(stComp, 0x00, sizeof(stComp); set_comp_data_all(&stComp[0], “株式会社AAA”, 100000000, 20020417, 500); set_comp_data_all(&stComp[1], “株式会社BBB”, 10000000, 20170401, 50); set_comp_data_all(&stComp[2], “株式会社CCC”, 300000000, 19930701, 1500); set_comp_data_all(&stComp[3], “株式会社DDD”, 400000000, 19971201, 1050); set_comp_data_all(&stComp[4], “株式会社EEE”, 500000000, 20030831, 100); output_comp_data(stComp); } void set_comp_data_all(stCompany* pComp, char *name, int capital, int date, int val) { set_data_string(pComp->name, name, sizeof(pComp->name)); set_data_int(&pComp->capital, capital); set_data_int(&pComp->date, date); set_data_int(&pComp->date, val); } // 文字列のデータセット処理 int set_data_string(char *out_val, char *in_val, int max_len) { int len; stCompany *pComp; pComp->name = out_val; // NULLチェック if((out_val == NULL) || (in_val == NULL)) { return -1; } // レングスチェック if(max_len > sizeof(pComp->name)) { // 引数の最大長が格納先より大きい場合、格納先のサイズに合わせる len = sizeof(pComp->name); } else { len = max_len; } strncpy(pComp->name, in_val, max_len); return 0; } // 文字列のデータセット処理 int set_data_int(int *in_val, int out_val) { *in_val = out_val; } // 出力処理 void output_comp_data(stCompany* pComp) { int cnt; char date[9], year[5], month[3], day[3]; for(cnt = 0; cnt < 5; cnt++) { // 初期化 memset(date, sizeof(date)); memset(year, sizeof(year)); memset(month, sizeof(month)); memset(day, sizeof(day)); // 文字列に変換 itoa((pComp + cnt)->date, date, 10); memcpy(year, &date[0], 4); memcpy(month, &date[4], 2); memcpy(day, &date[6], 2); // 出力 printf(“%80s, %10d円, %d/%d/%d, %d人\n”, (pComp + cnt)->name, (pComp + cnt)->capital, year, month, day, (pComp + cnt)->val); } }
上記の例の処理の流れは以下の通りです。
- 5個分の構造体配列を用意し、初期化する。
- 各配列にデータをセットする。
- セットしたデータを出力する。
データのセットについては、文字列をセットする関数と整数値をセットする関数の2つの関数を作成しています。
このようにセット処理を関数に置き換えることで、いつ、どこで、誰がセット処理をしているのかが明確になります。(実際にはプログラムの作成基準で、直接的な代入をしないことをルール化する必要があります)出力処理では、構造体変数配列をポインタで渡す場合のちょっと特殊な記述方法をしています。
ポインタはアドレスであり、インクリメントすることでアドレスをずらすことが出来るため、この手法を用いています。
2-2.構造体の注意点
構造体の注意点としては、sizeof演算子を使用して取得したサイズが、構造体内部のメンバ変数をsizeof演算子で取得したサイズの総和にならないことがある点です。OS依存にもなりますが、ここは注意していただきたいと思い、こちらに載せておきます。
struct stTest
{
char str[3];
int num1;
};
上記の構造体stTestのサイズは、char型(1バイト)×3、int型(4バイト)なので、各メンバ変数のサイズとしては、3バイト、4バイトとなり、総和は7バイトです。しかし、構造体のサイズは8バイトとなる場合があります。(OSや環境依存)
理由は、メモリ上では4バイトでパディングされることにあります。コンピュータの世界では、メモリ管理を4バイト単位でされることが往々にしてあり、この点を理解していないと、思いがけない不具合を生む可能性があるのです。このため、構造体を使用する際には十分注意してください。
3.最後に
以上、C言語における構造体の使い方をまとめました。構造体を使用する事で、C言語のプログラミングの幅が増大し、データ管理がやりやすくなります。また、C++やJavaのようなオブジェクト指向言語の基礎となる部分でもあります。なので、C言語を覚えたいと思っている方には是非覚えていただきたいと思います。
尚、本記事内で例として使用したコードはあくまで使い方を示すためのものであるため、実務での使用方法に合わないものも含まれています。
コメント