構造体の応用

構造体の応用は家の内装のようなものです。骨組みを建てるだけでなく、効率的な空間利用(アライメント)、材料の節約(ビットフィールド)、柔軟な拡張(フレキシブル配列)も重要です。

構造体と関数の応用

構造体を返す関数

C言語では関数が構造体を直接返せます。コンパイラがコピーを処理します:

C
struct Point make_point(int x, int y) {
    struct Point p = {x, y};
    return p;
}

int main(void) {
    struct Point pt = make_point(3, 4);
    printf("(%d, %d)\n", pt.x, pt.y);
    return 0;
}
💡 ヒント: 小さな構造体を値で返すのは問題ありません。大きな構造体の場合は、ポインタを渡して埋めることでコピーオーバーヘッドを避けてください。

ポインタを通じて構造体を埋める

C
void fill_student(struct Student *s, const char *name, int age, float score) {
    strncpy(s->name, name, 19);
    s->name[19] = '\0';
    s->age = age;
    s->score = score;
}

int main(void) {
    struct Student stu;
    fill_student(&stu, "Zhang", 20, 88.5);
    return 0;
}

構造体引数のconst保護

関数が構造体を変更しないようにするには、ポインタ引数に const を付けます:

C
void print_student(const struct Student *s) {
    printf("%s %d %.1f\n", s->name, s->age, s->score);
}
⚠️ 注意: const struct Student *s は、sを通じて指し先の構造体を変更できないことを意味しますが、s自体は別のものを指すことができます。

typedefによる型エイリアス

typedef は型にエイリアスを作成し、struct キーワードを繰り返し書く手間を省きます。

基本的な使い方

C
typedef struct {
    char name[20];
    int age;
    float score;
} Student;

Student s1 = {"Zhang", 20, 89.5};
Student *ps = &s1;
💡 ヒント: もう struct Student と書く必要はなく、Student を直接使えます。

構造体ポインタとのtypedef

C
typedef struct Node {
    int data;
    struct Node *next;
} Node, *NodePtr;

Node n1 = {10, NULL};
NodePtr head = &n1;
⚠️ 注意: 構造体が自分自身を参照する場合、まだエイリアスが有効になっていないため struct Node *next と書く必要があります。

typedefのその他の用途

C
typedef unsigned char Byte;
typedef int (*Comparator)(const void *, const void *);

Byte flag = 0xFF;
Comparator cmp = my_compare;

C
#include <stdio.h>
#include <string.h>

typedef struct {
    char title[50];
    int pages;
    float price;
} Book;

Book create_book(const char *title, int pages, float price) {
    Book b;
    strncpy(b.title, title, 49);
    b.title[49] = '\0';
    b.pages = pages;
    b.price = price;
    return b;
}

void discount(Book *b, float rate) {
    b->price *= rate;
}

void print_book(const Book *b) {
    printf("<%s> %d pages $%.2f\n", b->title, b->pages, b->price);
}

int main(void) {
    Book b1 = create_book("C Programming", 320, 59.0);
    print_book(&b1);
    discount(&b1, 0.8);
    print_book(&b1);
    return 0;
}
▶ 試してみよう
TEXT
<C Programming> 320 pages $59.00
<C Programming> 320 pages $47.20

構造体のメモリアライメント

構造体メンバはメモリ上で密に詰められるわけではありません。コンパイラはアライメント規則に従ってパディングバイトを挿入します。

アライメント規則

  1. 各メンバのオフセットはそのメンバのサイズの倍数でなければなりません
  2. 構造体の合計サイズは最も大きいメンバのサイズの倍数でなければなりません
C
struct Align1 {
    char a;
    int b;
    char c;
};

struct Align2 {
    char a;
    char c;
    int b;
};
C
printf("%zu\n", sizeof(struct Align1));
printf("%zu\n", sizeof(struct Align2));
TEXT
12
8

Align1 のレイアウト:a(1バイト)+3バイトのパディング+b(4バイト)+c(1バイト)+3バイトのパディング=12。

Align2 のレイアウト:a(1バイト)+c(1バイト)+2バイトのパディング+b(4バイト)=8。

