閉じる

C言語の構造体アライメントとパディング入門【サイズずれ防止】

C言語で構造体を使っていると、「思ったよりサイズが大きい」「メモリ配置が想定と違う」と感じることがあります。

これはコンパイラが自動的に行うアライメント(配置の揃え)とパディング(隙間の埋め草)が原因です。

本記事では、図解とサンプルコードを使いながら、C言語の構造体アライメントとパディングの基本を丁寧に解説し、サイズずれを防ぐ実践的なコツを紹介します。

C言語におけるアライメントとは

メモリアクセスとアライメントの基本

コンピュータの多くのCPUは、特定サイズのデータを「そのサイズの倍数」のアドレスに置くと高速にアクセスできるように設計されています。

これがアライメント(境界揃え)です。

例えば、一般的な32ビット環境では、次のようなルールを持つことが多いです。

  • int(4バイト)はアドレスが4の倍数の位置に置きたい
  • short(2バイト)はアドレスが2の倍数の位置に置きたい
  • char(1バイト)はどこに置いてもよい

これを満たすように、コンパイラは構造体メンバの間に「使われない隙間(パディング)」を自動で挿入します。

構造体とアライメントの関係

構造体のメモリ配置は、概ね次の方針で決まります。

  1. 各メンバはその型のアライメント制約を満たすように配置される
  2. メンバ同士の間に必要ならパディングが入る
  3. 構造体全体のサイズも、最大のアライメント制約に揃えて丸められる

この「3」が重要で、配列にしたときに各要素が正しく揃うために必要な処理です。

パディングとは何か

パディングが入る典型例

次のような構造体を考えます。

C言語
#include <stdio.h>

struct Sample {
    char c;   // 1バイト
    int  i;   // 4バイト (4バイト境界に置きたい)
};

int main(void) {
    struct Sample s;
    printf("sizeof(struct Sample) = %zu\n", sizeof(struct Sample));
    return 0;
}

多くの環境では、この出力は次のようになります。

実行結果
sizeof(struct Sample) = 8

メンバの合計サイズは1 + 4 = 5バイトですが、実際のサイズは8バイトになっています。

これは次のような配置になっているからです。

  • c が先頭のアドレス(例えば0)に配置される
  • 次のアドレス1はintの要件である「4の倍数」ではないので、そのままでは配置できない
  • アドレス4までの「3バイト分」がパディングとして空けられる
  • アドレス4からiが4バイト分配置される
  • 構造体全体も4の倍数になるように8バイトに丸められる

このようにメンバの間に挿入される使われない領域がパディングです。

アライメントとパディングのルール(典型的なケース)

環境によって違いはありますが、典型的な32ビット/64ビット環境では、次のような傾向があります。

  • 型ごとのアライメント制約の例
    • char: 1バイト境界
    • short: 2バイト境界
    • int, float: 4バイト境界
    • double, ポインタ: 8バイト境界(64bit環境の場合が多い)
  • 構造体サイズの丸め方
    構造体のサイズは、その構造体の中で「最大のアライメント制約」を持つメンバのアライメントに揃えられます。

構造体サイズの具体例で学ぶ

例1: メンバ順序によるサイズの違い

次の2つの構造体を比べてみます。

C言語
#include <stdio.h>

// メンバ順序その1
struct A {
    char c;   // 1バイト
    int  i;   // 4バイト
    char d;   // 1バイト
};

// メンバ順序その2
struct B {
    int  i;   // 4バイト
    char c;   // 1バイト
    char d;   // 1バイト
};

int main(void) {
    printf("sizeof(struct A) = %zu\n", sizeof(struct A));
    printf("sizeof(struct B) = %zu\n", sizeof(struct B));
    return 0;
}

例えば多くの64bit環境では、次のような出力になることが多いです。

実行結果
sizeof(struct A) = 12
sizeof(struct B) = 8

理由を解説します。

struct A の配置(例)

  • オフセット0: c(1バイト)
  • オフセット1〜3: 3バイトのパディング (次のintを4の倍数に揃えるため)
  • オフセット4〜7: i(4バイト)
  • オフセット8: d(1バイト)
  • オフセット9〜11: 3バイトのパディング (構造体全体を4の倍数に揃えるため)

合計で12バイトになります。

struct B の配置(例)

  • オフセット0〜3: i(4バイト)
  • オフセット4: c(1バイト)
  • オフセット5: d(1バイト)
  • オフセット6〜7: 2バイトのパディング (構造体全体を4の倍数に揃えるため)

合計で8バイトで済みます。

メンバの順番だけで4バイトも差が出ることがわかります。

パディングの確認方法

offsetofマクロとアドレス表示で配置を確認する

実際にメンバがどのオフセットに配置されているかを調べるにはoffsetofマクロが便利です。

C言語
#include <stdio.h>
#include <stddef.h> // offsetofマクロを使うために必要

struct A {
    char c;
    int  i;
    char d;
};

int main(void) {
    printf("sizeof(struct A) = %zu\n", sizeof(struct A));
    printf("offsetof(A, c) = %zu\n", offsetof(struct A, c));
    printf("offsetof(A, i) = %zu\n", offsetof(struct A, i));
    printf("offsetof(A, d) = %zu\n", offsetof(struct A, d));
    return 0;
}
実行結果
sizeof(struct A) = 12
offsetof(A, c) = 0
offsetof(A, i) = 4
offsetof(A, d) = 8

また、変数のアドレスを表示すると、実アドレスの間隔も確認できます。

C言語
#include <stdio.h>

struct A {
    char c;
    int  i;
    char d;
};

int main(void) {
    struct A s;

    printf("&s      = %p\n", (void *)&s);
    printf("&s.c    = %p\n", (void *)&s.c);
    printf("&s.i    = %p\n", (void *)&s.i);
    printf("&s.d    = %p\n", (void *)&s.d);

    return 0;
}

