C言語を用いた開発において、メモリ管理はプログラムのパフォーマンスと安定性を左右する極めて重要な要素です。

その中で、指定したデータ型や変数が占有するメモリサイズを取得するために欠かせないのがsizeof演算子です。

本記事では、初心者から中級者までを対象に、sizeof演算子の基本的な使い方から、配列や構造体での応用、そして多くの開発者が陥りやすい注意点について詳しく解説します。

sizeof演算子の基本概念と役割

C言語におけるsizeofは、一見すると関数のようにも見えますが、実際にはコンパイル時に処理される単項演算子です。

この演算子の主な役割は、対象となる型や式がメモリ上で何バイトを占有しているかを計算し、その結果をsize_t型(符号なし整数型)の定数として返すことです。

メモリサイズをハードコーディング(数値を直接記述)することは、プログラムの移植性を著しく低下させます。

なぜなら、C言語の各データ型のサイズは、使用するコンパイラやCPUアーキテクチャ(32bitか64bitかなど)によって異なる場合があるからです。

例えば、int型は多くの環境で4バイトですが、古い組み込みシステムなどでは2バイトであることも珍しくありません。

sizeof演算子を使用することで、環境に依存しない柔軟なコードを記述することが可能になります。

sizeofの基本的な構文

sizeof演算子の使い方は非常にシンプルで、主に以下の2通りの記述方法があります。

  1. 型名を指定する場合:sizeof(型名)
  2. 変数や式を指定する場合:sizeof(変数名) または sizeof 変数名

型名を指定する場合はカッコが必須となりますが、変数名を指定する場合はカッコを省略することも可能です。

ただし、可読性を維持するために、一般的には変数名に対してもカッコを付けて記述するスタイルが多く採用されています。

基本データ型のサイズ確認

まずは、C言語で標準的に用意されている基本データ型が、実際にどの程度のサイズを持つのかをプログラムで確認してみましょう。

以下のコードは、主要な型のバイトサイズを出力する例です。

C言語
#include <stdio.h>

int main(void) {
    // 各基本データ型のサイズを表示
    // size_t型の出力には %zu フォーマット指定子を使用するのが標準的です
    printf("char      : %zu byte\n", sizeof(char));
    printf("short     : %zu bytes\n", sizeof(short));
    printf("int       : %zu bytes\n", sizeof(int));
    printf("long      : %zu bytes\n", sizeof(long));
    printf("long long : %zu bytes\n", sizeof(long long));
    printf("float     : %zu bytes\n", sizeof(float));
    printf("double    : %zu bytes\n", sizeof(double));
    printf("long double: %zu bytes\n", sizeof(long double));

    return 0;
}

実行結果(一般的な64bit環境の例):

実行結果
char      : 1 byte
short     : 2 bytes
int       : 4 bytes
long      : 8 bytes
long long : 8 bytes
float     : 4 bytes
double    : 8 bytes
long double: 16 bytes

このように、char型は規格によって常に1バイトであることが保証されていますが、それ以外の型については環境によって変動する可能性があります。

特にlong型は、Windows(LLP64モデル)では4バイト、Linux(LP64モデル)では8バイトとなるなど、環境差が出やすい部分であるため注意が必要です。

配列におけるsizeofの活用と要素数の算出

sizeof演算子が最も頻繁に活用される場面の一つが、配列の処理です。

配列に対してsizeofを適用すると、その配列が確保している合計のメモリサイズを取得することができます。

配列の全体サイズと要素数の求め方

配列全体のサイズを取得できるという特性を利用して、配列の要素数を動的に計算することができます。

以下の式は、C言語のイディオムとして非常によく使われます。

要素数 = sizeof(配列) / sizeof(配列[0])

この計算式を用いることで、配列の宣言サイズを変更しても、ループ処理などの箇所を修正することなく自動的に正しい要素数が得られるようになります。

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

C言語
#include <stdio.h>

