閉じる

【C言語】オーバーフロー防止に効く INT_MAX/INT_MIN活用術

整数演算で起こるオーバーフローは発見が難しく、バグや脆弱性につながります。

本記事では<limits.h>が提供する範囲マクロ、とくにINT_MAXINT_MINの読み方と実践的な使い方を、初心者向けに丁寧に解説します。

併せてcharやlongなど各型の最大値・最小値の調べ方、オーバーフローを未然に防ぐチェック方法も具体例で示します。

<limits.h>で最大値・最小値を取得

<limits.h>の役割

<limits.h>は、C言語の整数型が取りうる最大値と最小値を表すマクロ定数を宣言するヘッダファイルです。

プラットフォームやコンパイラが異なると型のビット幅が変わる場合がありますが、<limits.h>のマクロを使えば移植性の高いコードを書けます。

これらのマクロはコンパイル時定数なので、条件分岐や配列サイズなど様々な場面で安全に使用できます。

代表的な役割のまとめ

  • 各整数型の範囲を表すマクロを提供します。
  • 値の境界チェックによりオーバーフローやアンダーフローの防止に役立ちます。
  • プラットフォーム差を吸収し、マジックナンバーの記述を避けられます。

INT_MAX/INT_MINの意味

INT_MAXint型の最大値、INT_MINint型の最小値です。

たとえば多くの環境でintは32ビットのためINT_MAXは2147483647、INT_MINは-2147483648になります。

ただし環境によっては異なるため、数値を決め打ちせずマクロを参照することが重要です。

よくある誤解と注意

数値をリテラルで直書きしないことが大切です。

例えば-2147483648は、コンパイラによっては-(2147483648)と解釈され、リテラル側がintで表せないため不正になる場合があります。

INT_MINを使えば、このような落とし穴を避けられます。

使い方の基本

まずは値を確認するのが理解の近道です。

以下のサンプルでは、各型の最小値・最大値を表示します。

C言語
#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の符号を調べる簡単な方法

C言語
#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_MAX
  • USHRT_MAX
  • UINT_MAX
  • ULONG_MAX
  • ULLONG_MAX

代表的な範囲マクロの早見表(概要)

値は代表例です。

実際の値は環境依存である点に注意してください。

最小値マクロ最大値マクロ符号なし最大値マクロ代表例(LP64)
charCHAR_MIN または SCHAR_MINCHAR_MAX または SCHAR_MAXUCHAR_MAX-128〜127 または 0〜255
shortSHRT_MINSHRT_MAXUSHRT_MAX-32768〜32767
intINT_MININT_MAXUINT_MAX-2147483648〜2147483647
longLONG_MINLONG_MAXULONG_MAX-9223372036854775808〜9223372036854775807
long longLLONG_MINLLONG_MAXULLONG_MAX-9223372036854775808〜9223372036854775807

オーバーフロー防止に効く使い方

加算前のチェック

加算は符号付き整数で特に危険です。

オーバーフローは未定義動作を引き起こします。

以下はintの安全な加算関数の例です。

C言語
#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 - ba + (-b)と同じですが、直接チェックする場合は次のように書けます。

C言語
#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への変換例を示します。

C言語
#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で範囲を検証して安全に狭い型へ変換します。

C言語
#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
入力エラー: 整数を読み取れませんでした
補足

より厳密な入力検証にはfgetsstrtollの併用も有効ですが、本記事では<limits.h>の活用に焦点を当てています。

初心者が知っておく注意点

32ビットと64ビットで値が異なる

同じコードでも環境により最大値・最小値が異なることがあります。

代表的なデータモデルは次の通りです。

ILP32(32ビット環境)ではintlongが32ビット、LP64(UNIX系の64ビット)ではintは32ビットだがlongは64ビット、LLP64(Windowsの64ビット)ではlongが32ビットのままです。

必ずマクロで確認し、範囲を前提としたハードコーディングを避けてください。

サイズを確認する簡単なコード

C言語
#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_MAXLONG_MINなどのマクロを使いましょう。

悪い例と良い例

C言語
// 悪い例: マジックナンバー
if (x > 2147483647) { /* ... */ }

// 良い例: マクロでポータブルに
#include <limits.h>
if (x > INT_MAX) { /* ... */ }

型は合わせる

比較やprintfのフォーマット指定子は、型とマクロを一致させることが重要です。

例えばlong値をINT_MAXと比較すると、環境によっては意図しない型変換が起きてバグの原因になります。

その型に対応するマクロを使いましょう。

型と対応マクロ、printf指定子の対応

符号付き範囲マクロ符号なし最大値マクロprintf指定子の例
intINT_MIN / INT_MAXUINT_MAX%d, %u
longLONG_MIN / LONG_MAXULONG_MAX%ld, %lu
long longLLONG_MIN / LLONG_MAXULLONG_MAX%lld, %llu
signed charSCHAR_MIN / SCHAR_MAX%hhd
unsigned charUCHAR_MAX%hhu
shortSHRT_MIN / SHRT_MAXUSHRT_MAX%hd, %hu
ポイント

printfでは型に応じて指定子を正しく選びます。

誤った指定子は未定義動作につながります。

まとめ

<limits.h>のマクロを使って型ごとの最大値・最小値を正しく参照することは、オーバーフローやアンダーフローを未然に防ぐ最短経路です。

特にINT_MAX/INT_MINは加減算や代入前のチェックで頻出します。

プラットフォームによるビット幅の違いを前提に、マジックナンバーを避け、型とマクロを必ず対応させる習慣を身につけてください。

入力の検証でもまず広い型で受けてから範囲チェックするパターンを用いれば、初学者でも堅牢な整数処理が実現できます。

最後に、未定義動作は発生させないという意識を常に持ち、各所で<limits.h>の力を活用しましょう。

C言語 標準ライブラリの活用

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!