&sを基準に各メンバのアドレスを引き算することで、オフセットと一致することが確認できます。

パディングを減らすためのメンバ順序設計

コツ1: サイズの大きい型から並べる

一般的なテクニックは「アライメントの大きい型から順に並べる」ことです。

具体的には次のような順番を意識します。

  • 8バイト境界を要求する型(double, ポインタなど)
  • 4バイト境界を要求する型(int, floatなど)
  • 2バイト境界を要求する型(shortなど)
  • 1バイト境界を要求する型(char, uint8_tなど)

例として、次のような構造体を考えます。

C言語
#include <stdio.h>

// 悪い例: 小さい型と大きい型が交互
struct Bad {
    char   c1;
    double d;
    char   c2;
    int    i;
};

// 良い例: 大きい型から順に並べる
struct Good {
    double d;
    int    i;
    char   c1;
    char   c2;
};

int main(void) {
    printf("sizeof(struct Bad)  = %zu\n", sizeof(struct Bad));
    printf("sizeof(struct Good) = %zu\n", sizeof(struct Good));
    return 0;
}

想定出力の一例は次のようになります。

実行結果
sizeof(struct Bad)  = 24
sizeof(struct Good) = 16

同じ情報量でも、メンバの並び順だけで8バイト節約できています。

コツ2: よく使うグループごとに再設計する

パディング削減のためだけに、無理やり1つの構造体の中で順序をいじると、コードの可読性が落ちることがあります。

その場合は「論理的なグループごとに構造体を分ける」方法も考えられます。

例えば、位置情報と状態フラグを同じ構造体に無理に押し込まず、それぞれ別構造体にしてから、それらをまとめる構造体を作るような分割設計です。

このようにすると、パディングを抑えつつ、意味的にもわかりやすい構造になります。

pack指定・pragmaによる強制的な詰め込み

#pragma packや属性指定の概要

コンパイラ独自の拡張として、「パディングを入れないで詰め込む」指定があります。

代表的なものは次のようなものです。

  • MSVCなど: #pragma pack(push, 1)#pragma pack(pop)
  • GCC/Clangなど: __attribute__((packed))

例としてGCC/Clangでの書き方を示します。

C言語
#include <stdio.h>

// 通常の配置
struct Normal {
    char c;
    int  i;
    char d;
} ;

// packed属性で詰め込み配置を指示
struct __attribute__((packed)) Packed {
    char c;
    int  i;
    char d;
};

int main(void) {
    printf("sizeof(struct Normal) = %zu\n", sizeof(struct Normal));
    printf("sizeof(struct Packed) = %zu\n", sizeof(struct Packed));
    return 0;
}
実行結果
sizeof(struct Normal) = 12
sizeof(struct Packed) = 6

Packed構造体ではパディングが一切入らず、メンバの合計サイズと構造体サイズが一致していることがわかります。

pack指定の注意点

packやpacked属性は便利ですが、乱用すると危険です。

主な注意点を挙げます。

  • CPUによっては、アライメントされていないメモリアクセスが
    • 遅くなる
    • 例外(バスエラー)になる
  • 一部のプラットフォームでは、非アライメントアクセスをソフトウェアでエミュレートするため、大きく性能が低下します
  • ライブラリやOSが期待する構造体レイアウトとずれると、互換性問題の原因になります

そのためpack指定は「プロトコルやファイルフォーマットとのバイト単位一致が絶対に必要なとき」に限定して使うのが無難です。

基本的な最適化は、メンバ順序の工夫で行うことをおすすめします。

配列と構造体サイズの関係

構造体サイズが配列に与える影響

構造体のサイズは、その構造体の配列を作成したときに直接効いてきます。

例えばsizeof(struct A) = 12の構造体の配列struct A arr[1000];を宣言すると、少なくとも12,000バイトを消費します。

一方、メンバ順序を工夫してsizeof(struct B) = 8にできれば、同じ1000要素でも8,000バイトで済みます。

1要素あたりのパディングは、要素数に比例して全体の無駄なメモリとなるため、配列で大量に扱う構造体では特にアライメントとパディングを意識する価値があります。

実践的なまとめ方と設計指針

よくあるパターン別の考え方

構造体設計時には、次のような観点でアライメントとパディングを意識するとよいです。

  • 通信プロトコルやバイナリファイルのヘッダを表現するとき
    → バイト単位でレイアウトを一致させる必要があるため、pack指定や固定幅整数型(uint8_t, uint16_tなど)を用いることが多いです。ただし、アクセス時のアライメント違反に注意します。
  • メモリに大量に展開するゲームオブジェクトや物理シミュレーションの構造体
    性能とメモリ使用量のバランスが重要です。メンバ順序の最適化を第一に考え、必要なら構造体の分割やSoA(Array of Structures)への変更も検討します。
  • 通常のビジネスロジック用の構造体
    → まずは可読性や保守性を優先し、明らかに問題になるサイズでなければコンパイラ任せでも構いません。問題が出てから、局所的に最適化します。

まとめ

構造体のアライメントとパディングを理解すると、C言語のメモリレイアウトを正確に把握でき、サイズずれや予期しない挙動を防げます。

ポイントは、各型が持つアライメント制約を意識し、メンバ順序を工夫することです。

さらに、必要に応じてoffsetofで配置を確認し、pack指定は「プロトコルとの整合が必須な場合」に限定して慎重に使うと、安全かつ効率的な構造体設計ができます。

配列で大量に扱う構造体ほどパディングの影響は大きくなりますので、本記事の考え方を踏まえて、自分のプロジェクトのデータ構造を一度見直してみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!