close
名前空間
変種

未定義動作

提供: cppreference.com

C 言語標準は、以下のカテゴリのいずれかに該当する場合を除き、 C のプログラムの観察可能な動作を正確に規定します。

  • 未定義動作 - プログラムの動作について何の制約もありません。 未定義動作の例は、配列の境界外のメモリアクセス、符号付き整数のオーバーフロー、ヌルポインタの逆参照、副作用完了点のないひとつの式内での同じスカラーの2回以上の変更、異なる型のポインタを通したオブジェクトへのアクセス、などです。 コンパイラは未定義動作を診断することは要求されず (単純な状況は診断されることも多いですが)、コンパイルされたプログラムはいかなる意味のある動作を行うことも要求されません。
  • 未規定な動作 - 2パターン以上の動作が許容されます。 処理系はそれぞれの動作についてその効果を文書化することは要求されません。 例えば、評価順序、同一の文字列リテラルが区別可能かどうか、などです。 それぞれの未規定な動作は有効な結果の集合のいずれかになり、同じプログラムを繰り返し実行したときに異なる結果になっても構いません。
  • 処理系定義の動作 - 未規定な動作と同様ですが、処理系はどの選択肢を選んだかについて文書化しなければなりません。 例えば、1バイトのビット数、符号付き右シフトが算術か論理か、などです。
  • ロケール固有の動作 - 処理系定義の動作と同様ですが、現在のロケールに依存します。 例えば、 islower が26個のラテン小文字以外の文字に対して真を返すかどうか、などです。

(ノート: 厳密に準拠したプログラムは、未規定、未定義、または処理系定義の動作のいずれにも依存しません。)

コンパイラは C の構文ルールや意味論上の制約に違反するプログラムに対して診断メッセージ (エラーまたは警告) を発行することが要求されます (たとえその動作が未定義または処理系定義として規定されていたり、コンパイラがそのようなプログラムを受理する言語拡張を提供している場合でも)。 未定義動作に対する診断は断りのない限り要求されません。

未定義動作と最適化

正しい C のプログラムは未定義動作を含まないため、実際には未定義動作を含むプログラムを最適化有りでコンパイルしたとき、コンパイラは予期しない結果を生むことがあります。

例えば、

符号付きオーバーフロー

int foo(int x) {
    return x+1 > x; // true または符号付きオーバーフローによる未定義動作
}

これは以下のようにコンパイルされることがあります (デモ)。

foo:
        movl    $1, %eax
        ret

境界外のアクセス

int table[4] = {0};
int exists_in_table(int v)
{
    // 最初の4回の繰り返しのいずれかで true を返すか、または境界外アクセスによる未定義動作
    for (int i = 0; i <= 4; i++) {
        if (table[i] == v) return 1;
    }
    return 0;
}

これは以下のようにコンパイルされることがあります (デモ)。

exists_in_table:
        movl    $1, %eax
        ret

未初期化のスカラー

bool p; // 未初期化のローカル変数
if(p) // 未初期化スカラーへの未定義動作なアクセス
    puts("p is true");
if(!p) // 未初期化スカラーへの未定義動作なアクセス
    puts("p is false");

これは以下のように出力することがあります (古いバージョンの gcc で見られた動作)。

p is true
p is false
size_t f(int x)
{
    size_t a;
    if(x) // x が非ゼロであるか、または未定義動作
        a = 42;
    return a; 
}


これは以下のようにコンパイルされることがあります (デモ)。

f:
        mov     eax, 42
        ret

無効なスカラー

int f(void) {
  _Bool b = 0;
  unsigned char* p =(unsigned char*)&b;
  *p = 10;
  // この時点で b からの読み込みは未定義動作
  return b == 0;
}

これは以下のようにコンパイルされることがあります (デモ)。

f():
        movl    $11, %eax
        ret

ヌルポインタの逆参照

int foo(int* p) {
    int x = *p;
    if(!p) return x; // 上の *p が未定義動作であるか、そうでなければこの分岐に入ることはありません
    else return 0;
}
int bar() {
    int* p = NULL;
    return *p;       // 無条件の未定義動作
}

これは以下のようにコンパイルされることがあります (gcc を用いた foo と clang を用いた bar)。

foo:
        xorl    %eax, %eax
        ret
bar:
        retq

realloc に渡したポインタへのアクセス

以下に示されている出力例を観察するには clang を選択してください。

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int *p = (int*)malloc(sizeof(int));
    int *q = (int*)realloc(p, sizeof(int));
    *p = 1; // realloc に渡したポインタへの未定義動作なアクセス
    *q = 2;
    if (p == q) // realloc に渡したポインタへの未定義動作なアクセス
        printf("%d%d\n", *p, *q);
}

出力例:

12

副作用のない無限ループ

以下に示されている出力例を観察するためには clang を選択してください。

#include <stdio.h>

int fermat() {
  const int MAX = 1000;
  int a=1,b=1,c=1;
  // Endless loop with no side effects is UB
  while (1) {
    if (((a*a*a) == ((b*b*b)+(c*c*c)))) return 1;
    a++;
    if (a>MAX) { a=1; b++; }
    if (b>MAX) { b=1; c++; }
    if (c>MAX) { c=1;}
  }
  return 0;
}

int main(void) {
  if (fermat())
    puts("Fermat's Last Theorem has been disproved.");
  else
    puts("Fermat's Last Theorem has not been disproved.");
}

出力例:

Fermat's Last Theorem has been disproved.

参考文献

  • C11 standard (ISO/IEC 9899:2011):
  • 3.4 Behavior (p: 3-4)
  • 4/2 Undefined behavior (p: 8)