int型の限界 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、int型の整数について深掘りします。これまで、int型で扱うことができる整数の限界を気にしませんでした。しかし、コンピュータが計算している以上、扱える数にはどこかで限界が来ます。そのため、ここまでに作った電卓プログラムでも巨大な整数を入力したり、計算結果が巨大になったりすると、異常な結果になってしまいます。int型の限界がどこにあるのか、限界を超えたらどうなってしまうのかを確認していきます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



巨大な整数の入力

これまでの電卓プログラムにはまだ問題があります。それは、入力された整数そのものや、計算した結果が非常に大きな数(非常に大きな負数も含む)だったときに正しい結果を得られないことです。

現状の電卓プログラム(以下に掲載)を使って試してみます。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    constexpr auto addition = '+';
    constexpr auto subtraction = '-';
    constexpr auto multiplication = '*';
    constexpr auto division = '/';

    while (true) {

        // 計算式、または "exit" の入力を得る
        std::cout << "Please enter the formula.\n";
        std::string input_string {};
        std::getline(std::cin, input_string);

        // "exit" が入力されたら終わり
        if (input_string == "exit") {
            break;
        }

        // 計算式を分解する
        std::istringstream iss {input_string};
        int value1 {};
        std::string op {};
        int value2 {};
        iss >> value1 >> op >> value2;

        // 計算結果を出力
        switch (op[0]) {
        case addition:
            std::cout << value1 + value2 << "\n";
            break;
        case subtraction:
            std::cout << value1 - value2 << "\n";
            break;
        case multiplication:
            std::cout << value1 * value2 << "\n";
            break;
        case division:
            if (value2 == 0) {
                std::cout << "zero divide.\n";
            }
            else {
                std::cout << value1 / value2 << "\n";
            }
            break;
        default:
            std::cout << "The formula is incorrect.\n";
            break;
        }
    }
}

実行結果:

Please enter the formula.
100000000000 + 5  <-- 入力した文字列
The formula is incorrect.
exit  <-- 入力した文字列

試しに、「1000憶+5」という計算式を入力してみたところ、出力は “The formula is incorrect.” になりました。この文字列は、入力された演算子が「+」「-」「*」「/」のいずれでもないときに出力されるものです。演算子は「+」と正しく入力しているので、どこかで問題が起きているようです。

問題を探すため、std::istringstream による分解作業を終えた段階で、各変数がどんな値になっているのか調べてみます。変数の値を std::cout を使って出力してみましょう。

        // 計算式を分解する
        std::istringstream iss {input_string};
        int value1 {};
        std::string op {};
        int value2 {};
        iss >> value1 >> op >> value2;

        // 中身を出力してみる
        std::cout << value1 << ", " << op << ", " << value2 << "\n";

すると、次のような出力を得られます。

Please enter the formula.
100000000000 + 5  <-- 入力した文字列
2147483647, , 0
The formula is incorrect.
exit  <-- 入力した文字列

変数 value1 には 100000000000 が入っておらず、2147483647 という謎の整数が入っています。そして、変数 op は空になっており、変数 value2 は 5 でなく 0 になっています。

このような結果になるのは、std::istringstream が文字列を整数に分解するときに、正しく処理を行える限界値が存在するからです。限界値を超えた値が指定されると、その限界値によって置き換えられます1。その限界値というのが 2147483647 だったわけです。

限界値は負数の側にもあります。「-1000憶 + 5」を計算させてみます。

Please enter the formula.
-100000000000 + 5  <-- 入力した文字列
-2147483648, , 0
The formula is incorrect.
exit  <-- 入力した文字列

こちらは -2147483648 になりました。

正負のいずれにしても、指定された整数が限界値を超えている場合は、処理の過程でエラーが起きたという扱いになり、std::istringstream が行うその後の処理はすべて失敗します1変数 op や value2 に正しい値が入らないのはそのためです。

なお、std::istringstream型の変数と std::cin はどちらも入力の処理であり、基本的に同じ記述で使えるので(「stringstream」のページを参照)、この先の内容は原則として、std::cin にも当てはまります。std::istringstream型の変数を std::cin に置き換えてもうまくいくと考えてください。

入力エラーの対処

入力された整数が限界値を超えていた場合には、エラーになっているということでしたが、そうなると、エラーが起きているかどうかを知る手段が必要ということになります。さきほどのサンプルプログラムは、エラーの有無を確認していないので、不可解な出力結果になってしまいました。

