構造体(struct)
| 対応: | C89(1989) |
|---|
異なる型のデータをひとまとめにして扱うためのデータ構造です。名前・年齢・点数など関連するデータをグループ化することで、プログラムの構造が明確になります。
構文
// 構造体型を定義します。
struct 構造体タグ名 {
型 メンバ名1;
型 メンバ名2;
};
// 構造体変数を宣言します。
struct 構造体タグ名 変数名;
// 宣言と同時に初期化します。
struct 構造体タグ名 変数名 = {値1, 値2};
// メンバへのアクセスです。
変数名.メンバ名 // 変数の場合はドット演算子
ポインタ変数->メンバ名 // ポインタの場合はアロー演算子
// typedef と組み合わせて struct を省略できます。
typedef struct {
型 メンバ名;
} 型名;
構造体操作のまとめ
| 操作 | 構文 | 概要 |
|---|---|---|
| メンバアクセス | 変数.メンバ名 | 構造体変数のメンバに直接アクセスします。 |
| ポインタ経由のアクセス | ポインタ->メンバ名 | 『(*p).メンバ』の省略形です。ポインタで構造体を操作する際に使います。 |
| 初期化子 | {.メンバ名 = 値} | C99以降では指定初期化子(designated initializer)が使えます。 |
| 構造体の代入 | s1 = s2 | 同じ型の構造体同士はコピー代入できます(メンバが全部コピーされます)。 |
| 構造体のサイズ | sizeof(struct タグ名) | 構造体全体のバイト数を返します。パディングが入ることがあります。 |
サンプルコード
sample_struct.c
#include <stdio.h>
#include <string.h>
typedef struct {
char name[50];
int age;
double score;
} Student;
/* 構造体ポインタを引数に受け取って値を設定します。 */
void init_student(Student *s, const char *name, int age, double score) {
strncpy(s->name, name, sizeof(s->name) - 1);
s->name[sizeof(s->name) - 1] = '\0';
s->age = age;
s->score = score;
}
/* 構造体を値渡しで受け取って表示します。 */
void print_student(Student s) {
printf("名前: %s, 年齢: %d, 点数: %.1f\n", s.name, s.age, s.score);
}
int main(void) {
Student s1 = {"碇シンジ", 14, 85.5};
print_student(s1);
s1.score = 90.0;
printf("更新後の点数: %.1f\n", s1.score);
/* ポインタ経由のアクセスにはアロー演算子を使います。 */
Student s2;
Student *p = &s2;
init_student(p, "綾波レイ", 14, 92.0);
printf("名前: %s\n", p->name);
printf("点数: %.1f\n", p->score);
Student class[3] = {
{"碇シンジ", 14, 85.5},
{"綾波レイ", 14, 92.0},
{"惣流アスカ", 14, 78.0},
};
for (int i = 0; i < 3; i++) {
print_student(class[i]);
}
printf("Student のサイズ: %zu バイト\n", sizeof(Student));
return 0;
}
コンパイルして実行すると次のようになります。
gcc struct.c -o struct ./struct 名前: 碇シンジ, 年齢: 14, 点数: 85.5 更新後の点数: 90.0 名前: 綾波レイ 点数: 92.0 名前: 碇シンジ, 年齢: 14, 点数: 85.5 名前: 綾波レイ, 年齢: 14, 点数: 92.0 名前: 惣流アスカ, 年齢: 14, 点数: 78.0 Student のサイズ: 64 バイト
ネストした構造体と関数への渡し方
構造体の中に構造体を入れることで、複合的なデータを表現できます。大きな構造体を関数に渡す場合はポインタを使うとコピーを避けられます。
struct_nested.c
#include <stdio.h>
#include <string.h>
typedef struct {
int x;
int y;
} Vec2;
typedef struct {
char name[32];
Vec2 pos;
int hp;
} Character;
/* ポインタ渡し: コピーせず直接変更します。 */
void move(Character *c, int dx, int dy) {
c->pos.x += dx;
c->pos.y += dy;
}
/* const ポインタ渡し: 変更せずに読み取るだけです。 */
void show(const Character *c) {
printf("%s: pos=(%d,%d) hp=%d\n", c->name, c->pos.x, c->pos.y, c->hp);
}
int main(void) {
Character shinji = {"碇シンジ", {0, 0}, 100};
show(&shinji);
move(&shinji, 3, -2);
show(&shinji);
/* 構造体同士のコピー代入です(全メンバがコピーされます)。 */
Character rei = shinji;
strncpy(rei.name, "綾波レイ", sizeof(rei.name) - 1);
rei.pos.x = 10;
show(&rei);
show(&shinji); /* shinji は変更されていません。 */
return 0;
}
コンパイルして実行すると次のようになります。
gcc struct_nested.c -o struct_nested ./struct_nested 碇シンジ: pos=(0,0) hp=100 碇シンジ: pos=(3,-2) hp=100 綾波レイ: pos=(10,-2) hp=100 碇シンジ: pos=(3,-2) hp=100
よくあるミス
よくあるミス: ポインタ渡しと値渡しの混同
構造体を値渡しで渡すと呼び出し元のデータは変更されません。変更を反映したい場合はポインタで渡す必要があります。
struct_pass_ng.c
#include <stdio.h>
typedef struct { int hp; } Unit;
/* NG: 値渡しなので呼び出し元が変わらない */
void heal_ng(Unit u, int amount) {
u.hp += amount;
}
/* OK: ポインタ渡しなので呼び出し元が変わる */
void heal_ok(Unit *u, int amount) {
u->hp += amount;
}
int main(void) {
Unit asuka = {50};
heal_ng(asuka, 30);
printf("NG(値渡し後): hp = %d\n", asuka.hp); /* 50 のまま */
heal_ok(&asuka, 30);
printf("OK(ポインタ渡し後): hp = %d\n", asuka.hp); /* 80 になる */
return 0;
}
修正後は次の通りです。
gcc struct_pass_ng.c -o struct_pass_ng ./struct_pass_ng NG(値渡し後): hp = 50 OK(ポインタ渡し後): hp = 80
概要
構造体を関数に渡すと全メンバがコピーされます。大きな構造体の場合は、コピーを避けるためポインタで渡す方が効率的です。ポインタで構造体を受け取った場合、呼び出し元の構造体が変更されます。変更を望まない場合は引数を『const 構造体型 *』と宣言してください。
構造体のメモリサイズはアライメントの都合でパディングが挿入されることがあるため、単純にメンバサイズの合計にはなりません。正確なサイズは『sizeof』で確認してください。
複数の型でメモリを共有する場合は『union(共用体)』を、型名の別名については『typedef』を参照してください。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。