C言語で複雑なデータを扱うとき、構造体の宣言と初期化は避けて通れません。
しかし、メンバ数が増えたりネストしたりすると、どの値がどこに入るのか分かりにくくなりがちです。
本記事では、構造体の基本から初期化パターン、配列・ネスト・C++との違いまでを、図解とサンプルコードで丁寧に解説します。
C言語の構造体とは?基本と宣言の書き方
構造体(struct)の概要とメリット

構造体(struct)は、関連する複数の値を1つのまとまりとして扱うためのユーザー定義型です。
C言語の組み込み型はintやdoubleなど単独の値しか扱えませんが、構造体を使うと、名前・年齢・身長のような論理的にひとまとまりの情報を1つの型として扱えるようになります。
構造体を使うメリットとして、次のような点があります。
- 関連する情報を集約できるので、引数や戻り値がすっきりします。
- メンバ名でアクセスできるので、コードの意味が読み取りやすくなる効果があります。
- 配列やポインタと組み合わせることで、テーブルデータやオブジェクト風の表現が可能になります。
たとえば、人物を表す構造体は次のように定義します。
// 人物を表す構造体型の定義
struct Person {
char name[32]; // 名前
int age; // 年齢
double height; // 身長(m)
};
このようにラベル付きのメンバを持つ1つの変数として、構造体を使えるようになります。
構造体タグ名とtypedefの使い分け

構造体にはタグ名(structの後に書く名前)と、typedefで付ける別名があります。
タグ名とは
次のPersonが「構造体タグ名」です。
struct Person {
char name[32];
int age;
};
タグ名があると、変数宣言は次のように書けます。
struct Person alice; // struct + Person で型を指定
C言語ではタグ名だけでは型名にならない点に注意が必要です。
必ずstruct Personと書く必要があります。
typedefによる別名
毎回struct Personと書くのが煩雑な場合、typedefで別名を付けるのが一般的です。
// 構造体定義と同時に別名Personを定義
typedef struct Person {
char name[32];
int age;
} Person;
// 以降は「Person」だけで型として使える
Person bob;
あるいは、タグ名を使わずにtypedefだけで完結させる書き方もあります。
typedef struct {
char name[32];
int age;
} Person; // タグ無し、別名のみ
Person tom;
タグ名を付けておくと、自己参照ポインタ(例えばstruct Node *next;)で便利なので、リスト構造などではタグ名も付けておくのが無難です。
構造体の基本的な宣言・定義の例

構造体の「定義」と「変数の宣言」を整理しておきます。
構造体の定義は型を作る作業、宣言はその型の変数を作る作業です。
#include <stdio.h>
// 構造体型の定義
typedef struct {
char name[32];
int age;
} Person;
int main(void) {
// 構造体変数の宣言(定義済みの型Personを使う)
Person alice; // 中身は未初期化
Person bob; // こちらも未初期化
// メンバへの代入
alice.age = 20;
bob.age = 25;
printf("alice.age = %d\n", alice.age);
printf("bob.age = %d\n", bob.age);
return 0;
}
alice.age = 20
bob.age = 25
構造体変数を宣言した時点では自動変数(auto)は未初期化であり、必ず初期化または代入を行う必要があることを意識しておくと安全です。
構造体の初期化
波括弧による構造体の初期化

構造体の最も基本的な初期化は、宣言時に波括弧{ }で値を列挙する方法です。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
// 宣言と同時に初期化
struct Point p1 = {10, 20}; // x=10, y=20
printf("p1.x = %d, p1.y = %d\n", p1.x, p1.y);
return 0;
}
p1.x = 10, p1.y = 20
このとき、並び順は構造体定義のメンバ順に対応します。
つまり、上の例ではxが10、yが20になります。
メンバ名を使った構造体の初期化

