整数演算で起こるオーバーフローは発見が難しく、バグや脆弱性につながります。
本記事では<limits.h>が提供する範囲マクロ、とくにINT_MAXとINT_MINの読み方と実践的な使い方を、初心者向けに丁寧に解説します。
併せてcharやlongなど各型の最大値・最小値の調べ方、オーバーフローを未然に防ぐチェック方法も具体例で示します。
<limits.h>で最大値・最小値を取得
<limits.h>の役割
<limits.h>は、C言語の整数型が取りうる最大値と最小値を表すマクロ定数を宣言するヘッダファイルです。
プラットフォームやコンパイラが異なると型のビット幅が変わる場合がありますが、<limits.h>のマクロを使えば移植性の高いコードを書けます。
これらのマクロはコンパイル時定数なので、条件分岐や配列サイズなど様々な場面で安全に使用できます。
代表的な役割のまとめ
- 各整数型の範囲を表すマクロを提供します。
- 値の境界チェックによりオーバーフローやアンダーフローの防止に役立ちます。
- プラットフォーム差を吸収し、マジックナンバーの記述を避けられます。
INT_MAX/INT_MINの意味
INT_MAXはint型の最大値、INT_MINはint型の最小値です。
たとえば多くの環境でintは32ビットのためINT_MAXは2147483647、INT_MINは-2147483648になります。
ただし環境によっては異なるため、数値を決め打ちせずマクロを参照することが重要です。
よくある誤解と注意
数値をリテラルで直書きしないことが大切です。
例えば-2147483648は、コンパイラによっては-(2147483648)と解釈され、リテラル側がintで表せないため不正になる場合があります。
INT_MINを使えば、このような落とし穴を避けられます。
使い方の基本
まずは値を確認するのが理解の近道です。
以下のサンプルでは、各型の最小値・最大値を表示します。
#include <stdio.h>
#include <limits.h> // 範囲マクロを使うためにインクルード
int main(void) {
// charのビット幅
printf("CHAR_BIT = %d\n", CHAR_BIT);
// char系
printf("SCHAR_MIN = %d, SCHAR_MAX = %d\n", SCHAR_MIN, SCHAR_MAX);
printf("UCHAR_MAX = %u\n", UCHAR_MAX);
printf("CHAR_MIN = %d, CHAR_MAX = %d\n", CHAR_MIN, CHAR_MAX); // 実装依存でsigned/unsigned
// short系
printf("SHRT_MIN = %d, SHRT_MAX = %d\n", SHRT_MIN, SHRT_MAX);
printf("USHRT_MAX = %u\n", USHRT_MAX);
// int系
printf("INT_MIN = %d, INT_MAX = %d\n", INT_MIN, INT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
// long系
printf("LONG_MIN = %ld, LONG_MAX = %ld\n", LONG_MIN, LONG_MAX);
printf("ULONG_MAX = %lu\n", ULONG_MAX);
// long long系
printf("LLONG_MIN = %lld, LLONG_MAX = %lld\n", LLONG_MIN, LLONG_MAX);
printf("ULLONG_MAX = %llu\n", ULLONG_MAX);
return 0;
}
実行結果の例(LP64環境の一例)
実行結果は環境によって異なります。
以下はLinuxやmacOSの多くが採用するLP64環境の例です。
CHAR_BIT = 8
SCHAR_MIN = -128, SCHAR_MAX = 127
UCHAR_MAX = 255
CHAR_MIN = -128, CHAR_MAX = 127
SHRT_MIN = -32768, SHRT_MAX = 32767
USHRT_MAX = 65535
INT_MIN = -2147483648, INT_MAX = 2147483647
UINT_MAX = 4294967295
LONG_MIN = -9223372036854775808, LONG_MAX = 9223372036854775807
ULONG_MAX = 18446744073709551615
LLONG_MIN = -9223372036854775808, LLONG_MAX = 9223372036854775807
ULLONG_MAX = 18446744073709551615
Windowsの多くの環境ではlongは32ビットです。
この違いについては後述します。
型ごとの範囲マクロ一覧
charの範囲
charのビット幅はCHAR_BITで確認できます。
charが符号付きかどうかは実装依存で、CHAR_MIN/CHAR_MAXは実装に合わせて設定されます。
確実に符号付きの範囲を知りたいときはSCHAR_MIN/SCHAR_MAX、符号なしはUCHAR_MAXを使います。
符号なしの最小値は常に0です。
charの符号を調べる簡単な方法
#include <stdio.h>
int main(void) {
// charがsignedかどうかを動的に判定する簡易チェック
if ((char)-1 < 0) {
printf("charはsignedです\n");
} else {
printf("charはunsignedです\n");
}
return 0;
}
shortの範囲
shortの範囲はSHRT_MIN/SHRT_MAX、符号なしはUSHRT_MAXで与えられます。
shortは最低でも16ビットであることが規定されています。
intの範囲
intは多くの環境で32ビットです。
範囲はINT_MIN/INT_MAX、符号なしはUINT_MAXです。
演算の既定型として使われやすいため、加減算のオーバーフロー対策で最もよく参照するマクロです。
longの範囲
longのビット幅は環境差が大きく、LP64(多くのUNIX系)では64ビット、LLP64(Windows)では32ビットが一般的です。
範囲はLONG_MIN/LONG_MAX、符号なしはULONG_MAXです。
プラットフォーム差を吸収するために必ずマクロを使うのが安全です。
long longの範囲
long longは少なくとも64ビットです。
範囲はLLONG_MIN/LLONG_MAX、符号なしはULLONG_MAXです。
大きな整数が必要な場合の受け皿として、intより広い範囲チェックに使えます。
unsignedの最大値
符号なし整数の最小値は常に0です。
最大値は次のマクロで参照します。
UCHAR_MAXUSHRT_MAXUINT_MAXULONG_MAXULLONG_MAX
代表的な範囲マクロの早見表(概要)
値は代表例です。
実際の値は環境依存である点に注意してください。
| 型 | 最小値マクロ | 最大値マクロ | 符号なし最大値マクロ | 代表例(LP64) |
|---|---|---|---|---|
| char | CHAR_MIN または SCHAR_MIN | CHAR_MAX または SCHAR_MAX | UCHAR_MAX | -128〜127 または 0〜255 |
| short | SHRT_MIN | SHRT_MAX | USHRT_MAX | -32768〜32767 |
| int | INT_MIN | INT_MAX | UINT_MAX | -2147483648〜2147483647 |
| long | LONG_MIN | LONG_MAX | ULONG_MAX | -9223372036854775808〜9223372036854775807 |
| long long | LLONG_MIN | LLONG_MAX | ULLONG_MAX | -9223372036854775808〜9223372036854775807 |
オーバーフロー防止に効く使い方
加算前のチェック
加算は符号付き整数で特に危険です。
オーバーフローは未定義動作を引き起こします。
以下はintの安全な加算関数の例です。
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
// 安全な加算: a + b が int の範囲に収まるかを事前に判定
bool safe_add_int(int a, int b, int *out) {
// b > 0 のとき a > INT_MAX - b ならオーバーフロー
if (b > 0 && a > INT_MAX - b) return false;
// b < 0 のとき a < INT_MIN - b ならアンダーフロー
if (b < 0 && a < INT_MIN - b) return false;
*out = a + b;
return true;
}
int main(void) {
int r;
if (safe_add_int(1000000000, 1000000000, &r)) {
printf("OK: 1000000000 + 1000000000 = %d\n", r);
} else {
printf("NG: 1000000000 + 1000000000 は範囲外\n");
}
if (safe_add_int(INT_MAX, 1, &r)) {
printf("OK: INT_MAX + 1 = %d\n", r);
} else {
printf("NG: INT_MAX + 1 は範囲外\n");
}
if (safe_add_int(-2000000000, -2000000000, &r)) {
printf("OK: -2000000000 + -2000000000 = %d\n", r);
} else {
printf("NG: -2000000000 + -2000000000 は範囲外\n");
}
return 0;
}
OK: 1000000000 + 1000000000 = 2000000000
NG: INT_MAX + 1 は範囲外
NG: -2000000000 + -2000000000 は範囲外
減算前のチェック
減算a - bはa + (-b)と同じですが、直接チェックする場合は次のように書けます。
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
// 安全な減算: a - b が int の範囲に収まるかを判定
bool safe_sub_int(int a, int b, int *out) {
// b > 0 のとき a < INT_MIN + b ならアンダーフロー
if (b > 0 && a < INT_MIN + b) return false;
// b < 0 のとき a > INT_MAX + b ならオーバーフロー
if (b < 0 && a > INT_MAX + b) return false;
*out = a - b;
return true;
}
int main(void) {
int r;
if (safe_sub_int(100, -50, &r)) {
printf("OK: 100 - (-50) = %d\n", r);
} else {
printf("NG: 100 - (-50) は範囲外\n");
}
if (safe_sub_int(INT_MIN, 1, &r)) {
printf("OK: INT_MIN - 1 = %d\n", r);
} else {
printf("NG: INT_MIN - 1 は範囲外\n");
}
if (safe_sub_int(INT_MAX, -10, &r)) {
printf("OK: INT_MAX - (-10) = %d\n", r);
} else {
printf("NG: INT_MAX - (-10) は範囲外\n");
}
return 0;
}
OK: 100 - (-50) = 150
NG: INT_MIN - 1 は範囲外
NG: INT_MAX - (-10) は範囲外
代入前の範囲チェック
より広い型から狭い型へ代入する前には必ず範囲チェックを行います。
ここではlong longからintへの変換例を示します。
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
// long long -> int への安全な変換
bool to_int_checked(long long x, int *out) {
if (x < INT_MIN || x > INT_MAX) {
return false; // 範囲外
}
*out = (int)x;
return true;
}
int main(void) {
int v;
if (to_int_checked(3000000000LL, &v)) {
printf("OK: 3000000000LL -> %d\n", v);
} else {
printf("NG: 3000000000LL は int に収まりません\n");
}
if (to_int_checked(-3000000000LL, &v)) {
printf("OK: -3000000000LL -> %d\n", v);
} else {
printf("NG: -3000000000LL は int に収まりません\n");
}
if (to_int_checked(123LL, &v)) {
printf("OK: 123LL -> %d\n", v);
} else {
printf("NG: 123LL は int に収まりません\n");
}
return 0;
}
NG: 3000000000LL は int に収まりません
NG: -3000000000LL は int に収まりません
OK: 123LL -> 123
入力の検証
ユーザー入力は範囲外の値が入りやすい箇所です。
まず広い型で受けてから、INT_MIN/INT_MAXで範囲を検証して安全に狭い型へ変換します。
#include <stdio.h>
#include <limits.h>
int main(void) {
long long tmp;
printf("intに代入したい整数を入力してください: ");
// いったん long long で読み込む
if (scanf("%lld", &tmp) != 1) {
printf("入力エラー: 整数を読み取れませんでした\n");
return 1;
}
// int の範囲チェック
if (tmp < INT_MIN || tmp > INT_MAX) {
printf("範囲外: %lld は int(%d..%d)に収まりません\n", tmp, INT_MIN, INT_MAX);
return 0;
}
int value = (int)tmp; // 安全と分かった上でキャスト
printf("OK: %d を受け付けました\n", value);
return 0;
}
intに代入したい整数を入力してください: 42
OK: 42 を受け付けました
intに代入したい整数を入力してください: 5000000000
範囲外: 5000000000 は int(-2147483648..2147483647)に収まりません
intに代入したい整数を入力してください: abc
入力エラー: 整数を読み取れませんでした
より厳密な入力検証にはfgetsとstrtollの併用も有効ですが、本記事では<limits.h>の活用に焦点を当てています。
初心者が知っておく注意点
32ビットと64ビットで値が異なる
同じコードでも環境により最大値・最小値が異なることがあります。
代表的なデータモデルは次の通りです。
ILP32(32ビット環境)ではintとlongが32ビット、LP64(UNIX系の64ビット)ではintは32ビットだがlongは64ビット、LLP64(Windowsの64ビット)ではlongが32ビットのままです。
必ずマクロで確認し、範囲を前提としたハードコーディングを避けてください。
サイズを確認する簡単なコード
#include <stdio.h>
int main(void) {
printf("sizeof(int) = %zu, sizeof(long) = %zu, sizeof(long long) = %zu\n",
sizeof(int), sizeof(long), sizeof(long long));
return 0;
}
マジックナンバーを避ける
数値の直書きは移植性と可読性を下げます。
範囲チェックに具体的な数を入れるのではなく、INT_MAXやLONG_MINなどのマクロを使いましょう。
悪い例と良い例
// 悪い例: マジックナンバー
if (x > 2147483647) { /* ... */ }
// 良い例: マクロでポータブルに
#include <limits.h>
if (x > INT_MAX) { /* ... */ }
型は合わせる
比較やprintfのフォーマット指定子は、型とマクロを一致させることが重要です。
例えばlong値をINT_MAXと比較すると、環境によっては意図しない型変換が起きてバグの原因になります。
その型に対応するマクロを使いましょう。
型と対応マクロ、printf指定子の対応
| 型 | 符号付き範囲マクロ | 符号なし最大値マクロ | printf指定子の例 |
|---|---|---|---|
| int | INT_MIN / INT_MAX | UINT_MAX | %d, %u |
| long | LONG_MIN / LONG_MAX | ULONG_MAX | %ld, %lu |
| long long | LLONG_MIN / LLONG_MAX | ULLONG_MAX | %lld, %llu |
| signed char | SCHAR_MIN / SCHAR_MAX | ー | %hhd |
| unsigned char | ー | UCHAR_MAX | %hhu |
| short | SHRT_MIN / SHRT_MAX | USHRT_MAX | %hd, %hu |
printfでは型に応じて指定子を正しく選びます。
誤った指定子は未定義動作につながります。
まとめ
<limits.h>のマクロを使って型ごとの最大値・最小値を正しく参照することは、オーバーフローやアンダーフローを未然に防ぐ最短経路です。
特にINT_MAX/INT_MINは加減算や代入前のチェックで頻出します。
プラットフォームによるビット幅の違いを前提に、マジックナンバーを避け、型とマクロを必ず対応させる習慣を身につけてください。
入力の検証でもまず広い型で受けてから範囲チェックするパターンを用いれば、初学者でも堅牢な整数処理が実現できます。
最後に、未定義動作は発生させないという意識を常に持ち、各所で<limits.h>の力を活用しましょう。
