Cプログラミング
 Computer >> コンピューター >  >> プログラミング >> Cプログラミング

C/C++で起こる未定義動作(Undefined Behavior)とは?具体例と出力結果を徹底解説

CやC++には、言語仕様上「未定義動作(Undefined Behavior)」として定められている操作が数多く存在します。未定義動作が発生した場合、プログラムはクラッシュするかもしれないし、一見正常に動作しているように見えることもあり、その挙動はコンパイラによって異なります。

この記事では、実際に未定義動作を引き起こすコード例を紹介しながら、それぞれどのような問題が起きるのかをわかりやすく解説します。

1. ゼロ除算(0による割り算)

整数型の値をゼロで割る操作は、CおよびC++において未定義動作です。多くの環境ではランタイムエラーが発生します。

サンプルコード

#include <iostream>
using namespace std;
int main() {
    int x = 10, y = 0;
    int z = x / y;   // ゼロ除算 → 未定義動作
    cout << "Done" << endl;
}

出力結果

ゼロ除算によるランタイムエラー

2. 初期化されていない変数の使用

宣言だけを行い、初期化していない変数を読み取るのも未定義動作です。変数には不定値(ガベージ値)が入っており、コンパイラや実行環境によって結果が変わります。

サンプルコード

#include <iostream>
using namespace std;
int main() {
    bool x;          // 未初期化
    if(x == true)
        cout << "true value";
    else
        cout << "false value";
}

出力結果

false value(コンパイラによって結果は異なる可能性があります)

3. NULLポインタの参照(デリファレンス)

NULLポインタが指す先の値にアクセスしようとすると、通常はセグメンテーションフォルトなどのランタイムエラーが発生します。

サンプルコード

#include <iostream>
using namespace std;
int main() {
    int *ptr = NULL;
    cout << "The pointer value is: " << *ptr;  // NULL参照 → 未定義動作
}

出力結果

NULLポインタへのアクセスによるランタイムエラー

4. 配列の範囲外アクセス

配列の有効なインデックス範囲を超えて要素にアクセスすることも未定義動作です。以下の例では、要素数10の配列に対して i = 10 のときに範囲外へアクセスしています。

サンプルコード

#include <iostream>
using namespace std;
int main() {
    int array[10];
    for(int i = 0; i<=10; i++) {   // i = 10 で範囲外アクセス
        cout << array[i] << endl;
    }
}

出力結果

配列の範囲外アクセスによるランタイムエラー。
コンパイラによっては、エラーを出さずに任意のゴミ値を返す場合もあります。

5. 符号付き整数のオーバーフロー

符号付き整数型(intなど)の最大値を超える演算も未定義動作です。多くの環境では最小値に折り返しますが、これは標準で保証された動作ではありません。

サンプルコード

#include <iostream>
using namespace std;
int main() {
    int x = INT_MAX;
    cout << "x + 1: " << x + 1;   // 最大値 + 1 → 未定義動作
}

出力結果

x + 1: -2147483648
符号付きintの最小値に折り返される(環境依存の挙動)

6. 文字列リテラルの書き換え

文字列リテラルは読み取り専用領域に配置されるため、その内容を変更しようとすると未定義動作となります。多くの場合、セグメンテーションフォルトでクラッシュします。

サンプルコード

#include <iostream>
using namespace std;
int main() {
    char *str = "Hello World";
    str[2] = 'x';   // リテラルの変更 → 未定義動作
    cout << str;
}

出力結果

定数領域の値を変更しようとしたため、ランタイムエラーが発生

まとめ

未定義動作は「コンパイル時にエラーにならない」ため見逃されがちですが、予測不能なクラッシュやセキュリティ脆弱性の原因になります。ゼロ除算、NULL参照、範囲外アクセス、整数オーバーフロー、文字列リテラルの変更などは特に頻出のパターンなので、コーディング段階で必ず回避するようにしましょう。また、コンパイラの警告オプション(-Wall -Wextraなど)やサニタイザ(AddressSanitizer、UBSan)を活用すると、未定義動作の早期発見に役立ちます。

  1. C++で円と長方形の重なりを判定するアルゴリズム

    問題の概要円を (radius, xc, yc) という形式で表します。ここで (xc, yc) は円の中心座標です。同様に、軸に平行な長方形(軸平行境界ボックス)を (x1, y1, x2, y2) という形式で表し、(x1, y1) が左下隅の座標、(x2, y2) が右上隅の座標とします。このとき、円と長方形が互いに重なっているかどうかを判定する必要があります。たとえば、次のような入力が与えられた場合を考えてみましょう。この場合、出力は true(重なりあり)となります。解決のアプローチこの問題を解く鍵は、「長方形の中で円の中心に最も近い点」を見つけることです。その点と円の中心との距離が

  2. C++で解くドミノとトロミノを使ったタイル敷き詰め問題(2×Nボード)

    問題の概要本記事では、「ドミノ」と「トロミノ」という2種類の形状を使ったタイル敷き詰め(タイリング)問題をC++で解く方法を解説します。これらのピースは、以下のように回転させて使用することができます。タイリングでは、盤面上のすべてのマスを必ずタイルで覆わなければなりません。また、2つのタイリング方法は、盤上の4方向に隣接する2つのセルにおいて、片方のタイリングだけがその両方のマスを同じタイルで占有している場合に限り「異なる」とみなされます。入力と出力の例整数Nが与えられたとき、2×Nのボードを敷き詰める方法が何通りあるかを求めます。例えば、入力が3の場合、出力は5となります。敷き詰め方は以下の