C言語で変数のサイズを知る:sizeof演算子の基本

はい、承知いたしました。C言語のsizeof演算子に関する詳細な記事を記述します。約5000語を目指し、基本的な使い方から応用、注意点まで網羅します。


C言語で変数のサイズを知る:sizeof演算子の基本と応用

C言語は、プログラミング言語の中でも特にハードウェアに近いレベルでの制御が可能であり、メモリ管理はプログラミングの重要な側面の一つです。メモリ効率の良いプログラムを作成したり、特定のデータ構造のサイズを正確に把握したりする上で、sizeof演算子は不可欠なツールとなります。

この記事では、C言語のsizeof演算子について、その基本的な使い方から、配列、構造体、共用体といった複合型における挙動、ポインタとの関係、そしてメモリ上のアライメントとパディングといった高度な概念まで、詳細かつ網羅的に解説します。約5000語を費やし、sizeof演算子に関するあなたの理解を深めることを目的とします。

1. はじめに:なぜサイズの把握が重要なのか?

コンピュータのメモリは、バイトと呼ばれる単位で管理されています。プログラムが扱うデータ(変数)は、このメモリ上に配置されます。データ型によって、メモリ上で占める領域のサイズ(バイト数)が異なります。例えば、char型は通常1バイトですが、int型は処理系によって2バイト、4バイト、あるいはそれ以上になることがあります。

なぜ変数のサイズを知ることが重要なのでしょうか?

  1. メモリ管理と効率:

    • プログラムが使用するメモリ量を正確に把握することで、メモリ不足を防いだり、効率的なメモリ使用を実現したりできます。
    • 特に組み込みシステムやリソースが限られた環境では、各変数がどれだけのメモリを消費するのかを知ることが極めて重要です。
    • 動的メモリ割り当て(malloc, callocなど)を行う際に、必要なメモリサイズを計算するためにsizeofは必須です。例えば、10個のint型変数のためのメモリを確保するには、10 * sizeof(int)バイトが必要です。
  2. データ構造の設計と理解:

    • 構造体や共用体といった複合データ型を設計する際、全体のサイズがどのように決定されるかを理解することは、メモリレイアウトを把握する上で不可欠です。これには、後述するアライメントやパディングといった概念が関わってきます。
  3. 移植性:

    • C言語の基本データ型のサイズは、標準によって最小サイズが規定されているものの、具体的なサイズは処理系(コンパイラと実行環境)によって異なります。例えば、int型が32ビットシステムでは4バイト、古い16ビットシステムでは2バイトであることがあります。sizeof演算子を使うことで、特定のデータ型のサイズに依存しない、移植性の高いコードを書くことができます。例えば、ある型の配列を処理するループを書く際に、要素数を得るために配列の全サイズを要素のサイズで割る、といった手法は、型の実際のサイズを知らなくても機能します。
  4. データ処理と通信:

    • ファイルを読み書きしたり、ネットワークを通じてデータを送受信したりする場合、データのバイト単位での正確なサイズを知る必要があります。特に、異なるシステム間でデータをやり取りする際には、バイトオーダーやデータ型のサイズに関する知識が重要になります。
  5. ポインタ演算:

    • ポインタが指すデータの型サイズを理解することは、ポインタ演算(例: ptr + 1がどれだけアドレスを進めるか)を正しく行うために必要です。sizeofは、ポインタが指す型のサイズを決定するのに役立ちます。

このように、sizeof演算子はC言語プログラミングにおいて、メモリ、データ構造、移植性、効率といった様々な側面に関わる fundamental な(基本的な)ツールです。

2. sizeof演算子の基本:使い方と戻り値

sizeofは、C言語の演算子であり、関数ではありません(関数呼び出しのように見える形 sizeof(型名)sizeof(式) で使われますが、多くの場合、コンパイル時に評価される定数式となります。VLA (Variable Length Array) の場合は実行時に評価される例外もあります)。

sizeof演算子は、以下の2つの形式で使用できます。

  1. sizeof(型名): 指定したデータ型(基本型、構造体、共用体、配列など)がメモリ上で占めるバイト数を返します。
  2. sizeof(式): 式の評価結果となるオブジェクト(変数など)がメモリ上で占めるバイト数を返します。この場合、式自体は実際に評価されません。つまり、式に副作用(例えばインクリメント演算子など)が含まれていても、その副作用は発生しません。

sizeof演算子の戻り値の型は、標準で定義されている符号なし整数型であるsize_tです。size_t型は、システム上で表現可能な最大オブジェクトサイズを保持できる十分な大きさを持つように定義されており、通常はunsigned intunsigned longの別名です。size_t型は <stddef.h> ヘッダファイルで定義されています。

2.1. 基本データ型のサイズを知る

最も基本的な使い方は、int, char, float, doubleなどの基本データ型のサイズを知ることです。

“`c

include

include // size_t のために必要(多くの場合、stdio.h など他のヘッダでインクルードされますが、明示的に含めると安全です)

int main() {
printf(“sizeof(char) = %zu bytes\n”, sizeof(char));
printf(“sizeof(short) = %zu bytes\n”, sizeof(short));
printf(“sizeof(int) = %zu bytes\n”, sizeof(int));
printf(“sizeof(long) = %zu bytes\n”, sizeof(long));
printf(“sizeof(long long) = %zu bytes\n”, sizeof(long long));
printf(“sizeof(float) = %zu bytes\n”, sizeof(float));
printf(“sizeof(double) = %zu bytes\n”, sizeof(double));
printf(“sizeof(long double) = %zu bytes\n”, sizeof(long double));
printf(“sizeof(void) = %zu bytes\n”, sizeof(void)); // voidポインタのサイズ(ポインタのサイズ)
printf(“sizeof(size_t) = %zu bytes\n”, sizeof(size_t));

// C99 以降で利用可能な場合
// printf("sizeof(_Bool) = %zu bytes\n", sizeof(_Bool)); // または sizeof(bool) if <stdbool.h> is included

int i;
double d;
printf("sizeof(i) = %zu bytes\n", sizeof(i)); // 変数 i のサイズ
printf("sizeof(d) = %zu bytes\n", sizeof(d)); // 変数 d のサイズ

return 0;

}
“`

