C言語でプログラムを開発していると、配列の要素数やメモリのサイズを扱う際にsize_tという型に遭遇することが多々あります。

初心者の方の中には「整数のサイズを指定するならint型で十分ではないか」と感じる方もいるかもしれませんが、C言語の標準仕様においてsize_t型は非常に重要な役割を担っています。

特に、システムのメモリ空間が32bitから64bitへと移行した現代において、型の選択を誤ると深刻なバグやセキュリティホールを招く可能性があります。

本記事では、size_t型の定義から、int型との具体的な違い、printf関数での正しい出力方法、そして実務で注意すべき落とし穴までを詳しく解説します。

size_t型とは何か

size_t型は、C言語の標準ライブラリで定義されている「オブジェクトのサイズを表現するための符号なし整数型」です。

ここでいうオブジェクトとは、変数、配列、構造体など、メモリ上に配置されるあらゆるデータを指します。

size_tの定義とヘッダファイル

size_t型は言語そのものに組み込まれたキーワードではなく、標準ヘッダファイル内でtypedefによって定義されています。

一般的には以下のヘッダファイルをインクルードすることで利用可能になります。

  • <stddef.h>
  • <stdio.h>
  • <stdlib.h>
  • <string.h>
  • <time.h>

内部的には、そのプラットフォームにおいて「メモリ上の最大サイズを保持できる符号なし整数」として定義されています。

例えば、32bit環境であれば通常は32bitの符号なし整数(unsigned intなど)、64bit環境であれば64bitの符号なし整数(unsigned long longなど)の別名となっています。

sizeof演算子との関係

C言語において、size_t型と最も密接に関係しているのがsizeof演算子です。

sizeof演算子は、対象の型や変数が占有するメモリサイズをバイト単位で返しますが、その戻り値の型こそがsize_t型です。

以下のプログラムで、sizeof演算子の戻り値を受け取る例を確認してみましょう。

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

int main(void) {
    int num = 100;
    // sizeofの結果をsize_t型の変数に代入
    size_t s = sizeof(num);

    printf("int型のサイズは %zu バイトです。\n", s);

    return 0;
}
実行結果
int型のサイズは 4 バイトです。

このように、メモリサイズに関わる値を扱う際は、標準的にsize_t型を使用することが推奨されています。

size_t型とint型の決定的な違い

多くのプログラマが「size_tではなくintを使っても動く」と考えてしまいがちですが、両者には明確な違いがあります。

これらを理解せずに混用することは、ポータビリティ(移植性)を損なう原因となります。

符号の有無

最大の相違点は、size_tが「符号なし」であり、intが「符号あり」である点です。

メモリのサイズや配列のインデックスに負の値が登場することはありません。

そのため、size_t型は負の値を表現する能力を切り捨て、その分大きな正の値を扱えるよう設計されています。

扱える値の範囲とプラットフォーム依存

int型のサイズは、多くの現代的な環境(LP64やLLP64)において32bit固定であることが多いですが、size_t型のサイズは実行環境のメモリアドレス空間に依存します。

  • 32bitシステム:size_tは通常32bit(最大約42億)。
  • 64bitシステム:size_tは通常64bit(最大約1844京)。

もし64bit環境で4GBを超える大きな配列を扱う場合、32bitのint型(符号ありの場合は最大約21億)ではインデックスが溢れてしまい、正しくアクセスすることができません。

型のサイズを比較するサンプルコード

環境によってsize_tのサイズがどのように変化するかを確認するためのコードを以下に示します。

C言語
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

int main(void) {
    printf("int型のサイズ:    %zu バイト\n", sizeof(int));
    printf("long型のサイズ:   %zu バイト\n", sizeof(long));
    printf("size_t型のサイズ: %zu バイト\n", sizeof(size_t));

    // size_tの最大値を確認(stdint.hのSIZE_MAXを使用)
    printf("size_tの最大値:   %llu\n", (unsigned long long)SIZE_MAX);

    return 0;
}

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

実行結果
int型のサイズ:    4 バイト
long型のサイズ:   8 バイト
size_t型のサイズ: 8 バイト
size_tの最大値:   18446744073709551615

このように、64bit環境ではsize_tはintよりも大きなサイズを持ち、広大なメモリ空間を管理できるようになっています。

printf関数での出力方法

size_t型の値をprintf関数で出力する場合、正しい書式指定子(フォーマット指定子)を使用する必要があります。

推奨される書式指定子 %zu

C99規格以降、size_t型専用の書式指定子として%zuが導入されました。

以前はプラットフォームに合わせて %u%lu を使い分ける必要がありましたが、現在は %zu を使うのが最も安全で標準的な方法です。

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

int main(void) {
    char str[] = "Hello, C language!";
    size_t length = strlen(str);

    // %zu を使用して size_t を出力
    printf("文字列の長さ: %zu\n", length);

    return 0;
}
実行結果
文字列の長さ: 18

%d や %u を使った場合の危険性

もしsize_t型の値を %d (符号ありint)で出力しようとすると、特に大きな値を扱う際に不適切な結果が表示される可能性があります。

また、64bit環境で32bit用の指定子を使用すると、スタックの整合性が崩れたり、警告が発生したりする原因となります。

