第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