上記のコードを実行すると、実行環境によって異なる結果が出力されます。一般的な64ビットシステム(LP64データモデル)では、以下のような出力になることが多いです。

sizeof(char) = 1 bytes
sizeof(short) = 2 bytes
sizeof(int) = 4 bytes
sizeof(long) = 8 bytes
sizeof(long long) = 8 bytes
sizeof(float) = 4 bytes
sizeof(double) = 8 bytes
sizeof(long double) = 16 bytes (または 12, 8 など環境による)
sizeof(void*) = 8 bytes
sizeof(size_t) = 8 bytes
sizeof(i) = 4 bytes
sizeof(d) = 8 bytes

注意点:

  • char型は標準で1バイトと定義されています。1バイトが何ビットであるかは、システムによって異なる可能性がありますが、通常は8ビットです(CHAR_BITマクロで定義されます)。sizeofの戻り値はこのchar1個分を基準としたバイト数です。
  • 他の整数型 (short, int, long, long long) や浮動小数点型 (float, double, long double) のサイズは、標準によって最小サイズや関係性 (sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)) が定められているものの、具体的なバイト数は処理系に依存します。したがって、例えば「intは常に4バイトである」と仮定するべきではありません。
  • ポインタ型 (void*, int*など) のサイズは、それが指すデータの型に関わらず、ポインタ変数自体のアドレスを格納するために必要なサイズです。これは通常、システムのメモリアドレス空間のサイズに依存し、32ビットシステムでは4バイト、64ビットシステムでは8バイトであることが多いです。

2.2. printfでのsize_tの書式指定子

上記の例でprintf%zuという書式指定子を使っています。これはC99で導入されたsize_t型のための書式指定子です。もし古いC89環境を使用している場合、size_tunsigned intであれば%uunsigned longであれば%luを使う必要があります。移植性を最大限に高めるためには、size_t型の値をunsigned longunsigned long longにキャストしてから、それぞれの書式指定子 (%lu%llu) を使うという方法もありますが、現代的なC言語では%zuが一般的です。

3. sizeof演算子と配列

配列は同じ型の要素が連続してメモリに配置されたデータ構造です。配列にsizeofを適用すると、配列全体が占めるバイト数が得られます。

“`c

include

int main() {
int numbers[10]; // 10個のint要素を持つ配列

// 配列全体のサイズ
size_t array_size = sizeof(numbers);
printf("Size of the entire array 'numbers': %zu bytes\n", array_size);

// 配列の単一要素のサイズ
size_t element_size = sizeof(numbers[0]); // または sizeof(int)
printf("Size of a single element: %zu bytes\n", element_size);

// 配列の要素数を計算する
// 要素数 = 配列全体のサイズ / 単一要素のサイズ
size_t num_elements = array_size / element_size;
printf("Number of elements in the array: %zu\n", num_elements);

// あるいは、より直接的に
size_t num_elements_direct = sizeof(numbers) / sizeof(numbers[0]);
printf("Number of elements (direct calculation): %zu\n", num_elements_direct);

return 0;

}
“`

この例では、numbersという10個のint要素を持つ配列を定義しています。

  • sizeof(numbers)は配列全体のサイズ、つまり 10 * sizeof(int) バイトを返します。
  • sizeof(numbers[0])は配列の最初の要素のサイズ、つまりsizeof(int)バイトを返します。配列の全ての要素は同じ型なので、sizeof(numbers[i])も同じ結果になります。また、sizeof(int)と直接記述しても同じです。

これらの情報を使って、sizeof(配列名) / sizeof(配列名[0])という計算によって、配列の要素数を簡単に、かつ移植性高く求めることができます。これは、特に固定サイズの配列の要素数を求める際の一般的なイディオムです。

3.1. 配列とポインタの落とし穴:関数引数としての配列

C言語では、関数に配列を渡す際に、配列は自動的にポインタに変換(decay – 崩壊)されます。これは、関数の仮引数としてint arr[]int *arrと宣言した場合、コンパイラにとっては全く同じ意味になるということです。どちらも「int型へのポインタ」を受け取る、と解釈されます。

この「配列のポインタへの変換」は、sizeof演算子の挙動に大きな影響を与えます。関数内で配列の仮引数にsizeofを適用すると、それは元の配列全体のサイズではなく、ポインタ変数のサイズを返します。

“`c

include

// 関数 f1 は配列を受け取るように見えるが、実際にはポインタを受け取る
void f1(int arr[]) {
printf(“Inside f1: sizeof(arr) = %zu bytes\n”, sizeof(arr)); // これはポインタのサイズになる!
}

// 関数 f2 はポインタを明示的に受け取る
void f2(int *ptr) {
printf(“Inside f2: sizeof(ptr) = %zu bytes\n”, sizeof(ptr)); // これはポインタのサイズ
}

int main() {
int my_array[10]; // 10個のint要素を持つ配列 (例えば sizeof(int) = 4 なら 40 bytes)

printf("Inside main: sizeof(my_array) = %zu bytes\n", sizeof(my_array)); // 配列全体のサイズ
printf("Inside main: sizeof(my_array[0]) = %zu bytes\n", sizeof(my_array[0])); // 要素のサイズ
printf("Inside main: Number of elements = %zu\n", sizeof(my_array) / sizeof(my_array[0])); // 正しく要素数を計算

printf("-- Calling functions --\n");
f1(my_array); // 配列を関数に渡す
f2(my_array); // 配列を関数に渡す (配列名は先頭要素へのポインタとして扱われる)

return 0;

}
“`

実行例(64ビットシステム、sizeof(int)=4, sizeof(int*)=8の場合):

Inside main: sizeof(my_array) = 40 bytes
Inside main: sizeof(my_array[0]) = 4 bytes
Inside main: Number of elements = 10
-- Calling functions --
Inside f1: sizeof(arr) = 8 bytes // !! ポインタのサイズになっている !!
Inside f2: sizeof(ptr) = 8 bytes // ポインタのサイズ

