構造体のサイズが、メンバの合計より大きくなるのはなぜでしょうか。
答えは、CPUがデータへ効率よくアクセスするための配置ルールにあります。
本記事ではアライメントとパディングをやさしく説明し、構造体のサイズや並びの見え方がスッキリ理解できるように解説します。
初心者向けに、最小限のルールとコツに絞って進めます。
構造体のメモリ基礎とバイト境界
用語の整理
まずは用語を短く揃えておくと全体像がつかみやすくなります。
ここでは直感的に理解できる説明に絞ります。
バイトはメモリの最小単位で、アドレスで位置が表せます。
1バイトずつアドレスが増え、0x1000の次は0x1001というように並びます。
オフセットは、基準位置からの距離です。
構造体の先頭から何バイト目かを示す数字がオフセットです。
アライメントは、型ごとに決まったバイト境界へデータを揃えるルールです。
例えば4バイト境界に揃えるとは、アドレスが4の倍数の位置に置くことです。
パディングは、アライメントのために挿入される隙間の埋め草です。
プログラムからは使われないけれど、サイズには含まれる余白のバイトです。
バイト境界の基本
「Nバイト境界」とは、アドレスがNの倍数である位置を指します。
例えば0x1000は8バイト境界にも4バイト境界にもありますが、0x1004は4バイト境界で、8バイト境界ではありません。
CPUは特定の型を特定の境界へ置くと速く安全に読めるように設計されています。
次の表は、境界の言い方のイメージです。
倍数で考えると迷いにくくなります。
| 境界サイズ | 意味の要約 | 例のアドレス |
|---|---|---|
| 1バイト境界 | どこでもよい | 0x1001, 0x1002 |
| 2バイト境界 | 2の倍数に置く | 0x1000, 0x1002 |
| 4バイト境界 | 4の倍数に置く | 0x1000, 0x1004 |
| 8バイト境界 | 8の倍数に置く | 0x1000, 0x1008 |
構造体の並びとオフセットのイメージ
構造体の各メンバは、それぞれのアライメントが満たされるように並べられます。
その結果、メンバの間に余白が入ることがあります。
例として次の構造体を考えます。
- struct Example { char c; int i; short s; }
一般的な環境ではcharは1バイト境界、intは4バイト境界、shortは2バイト境界に置かれます。
配置のイメージとオフセットは次のようになります。
| オフセット | 内容 | バイト数 | 備考 |
|---|---|---|---|
| 0 | c | 1 | charは1バイト境界でOK |
| 1〜3 | パディング | 3 | 次のintを4の倍数に揃えるため |
| 4〜7 | i | 4 | intは4バイト境界 |
| 8〜9 | s | 2 | shortは2バイト境界 |
| 10〜11 | 末尾パディング | 2 | 構造体全体を4の倍数に揃えるため |
この場合、合計サイズは12バイトになり、メンバの合計1+4+2=7バイトより大きくなります。
アライメントとは
メモリアライメントの意味と目的
アライメントとは、CPUが扱いやすい境界にデータを置くことで、アクセスの正確性と性能を確保する仕組みです。
これによりCPUは1回の読み書きで値を取得しやすくなり、思わぬ遅延や例外を避けられます。
型ごとの境界サイズの目安
多くの環境での目安として、型のサイズと同じか近い境界に揃えられることが多いです。
以下は代表例です。
実際はコンパイラとCPUに依存します。
| 型の例 | 典型サイズ 32bit | 典型サイズ 64bit | 典型アライメント |
|---|---|---|---|
| char | 1 | 1 | 1 |
| short | 2 | 2 | 2 |
| int | 4 | 4 | 4 |
| float | 4 | 4 | 4 |
| double | 8 | 8 | 8 |
| ポインタ | 4 | 8 | 4 または 8 |
表はあくまで目安です。
実機やコンパイラで必ず確認してください。
不明なときはsizeofとalignofで測るのが一番確実です。
アライメントとアクセス効率の関係
データが適切に揃っていると、CPUは少ないメモリアクセスで値を読み出せるため高速です。
一方で、ズレた場所にあると追加の読み書きが発生したり、まれにハードウェアで禁止され例外になることもあります。
通常のCコードで自然な並びに任せていれば問題は起きにくいです。
C言語のアライメント
Cでは、各メンバはその型のアライメントに従って配置され、構造体全体のアライメントはメンバの中で最大のものになります。
また構造体サイズは、そのアライメントの倍数になるように末尾にパディングされます。
C11以降ではalignof演算子と_Alignofが使えます。
指定したい場合はalignasや_Alignasで強制できますが、初心者のうちは「ルールを理解して配置を工夫する」方が安全です。
packedやpragmaでアライメントを崩す指定は、読み書きが遅くなったり未定義動作の原因になるため、必要になるまで避けましょう。
まずは自然な配置に任せ、観測して学ぶのが近道です。
パディングとは
メモリパディングの仕組み
パディングは、次のメンバを正しい境界に乗せるために自動で挿入される余白バイトです。
プログラムから直接使うことはなく、値は意味を持ちません。
読み書きの対象はあくまでメンバそのものです。
パディングに入っているビットの内容は未規定です。
値を当てにしたり、構造体全体をバイト比較して同一性を判定するのは避けましょう。
常に「パディングは余白」と考えるのが安全です。
フィールド間パディングと末尾パディング
パディングには、メンバ同士の間に入るものと、構造体の末尾に足されるものの2種類があります。
前者は次のメンバの境界合わせ、後者は構造体自体のアライメント合わせのためです。
例えばcharの後にintを置くと、intを4の倍数に揃えるために数バイトの隙間が入ることがあります。
この「隙間」は使えないけれど、sizeofにはカウントされます。
sizeofに現れるサイズ差
sizeofで得られる構造体のサイズは、メンバの合計サイズより大きくなることがあります。
これはフィールド間パディングと末尾パディングの合計が加わるためです。
例として、charとdoubleをこの順に並べると、doubleを8バイト境界に置く都合で間に7バイトのパディングが挟まるケースがあります。
よくある配置例と注意点
小さな型の後に大きな型を置くとパディングが増えがち、というのが初心者にとって最重要の注意点です。
charやshortを先頭に並べるより、doubleやポインタなど大きい型を先に置くと、パディングを減らしやすくなります。
ビットフィールドなどは別ルールが絡むため、最初は避けると理解が進みます。
構造体のメモリ配置のコツ
フィールド順序でパディングを減らす
基本の並べ方は「大きい型から小さい型へ」です。
これだけで多くのケースでパディングが減ります。
例えば次の2通りを比べると違いが分かります。
- 悪い並びの例: char c; double d; int i;
- 良い並びの例: double d; int i; char c;
良い並びの方が、間に挟まるパディングが少なくなり、sizeofが小さくなることが多いです。
同じサイズの型をまとめるコツ
同じサイズのフィールドを連続させると、次のフィールドのために追加のパディングが入りにくくなります。
例えば、intとintを続けて置けば、その間にパディングは通常不要です。
charを複数使う場合は、配列char name[16]のようにまとめると管理もしやすくなります。
32bit 64bit環境の違いの目安
32bitと64bitで特に変わりやすいのはポインタとlong doubleなど一部の型です。
多くの環境ではポインタが32bitでは4バイト、64bitでは8バイトになります。
そのため、ポインタを含む構造体は64bit環境で大きくなりやすいです。
クロスプラットフォームを意識する場合は、サイズに依存しない設計や実測確認が大切です。
サイズとアライメントの確認方法
最も確実なのは、実際にコンパイルしてsizeofとalignof、offsetofで観測することです。
C11ならstdalign.hのalignofが使えます。
#include <stdio.h>
#include <stddef.h> // offsetof
#include <stdalign.h> // alignof in C11
struct S {
char c;
int i;
double d;
};
int main(void) {
printf("sizeof(struct S) = %zu\n", sizeof(struct S));
printf("alignof(struct S) = %zu\n", alignof(struct S));
printf("offsetof(S, c) = %zu\n", offsetof(struct S, c));
printf("offsetof(S, i) = %zu\n", offsetof(struct S, i));
printf("offsetof(S, d) = %zu\n", offsetof(struct S, d));
return 0;
}
出力を見れば、どこにパディングが入り、構造体全体がどの境界に揃えられているかが一目で分かります。
古い環境では_Alignofやコンパイラ拡張が必要なことがあります。
まとめ
アライメントはデータを適切なバイト境界に置くルール、パディングはそのために生まれる余白で、sizeofに現れる差の正体です。
多くの構造体では「大きい型から小さい型へ」「同サイズをまとめる」という並び替えだけで、無駄なパディングを減らせます。
さらに、32bitと64bitでポインタなどのサイズが変わる点を意識し、実際にsizeofやalignof、offsetofで確かめる習慣を持てば、見積もり違いに悩まされにくくなります。
まずは手元の環境で観測し、結果とルールを結び付けて覚えることが、理解への最短ルートです。