入力処理によるエラーの有無は、std::istringstream型の変数に問い合わせればわかります。次のように書きます。

// std::istringstream iss; があるとして

if (iss.fail()) {
    // エラーが発生している
}

または、

if (!iss) {
    // エラーが発生している
}

iss.fail() は、変数 iss で行っている入力処理の最中に、何らかのエラーが発生しているときに true となり、エラーが発生していなければ false になります。!iss の方もまったく同じ結果になります。「何らかのエラー」なので、必ずしも、入力した整数が大きすぎる(小さすぎる)ことが理由だとは限らず、整数とみなせないような入力の場合などにもエラーになりますone + two と入力したとか)。

! は初登場ですが、これは論理値を反転させる演算子です。

エラーが「起きていない」ことを調べたい場合は、else を使うのでもいいですが、次のように書くこともできます。

// std::istringstream iss; があるとして

if (!iss.fail()) {
    // エラーが発生していない
}

または、

if (iss) {
    // エラーが発生していない
}

std::istringstream型の変数を std::cin に置き換えれば、同じようにエラーの判定ができます(if (std::cin) とか if (std::cin.fail()) といったように)

では、電卓プログラムにエラーチェックを入れてみます。

#include <iostream>
#include <sstream>
#include <string>

int main()
{
    constexpr auto addition = '+';
    constexpr auto subtraction = '-';
    constexpr auto multiplication = '*';
    constexpr auto division = '/';

    while (true) {

        // 計算式、または "exit" の入力を得る
        std::cout << "Please enter the formula.\n";
        std::string input_string {};
        std::getline(std::cin, input_string);

        // "exit" が入力されたら終わり
        if (input_string == "exit") {
            break;
        }

        // 計算式を分解する
        std::istringstream iss {input_string};
        int value1 {};
        std::string op {};
        int value2 {};
        iss >> value1 >> op >> value2;

        if (iss) {
            // 計算結果を出力
            switch (op[0]) {
            case addition:
                std::cout << value1 + value2 << "\n";
                break;
            case subtraction:
                std::cout << value1 - value2 << "\n";
                break;
            case multiplication:
                std::cout << value1 * value2 << "\n";
                break;
            case division:
                if (value2 == 0) {
                    std::cout << "zero divide.\n";
                }
                else {
                    std::cout << value1 / value2 << "\n";
                }
                break;
            default:
                std::cout << "The formula is incorrect.\n";
                break;
            }
        }
        else {
            // 入力がエラーになっている
            std::cout << "error.\n";
        }
    }
}

実行結果:

Please enter the formula.
100000000000 + 5  <-- 入力した文字列
error.
Please enter the formula.
1000 + 5  <-- 入力した文字列
1005
Please enter the formula.
exit  <-- 入力した文字列

限界値を超えた入力がエラーとみなされるようになりました。

整数の表現方法

int型の限界値が、-2147483648 だとか 2147483647 といった謎めいた数であることには理由があります。これは、コンピュータが「整数」というデータをどのように表現しているのか、といったことに関わっています。

ごく簡単に説明すると、コンピュータは整数を(というよりどんなデータも)2進数 (binary number) で表現します。2進数とは 0 か 1 のいずれかだけを使って表現される数です。そして、2進数で1桁分の情報量をビット (bit) と呼びます。

1ビットあれば、0 か 1 の2通りの情報が表現できます。2ビットあれば、00、01、10、11 の4通りが表現でき、3ビットあれば、000、001、010、011、100、101、110、111 の8通りが表現できます。このように、nビットで表現できる情報は 2n 通りです。

C++ の int型は、現代の処理系の多くで 32ビットです。したがって、232 通りの情報が表現できます。232 は 4294967296 です。実際には正の数と負の数を表現しなければならないので、大体半分ずつを割り当てて、4294967296 ÷ 2 = 2147483648 です。この数はさきほどの謎めいた限界値 -2147483648 や 2147483647 とほぼ一致しています。

【上級】負数の側の数が 1 だけ大きいですが、これは負数の表現方法として、2の補数 (Two’s complement) を用いている場合です。負数の表現方法には種類があります(詳細は、「コンピュータサイエンス編基数」のページを参照)。負数をどのように表現するかは処理系の問題で、C++ は2の補数であることを強制してはいませんが、主要な処理系はすべて2の補数を使っています。

【上級】【C++20】C++20 で、2の補数であることが規定されました2

int型で扱える整数

ここからが問題なのですが、int型の大きさ(ビット数)は処理系によって異なり、保証されているのは 16ビット以上であるということだけです。