int main(void) {
    // 整数型の配列を宣言
    int numbers[] = {10, 20, 30, 40, 50, 60, 70};

    // 配列全体のサイズを取得
    size_t total_size = sizeof(numbers);
    
    // 配列の1要素あたりのサイズを取得
    size_t element_size = sizeof(numbers[0]);

    // 要素数を計算
    size_t length = total_size / element_size;

    printf("配列全体のサイズ: %zu バイト\n", total_size);
    printf("1要素のサイズ   : %zu バイト\n", element_size);
    printf("配列の要素数     : %zu\n", length);

    // ループでの活用例
    printf("配列の内容: ");
    for (size_t i = 0; i < length; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    return 0;
}
実行結果
配列全体のサイズ: 28 バイト
1要素のサイズ   : 4 バイト
配列の要素数     : 7
配列の内容: 10 20 30 40 50 60 70

【重要】関数引数としての配列とsizeofの罠

配列を扱う際、初心者が最も陥りやすいミスがあります。

それは、関数に渡された配列に対してsizeofを適用し、その要素数を求めようとすることです。

C言語では、配列が関数の引数として渡される際、配列そのものではなく「配列の先頭要素を指すポインタ」へと自動的に変換されます。

これを「ポインタへの退化(decay)」と呼びます。

そのため、関数内でのsizeofは配列全体のサイズではなく、ポインタ変数のサイズを返してしまいます。

C言語
#include <stdio.h>

// 配列を引数に取るように見えるが、実際には int *arr と同じ
void print_size(int arr[]) {
    printf("関数内での sizeof(arr): %zu\n", sizeof(arr));
}

int main(void) {
    int my_array[10];
    printf("メイン関数での sizeof(my_array): %zu\n", sizeof(my_array));
    
    print_size(my_array);
    
    return 0;
}

実行結果(64bit環境の例):

実行結果
メイン関数での sizeof(my_array): 40
関数内での sizeof(arr): 8

このように、関数内では配列のサイズ情報が失われているため、要素数を計算することができません。

関数で配列を扱う場合は、必ず呼び出し側で要素数を計算し、引数として別途渡す必要があります。

構造体におけるsizeofとパディングの仕組み

構造体に対してsizeof演算子を使用する場合、その結果は単純な各メンバのサイズの合計値になるとは限りません。

ここには、コンピュータが効率よくメモリにアクセスするためのアライメント(整列)と、それに伴うパディング(詰め物)という概念が関わっています。

パディングが発生する具体例

以下のコードで、構造体のサイズを確認してみましょう。

C言語
#include <stdio.h>

// メンバの合計サイズは 1 + 4 = 5 バイトに見える
struct Sample {
    char a;
    int b;
};

int main(void) {
    struct Sample s;
    printf("struct Sample のサイズ: %zu バイト\n", sizeof(s));
    printf("char a のサイズ: %zu\n", sizeof(s.a));
    printf("int b のサイズ : %zu\n", sizeof(s.b));
    
    return 0;
}

実行結果(多くの現代的な環境):

実行結果
struct Sample のサイズ: 8 バイト
char a のサイズ: 1
int b のサイズ : 4

合計は5バイトのはずですが、結果は8バイトとなっています。

これは、CPUが4バイト単位でメモリにアクセスする方が高速であるため、コンパイラがchar aの後に3バイトの空きスペース(パディング)を自動的に挿入したためです。

メンバの順序によるサイズの変化

構造体内のメンバを並べる順番を変えるだけで、パディングの入り方が変わり、構造体全体のサイズが変化することがあります。

C言語
#include <stdio.h>

struct Data1 {
    char a;   // 1バイト
    int b;    // 4バイト
    char c;   // 1バイト
}; // パディングにより 12バイトになる場合が多い

struct Data2 {
    int b;    // 4バイト
    char a;   // 1バイト
    char c;   // 1バイト
}; // パディングを抑えて 8バイトになる場合が多い

int main(void) {
    printf("Data1のサイズ: %zu\n", sizeof(struct Data1));
    printf("Data2のサイズ: %zu\n", sizeof(struct Data2));
    return 0;
}

このように、構造体を定義する際は、サイズの大きいメンバから順に並べることで、メモリ消費量を節約できる場合があります。

ネットワーク通信やファイル出力でバイナリデータを扱う際は、このパディングの存在を考慮しないとデータがズレてしまうため、sizeofの結果を過信せず、必要に応じてコンパイラ独自の#pragma packなどを使用してパディングを抑制することもあります。

動的メモリ確保(malloc)におけるsizeofの活用

C言語で動的にメモリを確保するmalloc関数を使用する際、sizeof演算子は不可欠な存在です。

確保したい型のサイズをsizeofで取得し、それに必要な要素数を掛けることで、安全にメモリを割り当てることができます。

推奨されるmallocの記述方法

一般的に、以下のような書き方が推奨されます。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 10;
    int *ptr;

    // パターン1: 型名を指定する(一般的)
    ptr = (int *)malloc(sizeof(int) * n);
    if (ptr == NULL) return 1;

    // パターン2: 変数の指す先のサイズを指定する(より堅牢)
    // ptrが指す型のサイズを自動的に取得するため、型の変更に強い
    int *ptr2 = malloc(sizeof(*ptr2) * n);
    if (ptr2 == NULL) {
        free(ptr);
        return 1;
    }

    // メモリ使用後の処理
    free(ptr);
    free(ptr2);

    return 0;
}