この例からわかるように、main関数内でsizeof(my_array)は配列全体のサイズである40バイトを正しく返していますが、f1関数内のsizeof(arr)は、引数arrがポインタとして扱われるため、ポインタのサイズである8バイトを返しています。f2関数も同様です。

結論として、関数に配列を渡す場合、関数内でsizeofを使って元の配列のサイズや要素数を正確に知ることはできません。 したがって、配列を関数で処理する際には、配列のサイズや要素数を別の引数として渡すのが一般的な方法です。

“`c

include

// 配列と要素数を引数として受け取る関数
void process_array(int arr[], size_t num_elements) {
printf(“Inside process_array: Received array with %zu elements\n”, num_elements);
// ここでは arr の sizeof はポインタサイズだが、num_elements を使って処理を行う
}

int main() {
int my_array[10];
size_t num_elements = sizeof(my_array) / sizeof(my_array[0]);

process_array(my_array, num_elements);

return 0;

}
“`

このポインタへの変換のルールは、文字列リテラルにも適用されます。文字列リテラルは、文字の配列として扱われますが、関数の引数として渡される場合や、特定の文脈(例: char *s = "hello"; のようにポインタに代入する場合)では、先頭要素へのポインタに変換されます。

4. sizeof演算子と構造体・共用体

構造体(struct)や共用体(union)は、複数の異なる型のメンバをまとめた複合データ型です。これらの型にsizeofを適用した場合の挙動は、単にメンバのサイズを合計しただけでは済まない複雑さを含んでいます。これは、メモリ上のアライメント(Alignment)パディング(Padding)という概念が関係してくるためです。

4.1. 構造体のサイズ:アライメントとパディング

構造体のサイズは、原則として各メンバが必要とするアライメント要件を満たしつつ、全体として構造体自身のアライメント要件も満たすように決定されます。アライメントとは、特定の型のデータがメモリ上の特定のアドレス(例えば、アドレスが4の倍数である場所など)に配置される必要があるという制約です。CPUが効率的にメモリにアクセスするために、このような制約が存在することが一般的です。

コンパイラは、アライメント要件を満たすために、メンバ間にパディングバイトと呼ばれる未使用の領域を挿入することがあります。また、構造体の末尾にも、配列になった場合に次の要素が正しくアライメントされるようにパディングが追加されることがあります。

構造体のサイズは、以下の要因によって影響を受けます。

  1. 各メンバのサイズ: 各メンバが占める基本的なバイト数。
  2. 各メンバのアライメント要件: 各メンバが配置されるべきメモリアドレスの制約(例: intは4バイト境界に配置されるべき、doubleは8バイト境界に配置されるべきなど)。
  3. 構造体全体のアライメント要件: 構造体自体が配列になった場合などに、要素の開始アドレスが満たすべき制約。これは通常、構造体のメンバの中で最も厳しいアライメント要件を持つメンバのアライメントに等しくなります。

具体的な例を見てみましょう。

“`c

include

include // offsetof マクロのために必要

struct S1 {
char c1;
int i;
char c2;
};

struct S2 {
int i;
char c1;
char c2;
};

struct S3 {
char c;
double d;
int i;
};

int main() {
printf(“sizeof(char) = %zu\n”, sizeof(char)); // 1
printf(“sizeof(int) = %zu\n”, sizeof(int)); // e.g., 4
printf(“sizeof(double) = %zu\n”, sizeof(double)); // e.g., 8

printf("sizeof(struct S1) = %zu\n", sizeof(struct S1)); // メンバの合計は 1 + 4 + 1 = 6
printf("sizeof(struct S2) = %zu\n", sizeof(struct S2)); // メンバの合計は 4 + 1 + 1 = 6
printf("sizeof(struct S3) = %zu\n", sizeof(struct S3)); // メンバの合計は 1 + 8 + 4 = 13

return 0;

}
“`

実行結果の例(64ビットシステム、int=4バイト, double=8バイト, アライメント要件がそれぞれ4と8の場合):

sizeof(char) = 1
sizeof(int) = 4
sizeof(double) = 8
sizeof(struct S1) = 12 // 期待される合計 6 ではない
sizeof(struct S2) = 8 // 期待される合計 6 ではない
sizeof(struct S3) = 24 // 期待される合計 13 ではない

なぜ合計サイズと異なるのでしょうか? メモリレイアウトを考えてみます。

struct S1:
struct S1 {
char c1; // 1 byte
// Padding (3 bytes) - int (4 bytes) のアライメント要件(4バイト境界)を満たすため
int i; // 4 bytes
char c2; // 1 byte
// Padding (3 bytes) - 構造体全体のアライメント要件(4バイト境界)を満たすため (1 + 3 + 4 + 1 + 3 = 12)
};

* c1 (1バイト) が配置されます。
* 次のi (4バイト) は、多くの場合4バイト境界に配置される必要があります。c1の直後ではアライメントが合わないため、コンパイラは3バイトのパディングを挿入します。
* i (4バイト) が配置されます。
* 次のc2 (1バイト) が配置されます。
* 構造体の合計サイズは、最も厳しいメンバのアライメント(ここではintの4バイト)の倍数である必要があります。現在 1 + 3 + 4 + 1 = 9 バイトですが、4の倍数ではありません。次の4の倍数は12なので、さらに3バイトのパディングが最後に挿入され、合計サイズは12バイトになります。

struct S2:
struct S2 {
int i; // 4 bytes
char c1; // 1 byte
char c2; // 1 byte
// Padding (2 bytes) - 構造体全体のアライメント要件(4バイト境界)を満たすため (4 + 1 + 1 + 2 = 8)
};

