【C言語】構造体の定義/宣言/初期化【struct 完全解説・豆知識】

構造体の定義・宣言・初期化方法、その他細かな仕様について解説します。

目次

構造体の定義・宣言

構造体はstruct タグ名 { メンバの並び }という形で定義します。

struct Number {
   int value;
};

変数宣言時にはstructキーワードが必須です。

struct Number object;
printf("%d", object.value);

変数宣言時のstructキーワードを省略したい場合には、typedefを用いた定義が必要となります。

typedef struct Number {
   int value;
} Number;

Number object; // structキーワードの省略が可能

typedef指定子で構造体struct Numberの別名Numberを定義しています。具体的な説明や原理、目的については以下の記事を参考にしてください。

参考: typedef structによる構造体の定義|一般的な宣言との違いや目的

構造体の初期化

構造体変数の初期化方法は複数あります。

よく知られている初期化方法

// struct Number { int value; };
struct Number object;
object.value = 99;

古い時代から使われている方法です。先に構造体の変数を宣言し、後から個別にメンバ変数を初期化します。

初期化子の並びによる初期化

配列の初期化方法と同様の形式でメンバ変数の初期化が行えます。

// struct Number { int value; };
struct Number obj = {99};
obj.value; // 99

構造体のメンバが複数ある場合にも対応できます。

// struct Range { int location, length; };
struct Range range = {1, 2};
range.location; // 1
range.length;   // 2

この波括弧で囲われた初期化子の並びによる記述は「初期化リスト」や「初期化子リスト」と呼ばれることもあります。

複合リテラルによる初期化

構造体を一時オブジェクトとして表現する場合には、初期化リストが使えません。代わりに複合リテラルを用います。

struct Range { int location, length; };

struct Range fn(struct Range) {
   return (struct Range){3, 4}; // OK(複合リテラル)
   return {3, 4};               // NO(初期化リスト)
}

fn((struct Range){1, 2}); // OK(複合リテラル)
fn({1, 2});               // NO(初期化リスト)
参考: 複合リテラル【構造体リテラルや配列リテラルを実現する】

指示初期化子による初期化

指示付きの初期化子を用いることで、構造体のメンバ名を明示した形での初期化が可能になります。

// struct Range { int location, length; };
struct Range r = {.location = 1, .length = 2};
struct Range r = {.length = 2, .location = 1};
fn((struct Range){.location = 1, .length = 2});
fn((struct Range){.length = 2, .location = 1});

初期化子の並びは自由です。

指示付きの初期化子(designated initializer)

{.メンバ名 = 初期値}という特殊な式は指示付きの初期化子/指示初期化子(designated initializer)と呼ばれるC言語(C99)の新機能です。

// struct Range { int location, length; };
struct Range r = {.location = 1, .length = 2};
struct Range r = {.length = 1, .location = 1};

指示付き初期化子は、現行のC++(C++17)には取り入れられていない機能であるため注意してください。ただしClang++コンパイラではC言語互換の拡張機能として実装されています。なおC++では現在「Designated Initialization」という名前で、正式な機能としての追加が提案されています。ただ注意したいのは、指定子の順序がメンバ変数の宣言順でなければならないという制限が検討されている点です。

struct { int a, b; } x{.a = 2, .b = 1}; // OK
struct { int a, b; } y{.b = 2, .a = 1}; // Error

Clang++で本拡張機能を使用する場合はその点の将来的な仕様を意識しておく必要があります。

構造体ポインタの初期化

構造体用のメモリをmalloc関数で動的に確保する際には、sizeof(struct 構造体タグ名)という形式で構造体のサイズを指定します。

struct Number *p = malloc(sizeof(struct Number));
p->value = 9; // 初期値

構造体のサイズをsizeof(*ポインタ変数名)と言う形で取得するテクニックもあります。

struct Number *p = malloc(sizeof(*p));

いずれも同等のサイズが指定されます。後者のテクニックには、記述の少なさや、リファクタリングのしやすさ等の利点があります。

構造体の前方宣言

構造体の宣言のみを事前に行うことができます。

struct Number;    /* 宣言 */
struct Number {}; /* 定義 */

定義が行われていない段階での宣言は前方宣言と呼ばれます。前方宣言された型は不完全型となるため、実際の定義が行われるまでは、メンバ変数への参照が行えなかったり、仮引数型としての宣言が行えないという制限があります。ただし、ポインタ変数としての宣言は可能です。

