トラック5・📦変数・型・演算子

オーバーフローという事件

読む目安 約13分・ゴール:オーバーフローが起きる仕組みと危険性を理解し、桁の見積もりと予防策を実行できる

アプリでこのトピックを学ぶ(無料・登録不要)

このトピックの要点

動画サイトの再生回数が21億を超えたとき、カウンタの表示がおかしくなった——という事件が実際にありました。原因は int の限界です。箱に入りきらない値を入れると何が起きるのか。オーバーフローという事件を見ます。

限界の1歩先

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

int main(void) {
    int n = INT_MAX;

    printf("%d\n", n);
    printf("%d\n", n + 1);   // 危険:符号付きのオーバーフロー
    return 0;
}

多くの環境では 2147483647 の次に -2147483648 が出ます。最大値の1つ上が最小値。メーターが一周して戻ってしまう動きです。

ただしCの規格では、符号付き整数のオーバーフローは未定義動作とされています。「必ず一周する」ことすら保証されていません。最適化の結果、はみ出しを調べる条件文が丸ごと消される、といった直感に反する挙動も現実に起きます。

符号なしは折り返しが決まっている

#include <stdio.h>

int main(void) {
    unsigned char n = 250;

    printf("%u\n", n);
    n = n + 5;   // 255(この型の最大)
    printf("%u\n", n);
    n = n + 1;   // 一周して0
    printf("%u\n", n);
    return 0;
}

出力は 250 / 255 / 0。符号なしの型は「最大値の次は0」と規格で決められています(時計の針と同じ考え方)。決まっているぶん扱いやすいものの、意図しない一周は同じように事故になります。

気づきにくいのは掛け算

#include <stdio.h>

int main(void) {
    int hitori = 50000;
    int ninzuu = 50000;
    int goukei = hitori * ninzuu;             // 25億 → int に入らない
    long long anzen = (long long)hitori * ninzuu;

    printf("%d\n", goukei);
    printf("%lld\n", anzen);
    return 0;
}

5万×5万=25億は int の範囲外です。goukei はでたらめな値になり、anzen は正しく 2500000000 と出ます。ポイントは (long long) を掛ける前に付けていること。(long long)(hitori * ninzuu) では、壊れた後の値を入れ直すだけです。

現実に起きた事件

・2038年問題 … 時刻を32ビットの秒数で持つシステムが、2038年1月19日に一周します。
・Ariane 5(1996年) … 大きな小数を16ビット整数へ変換して溢れ、打ち上げ37秒後に爆発。損害は数百億円規模でした。
・ゲームの所持金が突然マイナスになる、といったバグの多くもこの仲間です。

師範器の大きさを問わずに水を注げば、こぼれる。プログラムのこぼれは床を濡らさぬゆえ、誰も気づかぬまま船が沈むのじゃ。

防ぎ方

・桁を先に見積もる。 合計・積・秒数が21億を超えそうなら long long にします。
・足す前に確かめる。 a > INT_MAX - b なら、足すと溢れると分かります。
・C23の安全な演算。 <stdckdint.h> の ckd_add などは、溢れたかどうかを戻り値で教えてくれます(トラック23で扱います)。

まとめ

int の限界を超えると値は静かに壊れます。符号付きは未定義動作、符号なしは0へ折り返し。合計や掛け算では桁を先に見積もり、必要なら long long を計算の前に使います。エラーが出ないぶん、自分で気づく仕組みを持つことが唯一の防御です。

アプリでこのトピックを学ぶ(無料・登録不要)

確認問題

確認問題 1

unsigned char は 0〜255 の値だけを持てる型です。トレース表の空欄を埋めてください。

#include <stdio.h>

int main(void) {
    unsigned char n = 250;
    n = n + 5;
    n = n + 1;
    n = n + 2;
    printf("%u\n", n);
    return 0;
}
行n
4250
5?
6?
7?
答えを見る

空欄の答え(上から順に):255、0、2

250 + 5 = 255 でちょうど上限。そこに1を足すと 256 ではなく 0 へ折り返します(時計の針と同じ考え方)。さらに2を足して 2。符号なしの型では、この折り返しが規格で決まった正式な動作です。一方、int のような符号付きの型で上限を超えた場合は未定義動作で、何が起きても文句が言えません。

確認問題 2

アイの答案です。「デキマシタ! 1人50000円を50000人ぶん合計します。25億になるので、受け皿は long long にしました」。この答案にバグはあるでしょうか。あるなら、その行はどこですか。

1#include <stdio.h>
2
3int main(void) {
4    int hitori = 50000;
5    int ninzuu = 50000;
6    long long goukei = hitori * ninzuu;
7    printf("合計 %lld\n", goukei);
8    return 0;
9}
答えを見る

判定:バグあり(6行目)

受け皿を long long にしたのは正しい着眼ですが、6行目の右辺 hitori * ninzuu は int どうしの掛け算なので、25億が int の範囲(約21億)を超えた時点で壊れます。壊れた値を long long に入れ直しても手遅れです。正しくは long long goukei = (long long)hitori * ninzuu; と、掛ける前に片方を long long にします。整数の割り算で受け皿を double にしても直らなかったのと、まったく同じ構造の誤りです。

「変数・型・演算子」の目次

  1. 変数とは名前付きの箱
  2. int型と整数の世界
  3. 浮動小数点数 float / double
  4. char型と文字コード
  5. bool型と真偽——C23標準の書き方
  6. 定数——const と constexpr
  7. 算術演算子と優先順位
  8. 整数の割り算と剰余の罠
  9. 型変換——暗黙と明示
  10. 2進数・16進数リテラル(0b / 0x / 桁区切り')
  11. sizeofとメモリ上のサイズ
  12. オーバーフローという事件

もっと先へ:ポインタ・メモリ・ファイル入出力・セキュアコーディングを含む全26トラックと、段位検定・模試のフルセットは完全版に収録しています。

完全版の販売ページは準備中です。

ほかのトラック

📚 姉妹教材:手を動かして覚える Linux 教材 — Linuxとインフラの仕組みを地図で学ぶ

Web版