C言語基礎|オーバフローと例外

あふれた瞬間、数は別人になる。オーバフローを“安全に扱う技術”を身につけよう。

オーバフローは「バグの入り口」になりやすい

整数型って、表せる範囲が決まっている“有限の箱”でしたよね。
だから計算結果が箱からはみ出した瞬間に、プログラムの挙動が一気に怪しくなります。

この「はみ出し」が オーバフロー(overflow)
そして「0で割る」みたいに計算として成り立たない状況も含めて、広い意味で 例外的な状況 として扱われます。

ただしここ、C言語の大事なポイントがあります。

  • 符号付き整数のオーバフローは、C言語では未定義動作(undefined behavior)
    → 例外が出るとは限らないし、たまたま動いて見えても、最適化で壊れることがあります。
  • 符号無し整数は、必ず“剰余(あまり)”になる(いわゆるラップアラウンド)
    → 「最大値 + 1」で割った余り、というルールが決まっています。

この差を表と図でしっかり整理していきますね。

まず整理:符号付きと符号無しで何が違うの?

オーバフロー時の基本ルール(C言語)

種類範囲を超えたとき期待できること
符号付き整数(int など)INT_MAX + 1未定義動作結果・動作は保証されない(落ちる場合も、落ちない場合もある)
符号無し整数(unsigned など)UINT_MAX + 1U剰余になる常に (数学的結果) % (最大値 + 1)

表の説明

  • 符号付きは「こうなる」と決められていません。だから危険。
  • 符号無しは「必ずこうなる」と決められています。だから予測できる。

図でイメージ:箱の端っこで何が起きる?

図1:符号付き整数(範囲の端で“保証がない”)

図2:符号無し整数(円環:ぐるっと回る)

図の説明

  • 符号無しは「0〜最大値を順番に使い回す」イメージです。
  • だから UINT_MAX + 1U は 0 に戻ります。

よく出てくる例外的な状況:0による除算

オーバフロー以外で代表的なのが 0による除算 です。

  • x / 0
  • x % 0

これは数学的に定義できないので、C言語では未定義動作になります。
多くの環境では実行時に異常終了しますが、これも「必ずそうなる」とは言い切れない点がC言語っぽいところです。

“例外”という言葉の扱い(C言語らしい注意)

質問文では「例外が発生する」と書かれていましたが、C言語の規格としては次の理解が安全です。

  • 符号付きのオーバフローや 0 除算は 未定義動作
    → 例外が出るかどうか、強制終了するかどうかは環境次第
  • ただし実行環境によっては、CPU例外やランタイム検出で落ちることがある
    → 「落ちることが多い」は事実としても、「必ず落ちる」は言えません

なので、教材としては 落ちるかどうかより、未定義動作を起こさない設計 を優先するのが一番スッキリします。

安全にするための実務的なコツ

よく使う対策

状況対策の例ねらい
int の加算が危ないINT_MAX / INT_MIN を使って事前判定未定義動作を回避
計算結果が大きくなりそうより広い型で計算(long long など)途中計算のはみ出し防止
符号無しの剰余が欲しいunsigned を使うルールが明確で予測できる
0除算があり得る割る前に if (y == 0) で分岐未定義動作を回避

演習問題

演習7-6:符号無しの剰余ルールを確認しよう

unsigned の最大値が分かるように UINT_MAX を使い、次を確認するプログラムを作成せよ。

  1. UINT_MAX + 1U が 0 になる
  2. UINT_MAX + 2U が 1 になる
  3. UINT_MAX - 1U に 3U を足すと 1 になる(ぐるっと回る)

解答例

プロジェクト名:chap7-23-1 ソースファイル名:chap7-23-1.c

#include <stdio.h>
#include <limits.h>

int main(void)
{
    unsigned a = UINT_MAX;

    printf("符号無し整数は剰余で回ります。\n");
    printf("UINT_MAX       = %u\n", a);
    printf("UINT_MAX + 1U  = %u\n", a + 1U);
    printf("UINT_MAX + 2U  = %u\n", a + 2U);

    unsigned b = UINT_MAX - 1U;
    printf("UINT_MAX - 1U  = %u\n", b);
    printf("(UINT_MAX - 1U) + 3U = %u\n", b + 3U);

    return 0;
}

実行結果例

符号無し整数は剰余で回ります。
UINT_MAX       = 4294967295
UINT_MAX + 1U  = 0
UINT_MAX + 2U  = 1
UINT_MAX - 1U  = 4294967294
(UINT_MAX - 1U) + 3U = 1

解説

  • unsigned は 0〜最大値までが循環するので、最大値の次は 0 になります。
  • つまり計算結果は常に (数学的結果) % (最大値 + 1) です。
  • この性質はビット演算や暗号、ハッシュ、リングバッファなどで便利に使われますが、意図せず起きるとバグの原因にもなるので「知って使う」が大事です。