C言語における構造体の基礎:宣言、自己参照、およびメモリ配置の仕組み

1. 構造体型の宣言と基本

構造体は、異なるデータ型の変数を一つの単位としてまとめることができる「値の集合」です。それぞれの構成要素はメンバ変数と呼ばれます。

1.1 構造体の宣言

構造体を定義する際の基本的な構文は以下の通りです。

struct 構造体タグ
{
    メンバリスト;
} 変数リスト;

例えば、社員情報を管理する構造体は次のように定義できます。

struct Employee
{
    char name[30]; // 名前
    int age;       // 年齢
    char gender[10]; // 性別
    char emp_id[15]; // 社員番号
}; // セミコロンが必要です

1.2 変数の生成と初期化

構造体変数は、宣言時の順序に従って初期化することも、メンバを指定して初期化することも可能です。

#include <stdio.h>

struct Employee
{
    char name[30];
    int age;
    char gender[10];
    char emp_id[15];
};

int main()
{
    // 定義済みの順序による初期化
    struct Employee e1 = { "田中太郎", 30, "男性", "E2024001" };
    printf("名前: %s, 年齢: %d\n", e1.name, e1.age);

    // メンバを指定した初期化 (C99以降)
    struct Employee e2 = { .age = 25, .name = "佐藤花子", .emp_id = "E2024002", .gender = "女性" };
    printf("名前: %s, ID: %s\n", e2.name, e2.emp_id);

    return 0;
}

2. 特殊な構造体宣言

2.1 匿名構造体

タグ名を省略して構造体を宣言することができます。これを匿名構造体と呼びます。

struct
{
    int id;
    char type;
    float value;
} obj;

struct
{
    int id;
    char type;
    float value;
} *ptr;

注意点として、コンパイラは上記の2つの宣言を「全く別の型」として認識します。そのため、ptr = &obj; という代入は型不一致の警告またはエラーになります。匿名構造体は再利用が難しいため、通常はその場限りの変数宣言に使用されます。

2.2 構造体の自己参照

構造体の中に、自分自身と同じ型のメンバを含める必要がある場合があります(例:連結リスト)。しかし、構造体そのものをメンバに含めることはできません。サイズが無限に計算されてしまうためです。

// 誤った例
struct Node
{
    int data;
    struct Node next; // コンパイルエラー
};

// 正しい例(ポインタを使用する)
struct Node
{
    int data;
    struct Node* next;
};

また、typedef を使用して匿名構造体をリネームする場合、その内部でリネーム後の名前を使用することはできません。

// 誤った例
typedef struct
{
    int data;
    Node* next; // Nodeが未定義のためエラー
} Node;

// 正しい例
typedef struct Node
{
    int data;
    struct Node* next;
} Node;

3. 構造体のメモリ配置とアライメント

構造体のサイズを計算する際、単純なメンバの合計値にはなりません。これは、CPUのアクセス効率を高めるために「メモリ配置(アライメント)」が行われるからです。

3.1 アライメントの規則

  1. 最初のメンバは、構造体の開始アドレス(オフセット0)に配置されます。
  2. 2番目以降のメンバは、そのメンバの「アライメント数」の整数倍のオフセットに配置されます。
    • アライメント数 = コンパイラのデフォルト値(VSでは8)メンバ自体のサイズ のうち、小さい方。
  3. 構造体全体のサイズは、構造体内の最大のメンバのアライメント数の整数倍になるよう調整されます。
  4. 構造体が入れ子になっている場合、内部の構造体は自身の最大アライメント数の整数倍のオフセットに配置されます。

3.2 実践的なサイズ計算の例

以下のコードを実行した際の結果を考察します(デフォルトアライメントが8の場合)。

// 例1
struct Layout1
{
    char v1;   // 1バイト。オフセット0
    // 3バイトのパディング
    int v2;    // 4バイト。オフセット4 (4の倍数)
    char v3;   // 1バイト。オフセット8
};
// 最大アライメントは4。現在のサイズ9を4の倍数に調整し、12バイト。

// 例2
struct Layout2
{
    char v1;   // 1バイト。オフセット0
    char v2;   // 1バイト。オフセット1
    // 2バイトのパディング
    int v3;    // 4バイト。オフセット4 (4の倍数)
};
// 最大アライメントは4。合計8バイト。

// 例3
struct Layout3
{
    double d1; // 8バイト。オフセット0
    char c1;   // 1バイト。オフセット8
    // 3バイトのパディング
    int i1;    // 4バイト。オフセット12 (4の倍数)
};
// 最大アライメントは8。合計16バイト。

// 例4 (入れ子構造)
struct Layout4
{
    char c1;            // 1バイト。オフセット0
    // 7バイトのパディング
    struct Layout3 l3;  // 16バイト。オフセット8 (内部の最大アライメント8の倍数)
    double d1;          // 8バイト。オフセット24
};
// 最大アライメントは8。合計32バイト。

構造体の設計において、メモリ使用量を最小限に抑えたい場合は、アライメントによる隙間(パディング)を減らすために、サイズが大きいメンバから順に宣言するなどの工夫が有効です。

タグ: C言語 データ構造 メモリ管理 構造体 アライメント

5月27日 02:32 投稿