第8章: 構造体と共用体
8.1 構造体の理論
積型(Product Type)
構造体は積型(Product Type)の一種である。代数的データ型の観点から:
$$\text{struct } \{A, B\} \cong A \times B$$
つまり、構造体の取りうる値の数は、各メンバの取りうる値の数の積である。
struct Point {
char x; /* 256通り */
char y; /* 256通り */
};
/* Point構造体は 256 × 256 = 65536 通りの値を取りうる */
レコードとしての構造体
構造体は異種データの集約を可能にする:
struct Person {
char name[50];
int age;
double height;
};
/* 名前、年齢、身長を1つの単位として扱える */
8.2 構造体の定義と宣言
基本的な定義
/* 構造体タグを定義 */
struct Point {
int x;
int y;
};
/* 変数を宣言 */
struct Point p1;
struct Point p2 = {10, 20};
typedefとの組み合わせ
/* パターン1: 別々に定義 */
struct Point {
int x;
int y;
};
typedef struct Point Point;
/* パターン2: 同時に定義 */
typedef struct Point {
int x;
int y;
} Point;
/* パターン3: タグなしで定義 */
typedef struct {
int x;
int y;
} Point;
/* 使用: struct キーワード不要 */
Point p = {10, 20};
自己参照構造体
/* 連結リストのノード */
struct Node {
int data;
struct Node *next; /* 自己参照 */
};
/* typedefを使う場合は前方宣言が必要 */
typedef struct Node Node;
struct Node {
int data;
Node *next;
};
8.3 構造体のメモリレイアウト
アラインメントとパディング
struct Example {
char a; /* 1バイト */
/* 3バイトのパディング */
int b; /* 4バイト */
char c; /* 1バイト */
/* 3バイトのパディング */
};
sizeof(struct Example); /* 通常12バイト(6バイトではない) */
メモリレイアウト:
アドレス: 0 1 2 3 4 5 6 7 8 9 10 11
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ a │pad│pad│pad│ b │ b │ c │pad│pad│pad│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
パディングを最小化
/* メンバをサイズの大きい順に並べる */
struct Optimized {
int b; /* 4バイト */
char a; /* 1バイト */
char c; /* 1バイト */
/* 2バイトのパディング */
};
sizeof(struct Optimized); /* 8バイト */
パッキング
/* GCC/Clang: パッキング属性 */
struct __attribute__((packed)) Packed {
char a;
int b;
char c;
};
sizeof(struct Packed); /* 6バイト(パディングなし) */
/* 注意: パックされた構造体へのアクセスは遅くなる可能性がある */
/* また、アラインメント違反によるクラッシュの可能性もある(一部のアーキテクチャ) */
offsetof マクロ
#include <stddef.h>
struct Example {
char a;
int b;
char c;
};
size_t offset_a = offsetof(struct Example, a); /* 0 */
size_t offset_b = offsetof(struct Example, b); /* 4(通常) */
size_t offset_c = offsetof(struct Example, c); /* 8(通常) */
8.4 構造体の操作
メンバアクセス
struct Point p = {10, 20};
/* ドット演算子 */
int x = p.x;
p.y = 30;
/* アロー演算子(ポインタ経由) */
struct Point *pp = &p;
int x2 = pp->x; /* (*pp).x と同じ */
pp->y = 40;
代入とコピー
struct Point p1 = {10, 20};
struct Point p2;
/* 構造体の代入(メンバごとのコピー) */
p2 = p1; /* OK: 全メンバがコピーされる */
/* 比較は演算子でできない */
if (p1 == p2) { } /* エラー! */
/* memcmpを使用(注意: パディングの問題) */
if (memcmp(&p1, &p2, sizeof(struct Point)) == 0) { }
初期化
/* 位置による初期化 */
struct Point p1 = {10, 20};
/* 指定初期化子(C99) */
struct Point p2 = {.x = 10, .y = 20};
struct Point p3 = {.y = 20, .x = 10}; /* 順序は任意 */
/* 部分初期化(残りは0) */
struct Point p4 = {.x = 10}; /* y = 0 */
/* 複合リテラル(C99) */
struct Point p5 = (struct Point){10, 20};
8.5 構造体と関数
値渡し
/* 値渡し: 構造体全体がコピーされる */
void print_point(struct Point p)
{
printf("(%d, %d)\n", p.x, p.y);
}
/* 大きな構造体では非効率 */
ポインタ渡し
/* ポインタ渡し: 効率的 */
void print_point(const struct Point *p)
{
printf("(%d, %d)\n", p->x, p->y);
}
/* 変更する場合 */
void move_point(struct Point *p, int dx, int dy)
{
p->x += dx;
p->y += dy;
}
構造体を返す
struct Point create_point(int x, int y)
{
struct Point p = {x, y};
return p; /* 構造体全体をコピーして返す */
}
/* C99: 複合リテラルで直接返す */
struct Point create_point_c99(int x, int y)
{
return (struct Point){x, y};
}
8.6 ビットフィールド
定義と使用
struct Flags {
unsigned int read : 1; /* 1ビット */
unsigned int write : 1; /* 1ビット */
unsigned int exec : 1; /* 1ビット */
unsigned int mode : 4; /* 4ビット */
};
struct Flags f = {1, 1, 0, 5};
f.read = 0;
注意点
/* ビットフィールドのアドレスは取得できない */
struct Flags f;
int *p = &f.read; /* エラー! */
/* レイアウトは処理系依存 */
/* 移植性が必要な場合は使用を避ける */
8.7 共用体(Union)
共用体の理論
共用体は和型(Sum Type)に似ているが、C言語ではタグがない:
union Data {
int i;
float f;
char str[20];
};
/* 全メンバが同じメモリを共有 */
sizeof(union Data); /* 最大メンバのサイズ(20バイト) */
メモリレイアウト
union Data のレイアウト:
┌────────────────────────────────────┐
i: │ int (4 bytes) │
├────────────────────────────────────┤
f: │ float (4 bytes) │
├────────────────────────────────────┤
str: │ char[20] │
└────────────────────────────────────┘
全て同じアドレスから始まる
型パニング
/* floatのビットパターンをintとして見る */
union FloatBits {
float f;
uint32_t bits;
};
union FloatBits u;
u.f = 3.14f;
printf("Bits: 0x%08X\n", u.bits);
/* 注意: 最後に書き込んだメンバ以外を読むことは、
C99では「実装定義」の動作 */
タグ付き共用体
/* 判別子(discriminator)を使って型を追跡 */
enum DataType { TYPE_INT, TYPE_FLOAT, TYPE_STRING };
struct TaggedUnion {
enum DataType type;
union {
int i;
float f;
char str[20];
} data;
};
void print_data(const struct TaggedUnion *tu)
{
switch (tu->type) {
case TYPE_INT:
printf("%d\n", tu->data.i);
break;
case TYPE_FLOAT:
printf("%f\n", tu->data.f);
break;
case TYPE_STRING:
printf("%s\n", tu->data.str);
break;
}
}
8.8 無名構造体と共用体(C11)
struct Outer {
int tag;
union { /* 無名共用体 */
int i;
float f;
};
};
struct Outer o;
o.i = 42; /* 直接アクセス可能 */
8.9 柔軟な配列メンバ(C99)
struct FlexArray {
size_t size;
int data[]; /* 柔軟な配列メンバ(最後のメンバのみ) */
};
/* 使用法 */
struct FlexArray *fa = malloc(sizeof(struct FlexArray) + sizeof(int) * 10);
fa->size = 10;
for (int i = 0; i < 10; i++)
fa->data[i] = i;
/* サイズ */
sizeof(struct FlexArray); /* dataは含まない */
8.10 設計パターン
不透明ポインタ(Opaque Pointer)
/* header.h */
struct Handle; /* 前方宣言のみ */
typedef struct Handle *Handle;
Handle create_handle(void);
void destroy_handle(Handle h);
int use_handle(Handle h);
/* source.c */
struct Handle {
int private_data;
/* 実装の詳細 */
};
/* クライアントコードはHandleの内部を見れない */
VTable(仮想関数テーブル)
/* C言語でのポリモーフィズム */
struct Shape;
struct ShapeVTable {
double (*area)(const struct Shape *);
void (*draw)(const struct Shape *);
};
struct Shape {
const struct ShapeVTable *vtable;
};
struct Circle {
struct Shape base;
double radius;
};
double circle_area(const struct Shape *s)
{
const struct Circle *c = (const struct Circle *)s;
return 3.14159 * c->radius * c->radius;
}
const struct ShapeVTable circle_vtable = {
.area = circle_area,
.draw = circle_draw
};
/* 使用 */
struct Circle c = {{&circle_vtable}, 5.0};
double a = c.base.vtable->area(&c.base);
8.11 まとめ
本章では、構造体と共用体について学んだ:
- 構造体: 積型、異種データの集約
- メモリレイアウト: アラインメント、パディング
- 操作: メンバアクセス、代入、初期化
- 関数との連携: 値渡し、ポインタ渡し
- ビットフィールド: ビットレベルの制御
- 共用体: メモリ共有、型パニング
- 設計パターン: 不透明ポインタ、VTable
- ISO/IEC 9899:2011, Programming languages — C
- Kernighan, B. W., & Ritchie, D. M. (1988). "The C Programming Language", 2nd Edition, Prentice Hall
- Stroustrup, B. (1994). "The Design and Evolution of C++", Addison-Wesley
次章では、プリプロセッサについて学ぶ。
---