コンパイラの警告オプション(-Wall など)を有効にしている場合、size_tに対して %d を使用すると「型が一致しません」という警告が表示されるはずです。

これはバグの温床であるため、必ず %zu を使用するようにしましょう。

size_t型を使用する際の実践的な注意点

size_t型は便利な一方で、その「符号なし」という特性ゆえに特有の注意点が存在します。

1. 逆順ループでの無限ループ

配列を後ろからループで回す際、size_t型を使用すると論理エラーが発生しやすいです。

以下のコードを見てみましょう。

C言語
#include <stdio.h>

int main(void) {
    size_t i;
    // 5から0までカウントダウンしたい意図
    // しかし、iが0のときにi--を行うと、符号なし型のため最大値にループしてしまう
    for (i = 5; i >= 0; i--) {
        printf("%zu ", i);
        if (i == 0) break; // これがないと無限ループになる
    }
    return 0;
}

size_tは符号なし整数であるため、0よりも小さい値(負の値)を取ることができません。

そのため、i >= 0 という条件式は常に真となり、ループが終了しません。

0の次に i-- が実行されると、値は SIZE_MAX(非常に大きな正の値)へとラップアラウンド(回り込み)してしまいます。

このような場合は、以下のようにループの構造を工夫するか、符号付きの型(ssize_tやptrdiff_tなど)を検討する必要があります。

C言語
// 安全な逆順ループの例
for (size_t i = 5; i > 0; i--) {
    printf("%zu ", i - 1);
}

2. 符号付き整数との比較

size_t型とint型(符号あり)を比較する場合、C言語の「整数昇格」および「通常の算術変換」というルールにより、予期せぬ挙動が発生することがあります。

C言語
#include <stdio.h>

int main(void) {
    int a = -1;
    size_t b = 1;

    if (a < b) {
        printf("-1 は 1 より小さいです。\n");
    } else {
        printf("-1 は 1 より大きいです(!?)。\n");
    }

    return 0;
}
実行結果
-1 は 1 より大きいです(!?)。

なぜこのような結果になるのでしょうか。

比較演算において、符号ありの int と符号なしの size_t が混在する場合、int 型が size_t 型に変換されます。

-1 を符号なし整数として解釈すると、ビット表現上は型が保持できる最大値となるため、1 よりも大きいと判定されてしまうのです。

この問題を防ぐためには、比較する前に型を統一するか、そもそも負の値が入り得ない場面では最初からすべて size_t を使用する設計にすることが重要です。

メモリ確保とsize_t

動的なメモリ確保を行う malloc 関数や realloc 関数でも、引数には size_t 型が使われています。

C言語
void *malloc(size_t size);

この設計により、そのコンピュータのメモリが許す限りの広大な領域を一度に確保できるようになっています。

もし引数が int であったなら、2GB(31ビット分)を超えるメモリを一度に確保することができません。

安全なメモリ確保の例

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

int main(void) {
    size_t num_elements = 10;
    // 要素数 * 型のサイズ を size_t で計算
    int *array = (int *)malloc(num_elements * sizeof(int));

    if (array == NULL) {
        return 1; // 確保失敗
    }

    for (size_t i = 0; i < num_elements; i++) {
        array[i] = (int)(i * 10);
        printf("%d ", array[i]);
    }

    free(array);
    return 0;
}
実行結果
0 10 20 30 40 50 60 70 80 90

このように、配列の要素数管理からメモリの割り当て、ループカウンタに至るまで、サイズに関する一連の処理をsize_tで統一するのが現代的なC言語プログラミングのベストプラクティスです。

size_t型に関連するその他の型

size_t型を学ぶ上で、関連するいくつかの型についても知っておくと理解が深まります。

ssize_t型

POSIX標準(UNIX系OSなど)では、size_tに対応する符号付き整数型としてssize_tが定義されています。

これは主に「読み込んだバイト数」を返す関数(read 関数など)で使用され、成功時にはバイト数(正の値)、失敗時には -1(負の値)を返すために使われます。

ただし、標準Cライブラリ(ANSI C)の範囲外であるため、Windows環境などでは利用できない場合があります。

ptrdiff_t型

2つのポインタの差を保持するための型です。

ポインタの引き算の結果は負になる可能性があるため、こちらは符号付き整数として定義されています。

<stddef.h> で定義されています。

uintptr_t型

ポインタを整数として保持するための型です。

ポインタと同じサイズであることが保証されている符号なし整数型で、ポインタ演算をビット単位で行いたい場合などに利用されます。

まとめ

C言語におけるsize_t型は、単なる整数の代わりではなく、メモリ空間を安全かつ抽象的に扱うための重要な型です。

  • 符号なし整数であり、メモリのサイズや配列の要素数を表現する。
  • sizeof演算子の戻り値として使用される。
  • 移植性が高く、32bit環境でも64bit環境でも最適なサイズが選択される。
  • 出力には専用の書式指定子 %zu を使用する。
  • 負の値との比較や逆順ループの際にはラップアラウンドに注意が必要。

適切な場面で int ではなく size_t を選択できるようになることは、C言語プログラマとしてのステップアップに欠かせません。

メモリを扱うコードを書く際は、常に「この値はサイズを指しているか?」を意識し、size_t型を積極的に活用していきましょう。