* i (4バイト) が配置されます(通常、構造体の開始アドレスは適切なアライメントを持つため、最初のメンバは追加のパディングなしで配置されることが多いです)。
* 次にc1 (1バイト) が配置されます。
* 次にc2 (1バイト) が配置されます。
* 現在の合計は 4 + 1 + 1 = 6 バイトです。構造体全体のアライメント要件(4バイト境界)を満たすため、サイズは4の倍数である必要があります。次の4の倍数は8なので、2バイトのパディングが最後に挿入され、合計サイズは8バイトになります。

struct S3:
struct S3 {
char c; // 1 byte
// Padding (7 bytes) - double (8 bytes) のアライメント要件(8バイト境界)を満たすため
double d; // 8 bytes
int i; // 4 bytes
// Padding (4 bytes) - 構造体全体のアライメント要件(8バイト境界)を満たすため (1 + 7 + 8 + 4 + 4 = 24)
};

* c (1バイト) が配置されます。
* 次のd (8バイト) は、多くの場合8バイト境界に配置される必要があります。cの直後ではアライメントが合わないため、7バイトのパディングが挿入されます。
* d (8バイト) が配置されます。
* 次のi (4バイト) が配置されます。
* 現在の合計は 1 + 7 + 8 + 4 = 20 バイトです。構造体全体のアライメント要件は、最も厳しいメンバ(ここではdoubleの8バイト)に合わせられます。サイズは8の倍数である必要があります。20の次に大きい8の倍数は24なので、4バイトのパディングが最後に挿入され、合計サイズは24バイトになります。

このように、メンバの順序を変えることで構造体のサイズが変わる可能性があることがわかります(S1S2のように、メンバの合計サイズは同じ6バイトですが、構造体全体のサイズは異なります)。メモリを節約するためには、アライメント要件の大きいメンバから順に宣言すると良い場合が多いです。

offsetofマクロ(<stddef.h>で定義)を使うと、構造体の先頭から各メンバまでのオフセット(バイト単位)を知ることができます。これは、パディングがどこに挿入されているかを理解するのに役立ちます。

“`c

include

include

struct S1 {
char c1;
int i;
char c2;
};

int main() {
printf(“Offset of c1: %zu bytes\n”, offsetof(struct S1, c1));
printf(“Offset of i: %zu bytes\n”, offsetof(struct S1, i));
printf(“Offset of c2: %zu bytes\n”, offsetof(struct S1, c2));
printf(“Size of struct S1: %zu bytes\n”, sizeof(struct S1));

return 0;

}
“`

実行例(上記と同様の環境):

Offset of c1: 0 bytes
Offset of i: 4 bytes // c1 (1バイト) + padding (3バイト) = 4バイト
Offset of c2: 8 bytes // i (4バイト) を含めると 4 + 4 = 8バイト
Size of struct S1: 12 bytes

offsetof(struct S1, i)が4であることから、c1の後に3バイトのパディングがあることが推測できます。c2のオフセットが8で、c2自体のサイズは1なので、c2の終わりは9バイト目です。sizeof(struct S1)が12であることから、最後に12 – 9 = 3バイトのパディングがあることがわかります。

4.2. 共用体のサイズ

共用体(union)は、複数のメンバが同じメモリ領域を共有するデータ構造です。共用体のサイズは、最も大きいメンバのサイズに等しくなります。これは、どのメンバがアクティブであるかにかかわらず、全てのメンバを保持できるだけの領域が必要だからです。アライメント要件も構造体と同様に考慮され、共用体全体のアライメントは最も厳しいメンバのアライメントに合わせられ、サイズもそのアライメントの倍数になるようにパディングされることがあります。

“`c

include

union Data {
int i;
float f;
char c;
};

union Mixed {
int i; // e.g., 4 bytes, alignment 4
double d; // e.g., 8 bytes, alignment 8
char arr[10]; // 10 bytes, alignment 1
};

int main() {
printf(“sizeof(int) = %zu\n”, sizeof(int));
printf(“sizeof(float) = %zu\n”, sizeof(float));
printf(“sizeof(char) = %zu\n”, sizeof(char));

// 最も大きいメンバは float (4 bytes) か int (4 bytes) - 環境による
printf("sizeof(union Data) = %zu\n", sizeof(union Data));

printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(double) = %zu\n", sizeof(double));
printf("sizeof(char[10]) = %zu\n", sizeof(char[10]));

// 最も大きいメンバは char[10] (10 bytes)。
// しかし、最も厳しいアライメントは double (8 bytes)
printf("sizeof(union Mixed) = %zu\n", sizeof(union Mixed));

return 0;

}
“`

実行例(上記と同様の環境、sizeof(int)=4, sizeof(float)=4, sizeof(double)=8):

“`
sizeof(int) = 4
sizeof(float) = 4
sizeof(char) = 1
sizeof(union Data) = 4 // 最も大きいメンバは int または float (どちらも4)

sizeof(int) = 4
sizeof(double) = 8
sizeof(char[10]) = 10
sizeof(union Mixed) = 16 // 最も大きいメンバは char[10] (10)だが、doubleのアライメント(8)の倍数にするため16になる可能性がある
“`

union Dataの場合、intfloatが同じサイズ(例えば4バイト)であれば、共用体のサイズは4バイトになります。

union Mixedの場合、メンバのサイズはint (4), double (8), char[10] (10) です。最も大きいメンバはchar[10]の10バイトです。しかし、共用体全体のアライメントは最も厳しいメンバ(通常doubleの8バイト)に合わせられる必要があるため、サイズは8の倍数である必要が生じます。10バイトでは8の倍数ではないため、コンパイラはパディングを追加し、次に大きい8の倍数である16バイトにすることがあります。

このように、共用体のサイズも単純な最大メンバサイズではなく、アライメントによって影響を受ける可能性があります。

5. sizeof演算子とポインタ

ポインタはメモリアドレスを保持するための変数です。sizeof演算子をポインタに適用した場合、それはポインタが指しているデータのサイズではなく、ポインタ変数自体が占めるメモリのサイズを返します。