C99以降では、指定初期化子(designated initializer)を使ってメンバ名を明示して初期化できます。
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
// 通常の位置指定
struct Point p1 = {10, 20}; // x=10, y=20
// メンバ名を指定した初期化(C99以降)
struct Point p2 = {.y = 20, .x = 10}; // x=10, y=20 (順不同OK)
printf("p1 = (%d, %d)\n", p1.x, p1.y);
printf("p2 = (%d, %d)\n", p2.x, p2.y);
return 0;
}
p1 = (10, 20)
p2 = (10, 20)
この書き方の利点は、メンバ順に依存しないことと、コードを読んだときにどの値がどのメンバかが一目で分かる点にあります。
特にメンバ数が多い構造体では、指定初期化子を使うと可読性が大きく向上します。
省略時の暗黙の初期値とゼロ初期化

構造体の初期化では値を省略したメンバには暗黙の初期値として0が入るというルールがあります。
具体的には、整数型は0、浮動小数点型は0.0、ポインタはNULL相当になります。
#include <stdio.h>
struct S {
int a;
double b;
int c;
};
int main(void) {
// 先頭メンバだけ初期化、残りは暗黙に0初期化
struct S s1 = {42}; // a=42, b=0.0, c=0
// {0}とすると「全メンバを0で初期化」という慣用句
struct S s2 = {0}; // a=0, b=0.0, c=0
printf("s1: a=%d, b=%f, c=%d\n", s1.a, s1.b, s1.c);
printf("s2: a=%d, b=%f, c=%d\n", s2.a, s2.b, s2.c);
return 0;
}
s1: a=42, b=0.000000, c=0
s2: a=0, b=0.000000, c=0
また、静的記憶域期間(static・グローバル)の変数は、初期化を書かなくても自動的に0で初期化されます。
struct S g; // グローバル変数、暗黙に全メンバ0
int main(void) {
static struct S s; // staticローカルも暗黙に0
(void)g;
(void)s;
}
自動変数(普通のローカル変数)は暗黙の初期化はされません。
必要なら={0}などで明示的に初期化しましょう。
const構造体とグローバル変数の初期化

構造体はconstを付けて読み取り専用データとして宣言することがよくあります。
たとえば、設定テーブルやメッセージテーブルなどです。
#include <stdio.h>
typedef struct {
int id;
char name[16];
} Config;
// グローバルなconst構造体の初期化
const Config default_config = {
.id = 1,
.name = "default"
};
int main(void) {
printf("id=%d, name=%s\n", default_config.id, default_config.name);
// default_config.id = 2; // エラー: constなので書き換え不可
return 0;
}
id=1, name=default
グローバル変数・static変数・const変数の初期化子はコンパイル時に決まる必要があるため、mallocや関数呼び出しの結果などは使えません。
必ず定数式や文字列リテラルなどで初期化する必要があります。
悪い構造体の初期化例とアンチパターン

構造体の初期化でも、避けた方がよい書き方があります。
代表的なアンチパターンを挙げます。
1つ目は、メンバが多いのに位置だけで初期化するケースです。
// アンチパターン: 何がどのメンバか分かりにくい
typedef struct {
int id;
int x;
int y;
int width;
int height;
int visible;
} Widget;
Widget w1 = {1, 100, 200, 300, 400, 1}; // ぱっと見て意味が分からない
メンバ名を知らないと、数字の羅列が何を意味しているか読み取れません。
構造体定義を参照しないと理解できないコードは、保守性を大きく下げます。
2つ目は、構造体の途中までしか初期化していないのに、それを意図していないケースです。
// 意図せず一部が0初期化されている例
Widget w2 = {1, 100}; // id=1, x=100, それ以外は0
暗黙のゼロ初期化自体は仕様ですが、「あえて0にしたい」のか「初期化し忘れ」なのか判別できない書き方になってしまいます。
3つ目は、初期化と同時に意味のないマジックナンバーを使うことです。
// マジックナンバーだらけの例
Widget w3 = {42, 100, 200, 640, 480, 1};
何が42なのか、1は何を表すのか不明瞭です。
後述の「可読性の高い初期化スタイル」で、名前付き定数や指定初期化子を組み合わせる方が望ましいです。
可読性の高い構造体の初期化スタイル