struct Number;  /* 前方宣言 */
struct Number object;   // OK (仮定義)
struct Number *pointer; // OK (前方宣言)

void f(struct Number param) {}  // error: variable has incomplete type 'struct Number'
void g(struct Number *param) {  // OK (前方宣言)
   param->value;    // error: incomplete definition of type 'struct Number'
   object.value ;   // error: incomplete definition of type 'struct Number'
   pointer->value ; // error: incomplete definition of type 'struct Number'
} 

struct Number { /* 定義 */
   int value;
};

void h(struct Number param) { // OK
   param.value;    // OK
   object.value;   // OK
   pointer->value; // OK
}

構造体の定義が存在しない環境で変数宣言が行われた場合にはエラーが発生します。

// struct Number;    /* 宣言省略 */
struct Number object; // error: tentative definition has type 'struct Number' that is never completed
// struct Number {}; /* 定義省略 */

ポインタ型の宣言については常に前方宣言とみなされます。

// struct Number; /* 明示的な宣言は不要 */
struct Number *param; // 前方宣言

struct A { struct B *ptr; }; // 前方宣言 (`struct B`)
struct B { struct A *ptr; };

無名構造体/匿名構造体

タグ名を省略した無名の構造体をその場で定義し、変数宣言することが可能です。

int main() {
   struct { int a; int b } pair;
   pair.a = 1;
   pair.b = 2;
}

ちょっとした処理やアルゴリズムを実現する際に重宝します。typedefで型名を付けることも可能です。

int main() {
  typedef struct { char c; int i; } Pair;
  Pair pair = {'C', 99};
}

グローバルスコープでも同様に、構造体の定義時に匿名構造体を用いることもできます。この場合structキーワードによる変数宣言は行えなくなります。

typedef struct { int value; } Number;

int main() {
   Number object; // OK
   struct Number object; // error: variable has incomplete type 'struct Number'
}

自己参照構造体

自身の構造体型を自身のメンバ変数の型として利用する場合には、メンバ変数をポインタとして宣言する必要があります。

struct Node {
   struct Node *next;
};

ポインタとしての宣言は必須です。自身と同じ型の値を保持する構造体を宣言することはできません。自身を保持する構造体は再帰的な定義を招き、構造体のデータサイズを確定することが出来なくなるためです。

struct Node {
   int i;
   // error: field has incomplete type 'struct Node'
   // note: definition of 'struct Node' is not complete until the closing '}'
   struct Node next;
   // この`next`メンバ変数のサイズは`struct Node`のデータサイズに依存する
   // `struct Node`のデータサイズは`sizeof(i)`のサイズと`sizeof(next)`のサイズの合計
   // `sizeof(next)`のサイズは`struct Node`のデータサイズに依存する
   // 以下無限ループ...
};

そのため、ポインタによる宣言が必要となります。このように、自身と同じ型の構造体を参照するポインタ変数を持った構造体を「自己参照構造体(self-referential structure)」と呼びます。ポインタは実体ではなくあくまで参照であり、データメンバのサイズはポインタ型のサイズ(4バイト/8バイト)で確定するため、再帰的な定義が行われる心配もありません。

なお自身の型をメンバに持つ構造体を作る際には、構造体のタグ名を活用する必要があります。メンバを宣言する段階ではtypedefによる別名の定義が完了していないためです。

typedef struct Node_ {
   // この段階では自身の構造体は不完全型となっているため再帰的な宣言は行えない
   // error: field has incomplete type 'struct Node_'
   // note: definition of 'struct Node_' is not complete until the closing '}'
   struct Node_ next;
   
   // typedefによる別名の定義が確定していないため不可
   // error:  unknown type name 'Node'
   Node *next;
} Node;

タグ名による記述struct Node_が煩わしいと感じる場合は、# 構造体の前方宣言を活用し、事前にtypedefで別名を付けることも可能です。こうすると構造体のメンバで自身の型をNode *と書けるようになります。

typedef struct Node_ Node; // 前方宣言と別名の定義

struct Node_ {
   Node *next; // OK
   
   /* `struct Node_`の定義が確定していないため不可 */
   // Node next; // error: field has incomplete type 'Node' (aka 'struct Node_')
};

ちなみに

ちなみにC++ではstruct Number {};と書くだけでstruct NumberNumberの両記法による変数宣言が行えます。気の利くヤツです。


C++「どや」
私「うむ、これはいいものだ」

広告