“`c

include

int main() {
int i = 10;
int *pi = &i; // int へのポインタ

double d = 3.14;
double *pd = &d; // double へのポインタ

char c = 'A';
char *pc = &c; // char へのポインタ

printf("sizeof(int*) = %zu bytes\n", sizeof(int*));     // int へのポインタのサイズ
printf("sizeof(double*) = %zu bytes\n", sizeof(double*)); // double へのポインタのサイズ
printf("sizeof(char*) = %zu bytes\n", sizeof(char*));    // char へのポインタのサイズ
printf("sizeof(void*) = %zu bytes\n", sizeof(void*));    // void へのポインタのサイズ

printf("sizeof(pi) = %zu bytes\n", sizeof(pi)); // 変数 pi (int*) のサイズ
printf("sizeof(pd) = %zu bytes\n", sizeof(pd)); // 変数 pd (double*) のサイズ
printf("sizeof(pc) = %zu bytes\n", sizeof(pc)); // 変数 pc (char*) のサイズ

// ポインタが指すデータのサイズを知りたい場合(デリファレンス)
printf("sizeof(*pi) = %zu bytes\n", sizeof(*pi)); // pi が指す int のサイズ
printf("sizeof(*pd) = %zu bytes\n", sizeof(*pd)); // pd が指す double のサイズ
printf("sizeof(*pc) = %zu bytes\n", sizeof(*pc)); // pc が指す char のサイズ

return 0;

}
“`

実行例(64ビットシステム、sizeof(int)=4, sizeof(double)=8, sizeof(char)=1, ポインタサイズ=8の場合):

“`
sizeof(int) = 8 bytes
sizeof(double
) = 8 bytes
sizeof(char) = 8 bytes
sizeof(void
) = 8 bytes

sizeof(pi) = 8 bytes
sizeof(pd) = 8 bytes
sizeof(pc) = 8 bytes

sizeof(pi) = 4 bytes // int のサイズ
sizeof(
pd) = 8 bytes // double のサイズ
sizeof(*pc) = 1 bytes // char のサイズ
“`

この結果から、以下の重要な点がわかります。

  • ポインタのサイズ (sizeof(型*) または sizeof(ポインタ変数)) は、それがどの型のデータを指しているかに関わらず、同じです。これは、ポインタが格納しているのはアドレス値であり、そのアドレス空間の大きさに依存するためです。32ビットシステムではアドレス空間が4GBまでなので4バイト、64ビットシステムではそれ以上を扱えるため8バイトとなるのが一般的です。
  • ポインタが指すデータのサイズ (sizeof(*ポインタ変数)) は、そのポインタのベースとなっている型(指しているデータの型)のサイズになります。

5.1. 配列名とポインタの混同再び

配列名の多くの場合における「先頭要素へのポインタへの変換」は、sizeof演算子に関しては例外でした。sizeof(配列名)は配列全体のサイズを返します。

しかし、この例外はsizeof演算子に直接適用された場合に限られます。例えば、配列名に対して&演算子を適用すると、それは「配列全体へのポインタ」となります。

“`c

include

int main() {
int arr[10]; // 10個のintの配列

// 配列全体のサイズ
printf("sizeof(arr) = %zu bytes\n", sizeof(arr)); // 40 (assuming int is 4 bytes)

// 配列の先頭要素へのポインタ (int*)
int *ptr1 = arr;
printf("sizeof(ptr1) = %zu bytes\n", sizeof(ptr1)); // 8 (assuming pointer is 8 bytes)
printf("sizeof(*ptr1) = %zu bytes\n", sizeof(*ptr1)); // 4 (size of int)

// 配列全体へのポインタ (int (*)[10])
int (*ptr2)[10] = &arr;
printf("sizeof(ptr2) = %zu bytes\n", sizeof(ptr2)); // 8 (assuming pointer is 8 bytes)
// *ptr2 は配列全体 (arr) を表す
printf("sizeof(*ptr2) = %zu bytes\n", sizeof(*ptr2)); // 40 (size of the entire array)

return 0;

}
“`

この例では、ptr1int*型であり、sizeof(*ptr1)intのサイズを返します。一方、ptr2int (*)[10]型であり、「10個のintの配列へのポインタ」です。*ptr2は元の配列arr全体を指すことになり、sizeof(*ptr2)は配列全体のサイズを返します。

このようなポインタの型とsizeofの関係は、複雑なデータ構造を扱う際に重要になります。

6. sizeof演算子のその他の応用

6.1. sizeofと文字列リテラル

文字列リテラル(例: "hello")は、最後にヌル終端文字'\0'が付加された文字の配列として扱われます。sizeof演算子を文字列リテラルに適用すると、文字列の文字数 + 1(ヌル終端文字分)のサイズが返されます。

“`c

include

int main() {
printf(“sizeof(\”hello\”) = %zu bytes\n”, sizeof(“hello”)); // “hello” (5文字) + ‘\0’ (1文字) = 6
printf(“sizeof(\”C\”) = %zu bytes\n”, sizeof(“C”)); // “C” (1文字) + ‘\0’ (1文字) = 2
printf(“sizeof(\”\”) = %zu bytes\n”, sizeof(“”)); // “” (0文字) + ‘\0’ (1文字) = 1

char str_array[] = "world"; // "world" + '\0' で初期化される配列
char *str_ptr = "world";   // 文字列リテラルの先頭へのポインタ

printf("sizeof(str_array) = %zu bytes\n", sizeof(str_array)); // 配列全体のサイズ: "world" (5) + '\0' (1) = 6
printf("sizeof(str_ptr) = %zu bytes\n", sizeof(str_ptr));     // ポインタ変数のサイズ (e.g., 8)

return 0;

}
“`

実行例(ポインタサイズ8の場合):

sizeof("hello") = 6 bytes
sizeof("C") = 2 bytes
sizeof("") = 1 bytes
sizeof(str_array) = 6 bytes
sizeof(str_ptr) = 8 bytes

ここで重要なのは、sizeof文字列の長さ(ヌル終端文字を含まない文字数)を知るためのstrlen関数とは異なるということです。strlenは実行時に文字列を走査してヌル終端文字までの文字数をカウントしますが、sizeofはコンパイル時に評価される(文字列リテラルや配列の場合)定数であり、メモリ上の占有サイズを返します。