可読性を高めるためには、意図が読み手に伝わる初期化を心がけることが重要です。
いくつかスタイルを紹介します。
指定初期化子+定数を使う
#include <stdio.h>
typedef struct {
int id;
int x;
int y;
int width;
int height;
int visible;
} Widget;
enum { VISIBLE = 1, HIDDEN = 0 };
int main(void) {
// 可読性が高い初期化
Widget button = {
.id = 1,
.x = 100,
.y = 200,
.width = 120,
.height = 40,
.visible = VISIBLE
};
printf("button: id=%d, pos=(%d,%d), size=(%d,%d), visible=%d\n",
button.id, button.x, button.y, button.width, button.height, button.visible);
return 0;
}
button: id=1, pos=(100,200), size=(120,40), visible=1
このようにメンバ名と名前付き定数を併用することで、「何の値か」「どういう意味か」が非常に分かりやすくなります。
初期化専用の関数を用意する
複雑な初期化ロジックが必要な場合は、ファクトリ関数のような形でまとめるのも有効です。
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
// Pointを生成するヘルパ関数
Point make_point(int x, int y) {
Point p = { .x = x, .y = y };
return p;
}
int main(void) {
Point p1 = make_point(10, 20);
Point p2 = make_point(-5, 0);
printf("p1=(%d,%d), p2=(%d,%d)\n", p1.x, p1.y, p2.x, p2.y);
return 0;
}
p1=(10,20), p2=(-5,0)
関数名make_pointから「点を作る」という意図が明確になり、初期化の詳細を呼び出し側から隠蔽できます。
配列とネストした構造体の初期化
構造体配列の宣言と一括初期化

構造体は配列としてまとめて扱うこともできます。
配列の初期化では、各要素がさらに波括弧で囲まれる形になります。
#include <stdio.h>
typedef struct {
char name[16];
int age;
} Person;
int main(void) {
// 構造体配列の一括初期化
Person people[3] = {
{"Alice", 20},
{"Bob", 30},
{"Carol", 40}
};
for (int i = 0; i < 3; i++) {
printf("%s (%d)\n", people[i].name, people[i].age);
}
return 0;
}
Alice (20)
Bob (30)
Carol (40)
それぞれの{"Alice", 20}が配列の1要素(構造体)の初期化子になっています。
部分的な構造体配列の初期化と暗黙のゼロ埋め

配列の初期化でも足りない要素は0で初期化されます。
構造体配列でも同様です。
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
int main(void) {
// 5要素のうち先頭2要素だけ初期化
Point points[5] = {
{1, 2},
{3, 4}
// 残り3要素は {0, 0} で初期化される
};
for (int i = 0; i < 5; i++) {
printf("points[%d] = (%d, %d)\n", i, points[i].x, points[i].y);
}
return 0;
}
points[0] = (1, 2)
points[1] = (3, 4)
points[2] = (0, 0)
points[3] = (0, 0)
points[4] = (0, 0)
また、配列インデックスを指定して初期化する(C99)ことも可能です。
// インデックス指定初期化(C99)
Point table[5] = {
[2] = {10, 20}, // table[2]だけを初期化
[4] = {30, 40} // table[4]だけを初期化
}; // 他の要素は {0,0}
インデックス指定と指定初期化子を組み合わせると、スパースなテーブルを分かりやすく定義できます。
構造体の中に配列がある場合の初期化

構造体のメンバとして配列を持つこともよくあります。
この場合、配列メンバも波括弧で初期化します。
#include <stdio.h>
typedef struct {
int id;
int scores[3]; // 3科目の点数
} Student;
int main(void) {
Student s1 = {
.id = 1,
.scores = {80, 90, 100} // 配列メンバの初期化
};
printf("id=%d, scores=%d,%d,%d\n",
s1.id, s1.scores[0], s1.scores[1], s1.scores[2]);
return 0;
}
id=1, scores=80,90,100
Cでは、外側の構造体初期化子の中にさらに内側の配列初期化子が入る形になります。
指定初期化子を使うとさらに分かりやすく書けます。
Student s2 = {
.id = 2,
.scores = {[0] = 70, [2] = 90} // 一部の科目だけ指定
// scores[1]は0になる
};
構造体の中に構造体があるネスト構造の初期化