16ビットしかない処理系では、-32768 ~ 32767 の範囲の整数しか表現できません(216 = 65536)。これが int型で表現できる範囲の最低保証範囲ということになります。

int型が 16ビットしかない処理系でも動作するプログラムを書かなければならないのなら、-32768 ~ 32767 を逸脱しないように注意する必要があるということです。C++ にはより大きな整数を確実に扱える整数型が用意されていますから、まずはそちらを検討することになるでしょう。逆に、int型が 32ビット以上の処理系のことしか考えないのなら、-2147483648 ~ 2147483647 の範囲が使えるつもりでいればいいです。

【上級】処理系によって大きさが異なると、移植性のあるプログラムを書くことが難しくなります。そこで標準ライブラリには、std::int32_t のような、大きさが固定された整数型が定義されています(厳密にいえば、これらの型が存在するかどうかは処理系定義です)3

では、実際に int型で使える範囲がどれだけなのか知るにはどうすればいいでしょうか。処理系(コンパイラ)のドキュメントを調べてみるのも手ですが、プログラムの中で限界値を使いたいときもあるので、ここではプログラムを書いて確認する方法を取り上げておきます。

std::numeric_limits<int>::min() および std::numeric_limits<int>::max() という記述によって、それぞれ int型の最小値と、int型の最大値を調べられます。コンパイルを成功させるために、#include <limits> が必要です。

#include <iostream>
#include <limits>

int main()
{
    std::cout << "min: " << std::numeric_limits<int>::min() << "\n"
              << "max: " << std::numeric_limits<int>::max() << "\n";
}

実行結果:

min: -2147483648
max: 2147483647

Visual Studio 2015 では上記の結果を得られました。int型が 32ビットでない処理系では、異なる結果になるはずです。

【C言語プログラマー】INT_MININT_MAX で得られる値と同じですが、C++ ではマクロの利用を極力避けていくようにしましょう。C++ はプリプロセッサに頼らないようにする方向に進んでいます。

【C++98/03 経験者】std::numeric_limits の max() や min() が関数であるため、定数として使いたいときに困る経験をしたことがあるかもしれません。C++11 からは、これらの関数はコンパイル時に結果(戻り値)が確定できるようになりました(constexpr関数と呼ばれる新機能)。たとえば、constexpr auto max_value = std::numeric_limits<int>::max(); と書いて問題ありません。

桁区切り文字

さきほどから大きな整数が何度も登場していますが、大きな数にはよく、3桁ごとにコンマ(,)を入れて読みやすくすると思います(2,147,483,647 のように)。

C++ のソースコードでも、このような区切り文字を使うことができます。この機能は、桁区切り文字 (digit separators) と呼びます。

#include <iostream>

int main()
{
    std::cout << 2'147'483'647 << "\n";
}

実行結果:

2147483647

桁区切り文字は、整数リテラルの中に「’」を入れるというもので、入れる位置は先頭以外であれば自由です。ただし、使える記号は「,」ではなく「’ (シングルクォーテーション)」です。桁区切り文字は、純粋に読みやすさを改善するためだけの機能であり、プログラムの動作には影響を与えません。

計算によって範囲外の整数になるとき

今度は、計算結果が、表現の限界を超えてしまうケースを確認します。int型の正の限界値に +1 すれば、計算結果が限界を超えるはずです。

#include <iostream>
#include <limits>

int main()
{
    int value {std::numeric_limits<int>::max()};
    value += 1;  // int型の上限値を超える(問題あり)
    std::cout << value << "\n";
}

Visual Studio 2015 で試すと、実行結果は -2147483648 になりますが(正の数ではありません)、これは保証された結果ではありません。

int型の値による計算中に、int型で表現できる範囲から外れてしまった場合、未定義の動作になります。72'147'483'647 + 1 - 1 のように、最終的には表現可能な数に戻ってくるとしても同様です。なお、計算した結果が、表現できる範囲を超えてしまう現象を、オーバーフロー (overflow) と呼びます。

【上級】この説明は現時点で解説済みの内容にかぎっているので、少し省かれています。正確には、「式の評価中、その結果が数学的に定義されていないか、その型で表現できる範囲内にない場合は、未定義の動作」です。“数学的に定義されていない” の部分を無視したわけですが、これは符号無し整数型の場合に意味があります。符号無し整数型は、計算結果が数学的に定義されることが保証されており、「その型の上限値 + 1」で割った余りになります8。たとえば、上限値が 255 だとして、250 + 10 を計算すると、「260 % (255 + 1)」により 4 になります。普通の int型は符号付き整数型なので、このような保証はなく、範囲外に出てしまったのなら、その時点で未定義の動作です。