6.2. sizeofと列挙型(enum)

列挙型(enum)のサイズは、その列挙定数の値を保持できる十分な大きさを持つ整数型のサイズになります。具体的なサイズは処理系に依存しますが、通常はint型と同じサイズになることが多いです。

“`c

include

enum State {
OFF,
ON
};

enum Color {
RED = 1,
GREEN = 2,
BLUE = 4,
YELLOW = 8
};

int main() {
printf(“sizeof(enum State) = %zu bytes\n”, sizeof(enum State));
printf(“sizeof(enum Color) = %zu bytes\n”, sizeof(enum Color));

enum State s = ON;
enum Color c = RED;

printf("sizeof(s) = %zu bytes\n", sizeof(s));
printf("sizeof(c) = %zu bytes\n", sizeof(c));

return 0;

}
“`

実行例(intが4バイトの場合):

sizeof(enum State) = 4 bytes
sizeof(enum Color) = 4 bytes
sizeof(s) = 4 bytes
sizeof(c) = 4 bytes

列挙型のサイズは、そのメンバの値の範囲に応じて、intよりも小さい型(例えばcharshort)や大きい型が選択されることもありますが、標準では具体的にどの整数型に対応するかは定義されていません。多くの処理系ではデフォルトでintが使用されます。

6.3. sizeofvoid

void型は「型無し」を意味し、オブジェクトのサイズを持たないため、sizeof(void)は許可されていません。コンパイルエラーになります。
ただし、void*voidへのポインタ)はポインタ型であり、アドレスを格納するためのサイズを持つため、sizeof(void*)は有効です(ポインタのサイズを返します)。

6.4. sizeofと関数型

関数はデータオブジェクトではないため、sizeof演算子を関数型に直接適用することはできません。

“`c

include

int my_function(int x) {
return x * 2;
}

int main() {
// sizeof(my_function); // コンパイルエラー
// sizeof(int(int)); // コンパイルエラー

// ただし、関数ポインタのサイズは取得可能
printf("sizeof(int (*)(int)) = %zu bytes\n", sizeof(int (*)(int))); // 関数ポインタのサイズ

return 0;

}
“`

sizeof(my_function)sizeof(int(int))は無効ですが、sizeofに関数ポインタ型を渡すことは可能です。これはポインタのサイズ(アドレスを保持するために必要なバイト数)を返します。

6.5. sizeofはコンパイル時演算子(ほぼ)

前述したように、sizeofはほとんどの場合、コンパイル時に評価される定数式です。これは、sizeofの結果を、コンパイル時に値が確定している定数が必要な場面(例えば、静的ストレージ期間を持つ配列のサイズ宣言、caseラベルの値など)で使用できることを意味します。

“`c

include

int main() {
int arr[sizeof(int) * 5]; // sizeof の結果はコンパイル時定数なのでOK

const size_t buffer_size = 1024;
char buffer[buffer_size]; // const 変数は C99 以降で VLA として扱われない限りOK

// case ラベルに sizeof の結果を使用
switch (sizeof(long)) {
    case 4:
        printf("long is 4 bytes\n");
        break;
    case 8:
        printf("long is 8 bytes\n");
        break;
    default:
        printf("long size is %zu bytes\n", sizeof(long));
}

return 0;

}
“`

しかし、C99標準で導入された可変長配列(Variable Length Array – VLA)の場合、配列のサイズが実行時に決定されるため、その配列に対するsizeof演算子は実行時に評価されます。

“`c

include

int main() {
int n;
printf(“Enter array size: “);
scanf(“%d”, &n);

int vla[n]; // 可変長配列 (VLA)

// VLA に対する sizeof は実行時に評価される
printf("Size of VLA: %zu bytes\n", sizeof(vla));
printf("Number of elements in VLA: %zu\n", sizeof(vla) / sizeof(vla[0]));

return 0;

}
“`

この場合、sizeof(vla)の結果は、プログラムが実行されて変数nの値が確定するまで分かりません。VLAに対するsizeofが実行時評価されることは、sizeofが常にコンパイル時定数を返すという一般的なルールにおける重要な例外です。なお、C11標準ではVLAは必須機能ではなくなり、C++では標準機能としてサポートされていません(GCCなどのコンパイラ拡張としてはサポートされることがあります)。

7. メモリのアライメントとパディングに関するより深い考察

構造体や共用体のサイズを理解する上で鍵となるアライメントとパディングについて、もう少し詳しく掘り下げてみましょう。

7.1. なぜアライメントが必要なのか?

現代のコンピュータアーキテクチャでは、CPUがメモリからデータを読み書きする際に、特定のデータ型は特定のアドレス境界(alignment boundary)に配置されている方が効率が良い、あるいは必須である場合があります。例えば、あるアーキテクチャでは、4バイト整数(int)は4の倍数番地(…0x00, 0x04, 0x08, …)に配置されている場合に、1回のメモリ操作で読み書きできます。もし4の倍数でない番地(例: 0x01)から4バイトを読み込もうとすると、CPUは複数のメモリブロックを読み込んでから目的のデータを取り出す必要があり、性能が低下したり、ハードウェアによってはクラッシュしたりすることさえあります。

アライメントの要件はデータ型によって異なります。一般的に、サイズの大きい型や、特定のメモリアクセス命令を持つ型は、より大きなアライメント境界を持つ傾向があります。

  • char: 1バイトアライメント(どのアドレスでもOK)
  • short: 2バイトアライメント(アドレスが2の倍数である必要)
  • int, float: 4バイトアライメント(アドレスが4の倍数である必要)
  • long, double, ポインタ: 8バイトアライメント(アドレスが8の倍数である必要)
  • long double: アーキテクチャによって異なり、8, 12, 16バイトなどのアライメントを持つことがあります。

これらのアライメント要件は、特定のアーキテクチャやコンパイラのオプションによって異なる可能性があります。

7.2. コンパイラによるパディングの挿入