特にsizeof(*ptr)という書き方は、将来的にptrの型をintからdoubleなどに変更した際、mallocの引数を書き直す必要がないため、バグの混入を防ぐテクニックとして知られています。

sizeof演算子に関する高度な知識と注意点

sizeofと式の評価

sizeof演算子の中に式を記述した場合、その式はコンパイル時にサイズが計算されるだけで、実行時に式が評価(実行)されることはありません。

C言語
#include <stdio.h>

int main(void) {
    int i = 10;
    size_t s = sizeof(i++); // i++ は実行されない

    printf("sizeofの結果: %zu\n", s);
    printf("iの値: %d\n", i); // 11ではなく10のまま

    return 0;
}
実行結果
sizeofの結果: 4
iの値: 10

このように、sizeof(i++)としても変数iの値はインクリメントされません。

これはsizeofが「実行時の計算」ではなく「型の解析」を行っているためです。

可変長配列(VLA)とsizeof

C99規格以降で導入された可変長配列(Variable Length Array)の場合、sizeofの挙動は例外的に実行時の評価となります。

配列のサイズが実行時まで決まらないため、実行時にその時点でのサイズを計算して返します。

ただし、組み込み環境や特定のコーディング規約(MISRA Cなど)では、VLAの使用が禁止されていることも多いため、利用には注意が必要です。

size_t型と出力方法

sizeofが返す値の型はsize_tです。

これは符号なし整数型であり、そのサイズはプラットフォームの最大メモリサイズを保持できる大きさに定義されています。

printfで表示する際には、符号あり整数の%dではなく、size_t専用のフォーマット指定子である%zuを使用するのが正しい作法です。

まとめ

sizeof演算子は、C言語におけるメモリ操作の安全性を支える非常に強力なツールです。

本記事で解説した以下のポイントを理解しておくことで、より堅牢なプログラムを作成できるようになります。

  • sizeofは関数ではなく、コンパイル時に評価される演算子である。
  • 基本型のサイズは環境によって異なるため、常にsizeofを使用して取得するべきである。
  • 配列の要素数は sizeof(array) / sizeof(array[0]) で算出できるが、関数引数として渡された配列には無効である。
  • 構造体にはパディングが含まれるため、メンバの合計サイズと一致しないことがある。
  • mallocなどの動的メモリ確保では、型変更に強い sizeof(*ptr) の活用が推奨される。

メモリの仕組みを正しく把握し、sizeofを適切に使いこなすことは、C言語マスターへの第一歩です。

日々のコーディングの中で、常に「このデータは今何バイト消費しているのか」を意識する習慣をつけていきましょう。