配列は1つの型の値をたくさん保持します。struct(構造体)はその逆で、異なる型の値をいくつか保持し、それらを1つのものとして扱います。点は x と y です。従業員は名前とidと給与です。構造体がなければ、それらを3つのばらばらの変数として持ち回り、誰の給与がどれか取り違えないことを祈ることになります。
構造体型を宣言する
構造体の宣言はタグに名前を付け、波かっこの中にメンバを並べます。
struct Point {
int x;
int y;
};
閉じ波かっこのあとのセミコロンは必須です - これを忘れることはC言語の初心者が出会う最も混乱するエラーメッセージの1つを招きます。コンパイラが次の行を責めるからです。
これは型を宣言するもので、変数ではありません。Cではその型の完全な名前はキーワードを含めた struct Point です。
メンバはドット演算子 p.x で読み書きします。構造体の変数はふつうの変数です。スタック上に置かれ、同じ型の別の構造体へ = で代入でき、スコープの終わりで消えます。
いたるところに struct Point と書くのは疲れるので、実際のCコードのほとんどは宣言をtypedefで包み、型名を単に Point にします。
構造体を初期化する
宣言した瞬間に構造体を埋められます。位置による形は宣言順に値を並べます。
struct Point p = {3, 4}; // x = 3, y = 4
指示付き初期化子は代わりに各メンバを名指しし、より明快で安全です。
注目すべき点が3つあります。
- 指示付きの形はメンバを任意の順で並べられるので、あとでメンバを追加したり並べ替えたりしても、値が静かに違う枠へずれ込むことがありません。
- 省いたメンバはすべてゼロ初期化されます -
{0}は構造体全体をゼロにする慣用句です。 char name[32]のメンバは構造体の中の本物の配列なので、文字列は他のどこかではなく構造体自身の中に格納されます。それはまた、あとからa.name = "Ada";とするのが不正だということでもあります。strcpyが必要です(文字列関数を参照)。
初期化子がない構造体は未初期化で、書く前にメンバを読むのは未定義動作です。= {0} はコストがゼロで、この種のバグをまるごと取り除きます。
代入と比較
構造体の代入はすべてのメンバをコピーします。
a は手つかずです。b は自分のコピーを得ました。しかしCが与えてくれないものに注意してください - 構造体に == はありません。if (a == b) はコンパイルできません。比較するとはメンバを比較することです。
if (a.x == b.x && a.y == b.y) { /* 等しい */ }
近道として memcmp に手を出してはいけません。コンパイラは整列のためメンバの間に目に見えないパディングのバイトを挿入し、そのバイトにはゴミが入りうるので、同一のメンバを持つ2つの構造体がバイト単位では不一致になりえます。
構造体の配列
構造体は他のどの型とも同じ型なので、その配列を作れます - レコードの表を保持する標準的な方法です。
staff[i].salary は「要素 i、そしてその salary メンバ」と読めます - 添字づけはドットより強く結びつくので、かっこは要りません。
構造体は関数へコピーされる
これが人をつまずかせる規則です。構造体を関数に渡すと、Cはそれをコピーします。関数は自分専用のコピーに対して働きます。
p はまだ (3, 4) です。distance_from_origin のように構造体を値で読むのはまったく問題ありませんし、小さな構造体にとってはしばしば最も明快な選択です。しかし関数が呼び出し元の構造体を変更しなければならないなら - あるいは構造体が大きくてコピーが無駄になるなら - 代わりにポインタを渡します。それが構造体とポインタの主題です。
構造体を値で返すのも同じように働き、まったく合法です。
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
入れ子の構造体
構造体のメンバはそれ自体が構造体でありえます。ドットをつなげて内側へ届きます。
b.published.year は一度に1段ずつ内側へ入っていきます。入れ子こそC言語で本物のデータモデルを組み立てる方法であり、内側の構造体は外側のものに指されるのではなくその中に格納されるので、Book 全体がメモリ上の1つの連続した塊になります。
サイズ、パディング、メンバの順序
sizeof は構造体の大きさを教えてくれますが、その答えはしばしばメンバの合計より大きくなります。
典型的なマシンでは Wasteful は12バイト、Tidy は8バイトです。同じ3つのメンバを持っているのにです。コンパイラは各メンバがその型の好むアドレスに来るようパディングを挿入します - int はたいてい4の倍数を望みます。大きいメンバを小さいものより前にまとめると、詰まり方が良くなる傾向があります。
これが問題になることは稀で(100万要素の配列、ネットワークパケットの配置など)、自分のコードの正しさには決して関わりません。ただこれは、sizeof があなたを驚かせる理由と、構造体に対する memcmp が悪い考えである理由を説明してくれます。
よくある質問
C言語で構造体はどう宣言しますか?
struct に続けてタグ名と、波かっこで囲んだメンバの並びを書きます:struct Point { int x; int y; };。これは型を宣言するもので、変数ではありません。変数を作るにはそのあと struct Point p; と書きます - Cでは struct という語が型名の一部であり、だからこそ多くのコードがtypedefと組み合わせるのです。
構造体における . と -> の違いは何ですか?
. と -> の違いは何ですか?構造体そのものを持っているときは .(p.x)を、それへのポインタを持っているときは ->(ptr->x)を使います。矢印は (*ptr).x の省略記法です。詳しくは構造体とポインタを参照してください。
C言語の構造体は値渡しですか、参照渡しですか?
常に値渡しです。構造体を関数に渡すとすべてのメンバがコピーされるので、関数の中での変更は呼び出し元の変数に影響しません。元のものを変更するには - あるいは大きな構造体のコピーを避けるには - 代わりにポインタを渡します。
C言語で構造体はどう初期化しますか?
位置による方法 struct Point p = {3, 4}; か、指示付き初期化子による方法 struct Point p = {.y = 4, .x = 3}; です。指示付きの形は各メンバを名指しするので、あとで誰かが構造体の並び順を変えても生き延びますし、名指ししなかったメンバはゼロになります。