💡 ヒント: 小さいメンバをまとめて配置するとパディングバイトが減り、メモリの節約に繋がります。

#pragma pack

アライメント境界を指定して構造体を圧縮できます:

C
#pragma pack(push, 1)
struct Packed {
    char a;
    int b;
    char c;
};
#pragma pack(pop)

printf("%zu\n", sizeof(struct Packed));
TEXT
6
⚠️ 注意: パック済みアライメントはアクセス効率が低下し、一部のプラットフォームではエラーの原因にもなります。プロトコル解析やファイルフォーマット処理など、バイトアライメントが厳密に要求される場面でのみ使用してください。

ビットフィールド

ビットフィールドはメンバにビット単位で空間を割り当て、メモリを節約します。

C
struct Flags {
    unsigned int ready : 1;
    unsigned int error : 1;
    unsigned int mode  : 3;
    unsigned int       : 0;
    unsigned int count : 12;
};
C
struct Flags f = {1, 0, 5, 1024};
printf("ready=%u error=%u mode=%u count=%u\n", f.ready, f.error, f.mode, f.count);
printf("Struct size: %zu\n", sizeof(f));
TEXT
ready=1 error=0 mode=5 count=1024
Struct size: 8
⚠️ 注意: ビットフィールドのレイアウトはコンパイラに依存し、移植性がありません。ビットフィールドメンバのアドレスは取得できません(&f.ready は不正です)。

フレキシブル配列メンバ

C99では、構造体の最後のメンバを長さ0の配列にでき、これをフレキシブル配列メンバと呼びます。

C
typedef struct {
    int len;
    int data[];
} IntVec;

使用時には必要に応じて追加の空間を割り当てます:

C
int n = 5;
IntVec *v = (IntVec *)malloc(sizeof(IntVec) + sizeof(int) * n);
v->len = n;
for (int i = 0; i < n; i++) {
    v->data[i] = i * 100;
}
for (int i = 0; i < v->len; i++) {
    printf("%d ", v->data[i]);
}
free(v);
TEXT
0 100 200 300 400
💡 ヒント: フレキシブル配列メンバは可変長構造体の実装によく使われます。ポインタで別メモリを指すのに比べ、データが連続しており、free1回で済みます。

⚠️ 注意: フレキシブル配列は最後のメンバでなければならず、構造体には少なくとももう1つのメンバが必要です。フレキシブル配列に sizeof は使えません。

❓ よくある質問

Q typedefと#defineで型エイリアスを作る違いは何ですか?
A typedefはコンパイラによって処理され、スコープ規則に従い、ポインタ型も正しく扱えます。#defineはプリプロセッサのテキスト置換であり、微妙なエラーを引き起こす可能性があります。typedefを優先してください。
Q なぜメモリアライメントが存在するのですか?
A CPUはアライメントされた境界でメモリを読み取る方が効率的であり、一部のアーキテクチャでは非アライメントアクセスがフォールトを引き起こします。コンパイラは互換性とパフォーマンスのために自動的にパディングを追加します。
Q ビットフィールドのアドレスは取得できますか?
A いいえ。ビットフィールドメンバは1バイトより小さい場合があり、独立したアドレスを持ちません。& 演算子はビットフィールドに使えません。
Q フレキシブル配列メンバとポインタメンバはどちらがよいですか?
A フレキシブル配列はデータが連続しており、割り当てと解放が1回ずつで済み、キャッシュにも優れています。ポインタメンバはどこでも指せて柔軟性が高いですが、割り当てと解放が2回ずつ必要です。フレキシブル配列の方がシンプルな選択です。

📖 まとめ

📝 練習問題

  1. char、short、int、doubleを含む構造体を定義し、2つの異なるメンバ順序を試してsizeofでサイズの違いを確認してください
  2. typedefを使って連結リストのノード型を定義し、単方向連結リストを作成・走査する関数を作成してください
  3. フレキシブル配列メンバを使って動的文字列構造体(lenとdata[]を持つ)を実装し、文字追加操作に対応させてください
100%