構造体のメンバに別の構造体を持たせると、入れ子(ネスト)構造を表現できます。
#include <stdio.h>
typedef struct {
char city[16];
char ward[16];
int zip;
} Address;
typedef struct {
char name[16];
Address addr; // 構造体をメンバに持つ
} Person;
int main(void) {
// ネストした構造体の初期化
Person p1 = {
.name = "Alice",
.addr = { "Tokyo", "Chiyoda", 100000 } // Addressの初期化子
};
printf("%s lives in %s %s (zip=%d)\n",
p1.name, p1.addr.city, p1.addr.ward, p1.addr.zip);
return 0;
}
Alice lives in Tokyo Chiyoda (zip=100000)
入れ子が深くなる場合は、指定初期化子を併用すると構造が明確になります。
Person p2 = {
.name = "Bob",
.addr.city = "Osaka", // メンバをドットでつないで指定可能(C99)
.addr.ward = "Kita",
.addr.zip = 5300000
};
このように.addr.cityのような書き方で、ネストしたメンバに対しても直接指定初期化できます。
多次元配列+構造体の初期化パターン

構造体と多次元配列を組み合わせると、行列形式のテーブルなどを表現できます。
#include <stdio.h>
typedef struct {
int value;
char label;
} Cell;
int main(void) {
// 2行3列の盤面を表す構造体2次元配列
Cell board[2][3] = {
{ {0, 'A'}, {1, 'B'}, {2, 'C'} },
{ {3, 'D'}, {4, 'E'}, {5, 'F'} }
};
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("(%d,%d): value=%d, label=%c\n",
i, j, board[i][j].value, board[i][j].label);
}
}
return 0;
}
(0,0): value=0, label=A
(0,1): value=1, label=B
(0,2): value=2, label=C
(1,0): value=3, label=D
(1,1): value=4, label=E
(1,2): value=5, label=F
多次元配列の初期化では、外側から内側へとネストされた波括弧になります。
インデックス指定を併用することで、部分的な初期化も可能です。
// 特定のマスだけ初期化
Cell board2[2][3] = {
[0][1] = { .value = 10, .label = 'X' },
[1][2] = { .value = 20, .label = 'Y' }
}; // 他は{0, '\0'}で初期化
図解で理解する入れ子構造のメモリ配置と初期化

構造体はメモリ上ではただの連続した領域として配置されます。
入れ子構造も、外側の順番に内側のメンバが並ぶだけです。
#include <stdio.h>
typedef struct {
float x;
float y;
} Vec2;
typedef struct {
Vec2 pos; // 位置
Vec2 scale; // 拡大率
float angle; // 角度
} Transform;
int main(void) {
// 入れ子構造の初期化
Transform t = {
.pos = {1.0f, 2.0f},
.scale = {0.5f, 0.5f},
.angle = 90.0f
};
printf("pos=(%f,%f), scale=(%f,%f), angle=%f\n",
t.pos.x, t.pos.y, t.scale.x, t.scale.y, t.angle);
return 0;
}
pos=(1.000000,2.000000), scale=(0.500000,0.500000), angle=90.000000
この例では、メモリ上の順序はpos.x、pos.y、scale.x、scale.y、angleという並びになります。
初期化子もその順番に従って値が格納されます。
指定初期化子を使えば、この順序を意識せずに「ラベル」に対して値を指定できるので、安全かつ読みやすくなります。
CとC++の構造体初期化の違い
C言語の構造体初期化とC++の構造体初期化の比較

CとC++はいずれも構造体を持ちますが、初期化構文の種類に違いがあります。
代表的なパターンを簡単に比較します。
| 言語 | 構文例 | 特徴 |
|---|---|---|
| C | struct Point p = {1, 2}; | 波括弧による集成体初期化のみ |
| C | struct Point p = {.x = 1, .y = 2}; | C99の指定初期化子 |
| C++ | Point p = {1, 2}; | 集成体初期化(aggregate initialization) |
| C++ | Point p{1, 2}; | かっこ初期化(uniform initialization) |
| C++ | Point p(1, 2); | コンストラクタ呼び出し形式 |
C++では{ }を使ったかっこ初期化(brace initialization)が導入され、Point p{1, 2};のように、Cライクな構造体にもC++クラスにも使える統一的な初期化記法が採用されています。
C++らしい構造体の定義と初期化