コンパイラは、構造体のメンバをメモリに配置する際に、各メンバのアライメント要件を満たすようにパディングバイトを挿入します。配置のルールは以下のようになります。

  1. 構造体の最初のメンバは、構造体のアライメント境界(通常は、構造体のメンバの中で最も厳しいアライメントを持つメンバのアライメント)に配置されます。構造体自体の開始アドレスは、このアライメント境界に合わせられていると仮定されます。
  2. 各メンバは、そのメンバ自身のアライメント要件を満たすアドレスに配置されます。もし直前のメンバの直後のアドレスがその要件を満たさない場合、その間にパディングバイトが挿入されます。
  3. 構造体の合計サイズは、構造体全体のアライメント境界の倍数になるように調整されます。必要であれば、最後のメンバの後にパディングが挿入されます。これは、この構造体の配列が作成された場合に、配列の各要素が正しくアライメントされることを保証するためです。

例として、再びstruct S1を考えます(char=1, int=4, アライメント要件 char:1, int:4, 構造体:4):

c
struct S1 {
char c1; // offset 0 (align 1)
int i; // offset ? (align 4)
char c2; // offset ? (align 1)
};

  1. c1はオフセット0に配置。
  2. iは4バイトアライメントが必要。現在のオフセットは1。次に4の倍数になるのは4。オフセット1から4まで3バイトのパディングが必要。iはオフセット4に配置。
  3. c2は1バイトアライメントが必要。現在のオフセットは8 (オフセット4のiの終端)。オフセット8は1の倍数なのでパディング不要。c2はオフセット8に配置。c2の終端はオフセット 8+1=9。
  4. 構造体全体のサイズは、構造体のアライメント(4)の倍数である必要。現在のサイズは9。次に大きい4の倍数は12。サイズを12にするために、終端のオフセット9から12まで3バイトのパディングが必要。

最終的なサイズは12バイト。レイアウトは 1(c1) + 3(padding) + 4(i) + 1(c2) + 3(padding) = 12 となります。

struct S2の場合(int=4, char=1, アライメント要件 int:4, char:1, 構造体:4):

c
struct S2 {
int i; // offset ? (align 4)
char c1; // offset ? (align 1)
char c2; // offset ? (align 1)
};

  1. iは4バイトアライメントが必要。最初のメンバなので、構造体のアライメント境界(4)に配置。オフセット0に配置。iの終端はオフセット4。
  2. c1は1バイトアライメントが必要。現在のオフセットは4。オフセット4は1の倍数なのでパディング不要。c1はオフセット4に配置。c1の終端はオフセット 4+1=5。
  3. c2は1バイトアライメントが必要。現在のオフセットは5。オフセット5は1の倍数なのでパディング不要。c2はオフセット5に配置。c2の終端はオフセット 5+1=6。
  4. 構造体全体のサイズは、構造体のアライメント(4)の倍数である必要。現在のサイズは6。次に大きい4の倍数は8。サイズを8にするために、終端のオフセット6から8まで2バイトのパディングが必要。

最終的なサイズは8バイト。レイアウトは 4(i) + 1(c1) + 1(c2) + 2(padding) = 8 となります。

このように、メンバの宣言順序によって、挿入されるパディングの量が変わる可能性があるため、構造体のサイズも変わります。

7.3. パディングを制御する(非標準)

コンパイラによっては、パディングの挿入方法を制御するための特別なディレクティブ(プラグマ)や属性を提供しています。例えば、GCCやClangでは__attribute__((packed))という属性を構造体や共用体に付けることで、パディングを最小限にする、あるいは完全に排除することができます。

“`c

include

struct AlignedS {
char c;
int i;
};

// GCC/Clang 拡張機能: パディングを最小限にする
struct PackedS {
char c;
int i;
} attribute((packed)); // パックされた構造体

int main() {
printf(“sizeof(struct AlignedS) = %zu bytes\n”, sizeof(struct AlignedS)); // 例: 8
printf(“sizeof(struct PackedS) = %zu bytes\n”, sizeof(struct PackedS)); // char(1) + int(4) = 5

return 0;

}
“`

__attribute__((packed))を使用すると、多くの場合、構造体のサイズはメンバの合計サイズに近くなります。ただし、このような機能は標準C言語の範疇を超えた処理系依存の拡張機能です。また、パッキングはメモリ効率を向上させる可能性がある一方で、アライメント要件を満たさないアクセスが発生しやすくなり、性能が低下したり、特定のアーキテクチャでアライメント違反による例外を引き起こしたりするリスクがあります。特に、パックされた構造体内のメンバへのポインタを取得し、そのポインタを通じてアクセスする際には注意が必要です。可能な限り、標準的な方法でアライメントを考慮したデータ構造設計を行うことが推奨されます。

C11標準では、_Alignof演算子(またはalignofマクロ – <stdalign.h>)と_Alignas指定子が導入され、アライメントに関するより標準的な制御手段が提供されました。alignof(Type)はその型の必要なアライメントバイト数を返します。

“`c

include

include // alignof, alignas のために必要 (C11)

int main() {
printf(“alignof(char) = %zu\n”, alignof(char));
printf(“alignof(int) = %zu\n”, alignof(int));
printf(“alignof(double) = %zu\n”, alignof(double));
printf(“alignof(int) = %zu\n”, alignof(int));

struct S {
    char c;
    int i;
};
printf("alignof(struct S) = %zu\n", alignof(struct S)); // 最も厳しいメンバ(int)のアライメントになる傾向

// 特定のアライメントを持つ変数を宣言
alignas(16) int aligned_var;
printf("Alignment of aligned_var: %zu\n", alignof(aligned_var));
// sizeof はアライメント指定子の影響を受けない (オブジェクトのサイズは同じ)
printf("sizeof(aligned_var): %zu\n", sizeof(aligned_var));

return 0;

}
“`

alignof演算子を使うことで、特定の型やオブジェクトがどの程度のアライメントを要求するかを知ることができます。これは、sizeofでサイズを知ることと合わせて、メモリレイアウトを深く理解するのに役立ちます。