これは電卓プログラムでも当然起こることで、なんとかしたい問題です。解決策としては、オーバーフローするかどうかをうまく判定して、「オーバーフローします」のような出力結果を出すことですが、意外に難しいです。オーバーフローは加算・減算・乗算のそれぞれで起こり得ますが、加算でだけ対処する方法を練習問題④ で取り上げています。

範囲外の整数リテラルを記述したとき

整数の表現に限界があるということは、整数リテラルにも限界があるということです。

int型の正の限界値が 2,147,483,647 である処理系で、3'000'000'000 という整数リテラルを含んだプログラムを書いてどうなるか確認してみます。

#include <iostream>

int main()
{
    std::cout << 3'000'000'000 << "\n";
}

実行結果:

3000000000

このコードではコンパイルが通り、予定どおりの出力結果が得られました。


次のコードではどうでしょう。

#include <iostream>

int main()
{
    int value {3'000'000'000};  // コンパイルエラー
    std::cout << value << "\n";
}

これはコンパイルエラーになります。

1つ目のコードがコンパイルできることからわかるように、int型で表現できない整数リテラルを記述すること自体は問題ありません。2つ目のコードがコンパイルエラーになった理由は別にありますが、これはやや難しい解説になるので、以下の上級者向けコラムに譲ります。ここで確認してほしいのは、当然の話として、int型の変数に int型の限界値を超えた値は入れられないということです。

【上級】このプログラムがコンパイルエラーになるのは、3'000'000'000 が int型以外の整数型に認識され、その型を int型に変換して使おうとするも、‘{}’ を使った初期化では縮小変換が許されないからです。3'000'000'000 が int型で表現できないほど大きいことは理由ではありません。
(サフィックスがなく、10進数表記の)整数リテラルの型は、int型、long int型、long long int型の順に表現可能な型を探して決定されます4。あるいはさらに拡張整数型を検討し、それでも表現できないならコンパイルエラーになりますが5、long long int型は 64ビットあることが保証されているため、3'000'000'000 は必ず表現できます。
ただし、Visual Studio 2015/2017 では、3'000'000'000 は unsigned long型になるようです(Microsoft のドキュメントには、int か long long にしかならないかのように書かれているのですが…6)。Visual Studio 2019 では long long になります。いずれにしても、int への縮小変換になるので、ここまでの説明と同じ結果になります。

【上級】1つ目のほうのコードがコンパイルできるのは、3'000'000'000 が int型よりも大きい整数型とみなされたとしても、std::cout の ‘<<’ は、そういった大きな整数型にも対応しているからです。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

8ビットの情報量で表現できる整数の個数はいくつですか?

解答・解説

問題2 (確認★)

次のプログラムはコンパイルエラーになる可能性があります。その理由を説明してください。

#include <iostream>

int main()
{
    int value {123456};
    std::cout << value << "\n";
}

解答・解説

問題3 (基本★★)

整数の入力を受け取って、なんらかの処理をおこなうプログラムにおいて、次の各種の問題はどのように検知できますか?

  1. 入力された整数が int型で表現できない
  2. 入力されたデータが整数でない
  3. 入力された整数が、予定していた条件を満たしていない(たとえば、正の整数を要求したのに、負の整数が入力されてきた)

解答・解説

問題4 (発展★★★)

整数の加算がオーバーフローしないかどうかを確認するために、次のようにプログラムを書きました。このプログラムの問題点を指摘してください。また、その問題点の解決を試みてください。

#include <iostream>
#include <limits>

int main()
{
    int n1 {};
    int n2 {};

    std::cin >> n1 >> n2;
    if (std::cin) {
        if (n1 > 0) {
            if (n1 + n2 <= std::numeric_limits<int>::max()) {
                std::cout << n1 + n2 << "\n";
            }
            else {
                std::cout << "overflow.\n";
            }
        }
        else if (n1 < 0) {
            if (n1 + n2 >= std::numeric_limits<int>::min()) {
                std::cout << n1 + n2 << "\n";
            }
            else {
                std::cout << "overflow.\n";
            }
        }
        else {
            std::cout << n1 + n2 << "\n";
        }
    }
    else {
        std::cout << "input error.\n";
    }
}

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る