C++ではstructもclassもほぼ同じ機能を持ちます。
違いはデフォルトのアクセス指定子(publicかprivateか)だけです。
C++らしい構造体の定義と初期化の例を見てみます。
#include <iostream>
struct Point {
double x = 0.0; // メンバのデフォルト値(C++11以降)
double y = 0.0;
// コンストラクタ
Point(double x, double y) : x(x), y(y) {}
};
int main() {
Point p1{1.0, 2.0}; // コンストラクタを呼ぶかっこ初期化
Point p2; // デフォルトコンストラクタでx=0.0, y=0.0
std::cout << "p1=(" << p1.x << "," << p1.y << ")\n";
std::cout << "p2=(" << p2.x << "," << p2.y << ")\n";
}
p1=(1,2)
p2=(0,0)
このようにC++では、構造体にもコンストラクタ・デフォルトメンバ初期化子・メンバ関数などが使えるため、Cに比べて初期化の自由度が高くなります。
クラス・構造体・共用体のかっこ初期化

C++11以降では、構造体(struct)、クラス(class)、共用体(union)のすべてに対して{ }によるかっこ初期化が可能です。
#include <iostream>
struct S {
int x;
int y;
};
class C {
public:
int x;
int y;
};
union U {
int i;
float f;
};
int main() {
S s{1, 2}; // 集成体初期化
C c{3, 4}; // C++14以降: 集成体クラスならOK
U u{.f = 1.5f}; // C++20以降: 指定初期化も利用可能
std::cout << "S: " << s.x << "," << s.y << "\n";
std::cout << "C: " << c.x << "," << c.y << "\n";
std::cout << "U as float: " << u.f << "\n";
}
S: 1,2
C: 3,4
U as float: 1.5
C++ではコンストラクタの有無やメンバのpublic/privateなどにより、「集成体(aggregate)」として扱われるかどうかが決まります。
集成体であれば、C風の{ }初期化がそのまま使えます。
C++における構造体とクラスの違いと初期化の注意点

C++におけるstructとclassの主な違いは、デフォルトのアクセス指定子と継承指定子だけです。
struct… デフォルトpublicclass… デフォルトprivate
それ以外の面では、どちらもコンストラクタ・デストラクタ・メンバ関数・演算子オーバーロードなどを持てます。
初期化については、次の点に注意が必要です。
- コンストラクタを自前で定義すると、その型は「集成体」ではなくなるため、
Foo f = {1, 2};のようなC風の初期化ができなくなる場合があります。 - 代わりにコンストラクタ引数を使うかっこ初期化
Foo f{1, 2};を用いるのが、C++らしいスタイルです。
struct Foo {
int x;
int y;
};
struct Bar {
int x;
int y;
Bar(int x, int y) : x(x), y(y) {} // コンストラクタ定義
};
int main() {
Foo f1 = {1, 2}; // OK: Fooは集成体
Foo f2{3, 4}; // OK
// Bar b1 = {1, 2}; // C++のバージョンや仕様によってはエラーや非推奨
Bar b2{5, 6}; // コンストラクタを呼び出すC++的な初期化
return 0;
}
CとC++の両方を扱うコードベースでは、どの初期化構文がどの言語・バージョンで有効かを意識しながら記述することが重要です。
まとめ
構造体の初期化は、「どの値がどのメンバに入るか」をコードから一目で理解できるかどうかが最大のポイントです。
Cでは波括弧と指定初期化子を組み合わせ、配列やネストを意識した分かりやすい書き方を心がけると、後から見ても安全で保守しやすいコードになります。
C++では、さらにコンストラクタとかっこ初期化が加わり、構造体を小さなクラスとして設計することも可能です。
この記事のパターンを土台に、自分のプロジェクトに合った初期化スタイルを整えていくと良いでしょう。