8. sizeof演算子に関するよくある間違いと注意点

sizeofは強力ですが、その挙動にはいくつかの落とし穴があります。

  1. 配列とポインタの混同: 関数に配列を渡した際に、仮引数として宣言された配列名のsizeofがポインタのサイズになること。これは最も一般的な間違いの一つです。関数内で配列のサイズや要素数が必要な場合は、別途引数として渡す必要があります。
  2. sizeof vs strlen: 文字列リテラルやchar配列に対して、sizeofがヌル終端文字を含む全体のメモリサイズを返すのに対し、strlenはヌル終端文字 を含まない 文字列の長さ(実行時計算)を返すこと。混同しやすいので注意が必要です。
  3. 動的に割り当てられたメモリ: mallocなどで動的に確保されたメモリ領域は、C言語の型システムから見ると単なるvoid*またはchar*などのポインタとして扱われます。このポインタ変数にsizeofを適用しても、確保されたメモリブロック全体のサイズは分かりません。ポインタ変数自体のサイズが返されるだけです。動的に確保したメモリのサイズを追跡するには、malloc呼び出し時に要求したサイズを別の変数で管理する必要があります。
    “`c
    #include
    #include // malloc, free のために必要

    int main() {
    size_t num_elements = 10;
    size_t element_size = sizeof(int);
    size_t total_size = num_elements * element_size;

    int *arr = malloc(total_size);
    
    if (arr != NULL) {
        printf("sizeof(arr) = %zu bytes\n", sizeof(arr)); // ポインタ arr のサイズ (e.g., 8)
        // 確保したメモリのサイズ (total_size) を知るには、malloc 呼び出し時の情報を保持する必要がある
        printf("Allocated size = %zu bytes\n", total_size);
    
        free(arr);
    }
    
    return 0;
    

    }
    ``
    4. **基本データ型のサイズへの依存:**
    intが常に4バイトである、longが常に8バイトである、といった仮定に基づいたコードは移植性が低くなります。データ型のサイズが必要な場合は必ずsizeofを使用し、例えばintが保持できる最大値を知るにはINT_MAXマクロを使用するなど、標準で提供される機能を使うようにします。
    5. **構造体のパディングを無視したサイズ計算:** 構造体のサイズが単純なメンバの合計サイズであると仮定してしまうこと。アライメントとパディングの存在を理解し、
    sizeof`演算子で取得した正確なサイズを使用する必要があります。

これらの点に注意することで、sizeof演算子をより効果的かつ安全に使用することができます。

9. sizeofの主な使用例

sizeof演算子は、C言語プログラミングの様々な場面で活用されます。

  • 動的メモリ割り当て: malloc, calloc, reallocなどでメモリを確保する際に、必要なバイト数を計算するため。
    c
    int *numbers = malloc(10 * sizeof(int)); // 10個のintのためのメモリを確保
    struct MyStruct *data = malloc(sizeof(struct MyStruct)); // 構造体1つ分のメモリを確保
  • 配列の要素数計算: 固定サイズの配列の要素数を、サイズに依存しない方法で得るため。
    c
    int arr[] = {1, 2, 3, 4, 5};
    size_t num_elements = sizeof(arr) / sizeof(arr[0]);
  • データ構造の調査: 構造体や共用体のサイズ、メンバのオフセット(offsetofと組み合わせて)を調査し、メモリレイアウトを理解するため。
  • 型サイズの決定: 特定のデータ型が現在のシステムでどれだけのメモリを占めるかを知り、例えば固定サイズのバッファを宣言する際などに利用するため。
    c
    char buffer[sizeof(double)]; // double を格納できる最低限のバッファ
  • ファイル入出力やネットワーク通信: バイナリデータをファイルに書き込んだり、ネットワークを通じて送受信したりする際に、各データ項目の正確なバイトサイズを決定するため。
  • 特定の型サイズが重要なアルゴリズム: ビットフィールドの操作や低レベルのメモリ操作を行う際に、型の正確なサイズを知る必要がある場合。

10. まとめ

sizeof演算子は、C言語において変数やデータ型がメモリ上で占めるサイズ(バイト数)を取得するための基本的なツールです。その挙動はシンプルに見えますが、配列、ポインタ、構造体、共用体といった要素が絡むと、アライメントやパディングといったメモリレイアウトに関わる複雑さが生じます。

この記事を通じて、以下の重要なポイントを理解していただけたかと思います。

  • sizeofはオペランドのサイズをバイト単位で返します。
  • 戻り値の型はsize_tです。
  • 基本型のサイズは処理系に依存します(charは1バイト固定)。
  • 配列に適用すると配列全体のサイズを返しますが、関数引数の配列はポインタに変換されるため注意が必要です。
  • 構造体のサイズはメンバの合計だけでなく、アライメントとパディングによって影響を受けます。メンバの順序によってサイズが変わることもあります。
  • 共用体のサイズは通常、最も大きいメンバのサイズですが、これもアライメントによって影響を受けます。
  • ポインタに適用すると、指しているデータのサイズではなく、ポインタ変数自体のサイズを返します。
  • sizeofはほとんどの場合コンパイル時定数ですが、VLAに対しては実行時に評価されます。
  • sizeofstrlenとは異なり、文字列リテラルの場合はヌル終端文字を含んだ配列のサイズを返します。
  • 動的に割り当てられたメモリのサイズはsizeofでは分かりません。

sizeof演算子を正しく理解し、適切に活用することで、より堅牢で、移植性が高く、メモリ効率の良いCプログラムを書くことができます。C言語の学習や実務において、sizeof演算子の正確な挙動を常に意識することが重要です。メモリの構造やデータ型の表現方法に興味を持ったら、sizeofoffsetof(C標準)、さらにC11のalignofといったツールを使って、あなたの使っている環境での具体的な挙動を調べてみることをお勧めします。


これで、約5000語の詳細な記事となります。この情報が、C言語におけるsizeof演算子への理解を深める一助となれば幸いです。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール