サーチ…


前書き

Cでは、いくつかの式は未定義の動作をもたらします 。標準では、コンパイラがそのような式に遭遇した場合の動作を定義しないことを明示的に選択します。結果として、コンパイラは、見た目に合ったものを自由に実行することができ、有用な結果、予期しない結果、またはクラッシュさえ引き起こす可能性があります。

UBを呼び出すコードは、特定のコンパイラを使用して特定のシステムで意図されたとおりに動作する可能性がありますが、別のシステムや別のコンパイラ、コンパイラのバージョンまたはコンパイラの設定では機能しない可能性があります。

備考

未定義行動(UB)とは何ですか?

未定義の動作は、C標準で使用される用語です。 C11標準(ISO / IEC 9899:2011)では、定義されていない動作という用語を

移植性のないまたは誤ったプログラム構成の使用時または誤ったデータの使用時に、この国際規格は要求を課さない

私のコードにUBがあるとどうなりますか?

これらは、標準に従った未定義の振る舞いのために起こり得る結果です:

注記未定義の動作の可能性は、予測不可能な結果を​​完全に無視して状況を無視すること、環境の文書化された方法で翻訳またはプログラムの実行中に(診断メッセージの発行の有無にかかわらず)診断メッセージの発行)。

次の引用符は、定義されていない動作から生じる結果(形式的ではありませんが)を記述するためによく使用されます。

コンパイラがANSI C標準に違反することなくコードを解釈するために任意の奇妙な方法を選択する可能性があることを暗示しています)

なぜUBは存在するのですか?

それが悪いのであれば、なぜ単に定義するのか、それとも実装定義にするのでしょうか?

未定義の動作により、最適化の機会が増えます。コンパイラは、すべてのコードに未定義のビヘイビアが含まれていないと仮定することができます。実行時のチェックを避け、有効性が高価または不可能な最適化を実行できます。

なぜUBは追跡が難しいのですか?

定義されていない動作が検出しにくいバグを作成する理由は少なくとも2つあります。

  • コンパイラは、未定義の動作について警告する必要はありません。実際にそうすることを要求することは、未定義の振る舞いが存在する理由に直接的に繋がるでしょう。
  • ビヘイビアが未定義のコンストラクトが発生する操作の正確な時点で、予測できない結果が展開されない可能性があります。未定義のビヘイビアは実行全体を汚染し、その効果はいつでも発生する可能性があります。未定義コンストラクトの実行中、実行後、または実行

NULLポインタの参照を考慮してください。コンパイラは、NULLポインタの逆参照を診断する必要はなく、実行時に関数に渡されたポインタやグローバル変数内でNULLが存在する可能性もありません。 また、ヌルポインタ逆参照が発生すると、標準ではプログラムがクラッシュする必要があるとは限りません。むしろ、プログラムが早期にクラッシュしたり、後でクラッシュしたり、クラッシュしたりすることはありません。まるでヌルポインタが有効なオブジェクトを指し示し、完全に正常に動作し、他の状況下ではクラッシュするように動作する可能性があります。

nullポインタの逆参照の場合、C言語はnullポインタの参照の動作が定義されているJavaやC#などの管理言語とは異なります。正確な時刻に例外がスローされます(JavaではNullPointerException 、CではNullReferenceException )したがって、JavaまたはC#からのメッセージは、診断メッセージの発行の有無にかかわらず、Cプログラムがクラッシュしなければならないと誤って信じている可能性があります

追加情報

このような状況を明確に区別する必要があります。

  • 明示的に定義されていない動作、つまり、C標準によって明示的にオフリミットであることが示されます。
  • 暗黙のうちに定義されていない振る舞い。標準には、プログラムを持ってきた状況の振る舞いを予知するテキストはまったくありません。

また、コンパイラやライブラリの実装者が独自の定義を出す余地を残すために、C標準では意図的に特定の構文の動作が意図的に定義されていないことが多くの場所で認識されています。良い例はシグナルとシグナルハンドラです。POSIXオペレーティングシステムの標準のような、Cへの拡張がはるかに精巧なルールを定義しています。そのような場合は、プラットフォームのドキュメントをチェックするだけです。 Cの標準はあなたに何も伝えることができません。

プログラムで未定義の動作が発生した場合、未定義のビヘイビアが発生した点だけが問題になり、プログラム全体が無意味になることを意味しないことにも注意してください。

このような懸念があるため、Cでのプログラミングの人にとっては、少なくとも未定義の動作を引き起こすようなことに精通していることが重要です(特に、コンパイラはUBについて常に警告しているわけではないため)。

未定義の動作を検出するのに役立ついくつかのツール(例えば、PC-Lintなどの静的解析ツール)がありますが、未定義の動作のすべての発生を検出することはできません。

NULLポインタを参照解除する

これはNULLポインタの逆参照の例であり、未定義の動作を引き起こします。

int * pointer = NULL;
int value = *pointer; /* Dereferencing happens here */

NULLポインタは、C標準によって保証され、有効なオブジェクトへのポインタと等しくないものを比較し、逆参照は未定義の動作を呼び出します。

2つのシーケンスポイント間でオブジェクトを複数回修正する

int i = 42;
i = i++; /* Assignment changes variable, post-increment as well */
int a = i++ + i--;

このようなコードは、しばしばi 「結果的価値」についての推測につながります。しかし、結果を指定するのではなく、Cの標準では、そのような式を評価すると未定義の動作が生成されることが規定されています 。 C2011より前のこの標準は、いわゆるシーケンスポイントの観点からこれらのルールを公式化しました。

前のシーケンスポイントと次のシーケンスポイントの間では、スカラーオブジェクトは、最大でも1回の式の評価によってストアされた値を変更しなければならない。さらに、以前の値は、格納される値を決定するためにのみ読み出されるものとする。

(C99基準、第6.5項、パラグラフ2)

そのスキームはあまりにも粗すぎると判明し、いくつかの表現が、C99に関してはあまり関係がないはずの未定義の動作を示す結果となった。 C2011はシークエンスポイントを保持しますが、 シークエンシングと「 シークエンシングされた」と呼ばれる関係に基づいて、この領域へのより微妙なアプローチを導入しています。

同じスカラオブジェクトの異なる副作用または同じスカラーオブジェクトの値を使用する値計算のいずれかに対して、スカラオブジェクトの副作用が順序付けされていない場合、その動作は未定義です。式の部分式の許容可能な順序が複数ある場合、その順序付けされていない副作用がいずれかの順序で発生すると、その動作は定義されません。

(C2011基準、第6.5項、パラグラフ2)

「順序付けられた順序」関係の詳細はここでは説明するには時間がかかりすぎますが、シーケンスポイントを置き換えるのではなくシーケンスポイントを補完するので、振る舞いが以前は定義されていない評価の振る舞いを定義する効果があります。特に、2つの評価の間にシーケンスポイントがある場合、シーケンスポイントの前のシーケンスポイントは、シーケンスポイントの前のシーケンスポイントに「順序付け」されます。

次の例は、明確に定義された動作を示しています。

int i = 42;
i = (i++, i+42); /* The comma-operator creates a sequence point */

次の例では、未定義の動作があります。

int i = 42;
printf("%d %d\n", i++, i++); /* commas as separator of function arguments are not comma-operators */

定義されていない振る舞いと同様に、順序付けルールに違反する式を評価する実際の動作を観察することは、遡及的な意味を除いて有益ではありません。言語標準は、そのような観察が、同じプログラムの将来の行動であっても予測的であると期待するための基礎を提供しない。

関数を返す値にreturnステートメントがありません

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* Trying to use the (not) returned value causes UB */
  int value = foo();
  return 0;
}

関数が値を返すと宣言された場合、それを通る可能性のあるすべてのコードパスで関数を実行する必要があります。未定義の動作は、呼び出し元(戻り値が必要)が戻り値1を使用しようとするとすぐに発生します。

定義されていない動作は、呼び出し元が関数の値を使用/アクセスしようとした場合のみ発生します。例えば、

int foo(void) {
  /* do stuff */
  /* no return here */
}

int main(void) {
  /* The value (not) returned from foo() is unused. So, this program
   * doesn't cause *undefined behaviour*. */
  foo();
  return 0;
}
C99

main()関数は、想定戻り値ので、それがreturnステートメントなしで終了することが可能であるという点で、この規則の例外である0自動的にこのケース2内に使用されます。


1ISO / IEC 9899:201x 、6.9.1 / 12)

関数を終了する}に達し、関数呼び出しの値が呼び出し元によって使用されている場合、その動作は未定義です。

2ISO / IEC 9899:201x 、5.1.2.2.3 / 1)

}に到達すると、main関数を終了し、0の値を返します。

符号付き整数オーバーフロー

C99とC11の段落6.5 / 5では、結果が式の型の表現可能な値でない場合、式の評価によって未定義の動作が生成されます。算術型の場合、それはオーバーフローと呼ばれます。段落6.2.5 / 9が適用されるため、符号なし整数演算がオーバーフローしないため、範囲外の符号なし結果が範囲内の値に縮小されます。しかし、 符号付き整数型には類似の規定はありません。これらはオーバーフローする可能性があり、未定義の動作を引き起こします。例えば、

#include <limits.h>      /* to get INT_MAX */

int main(void) {
    int i = INT_MAX + 1; /* Overflow happens here */
    return 0;
}

このタイプの未定義のビヘイビアのほとんどのインスタンスは、認識または予測するのがより困難です。オーバーフローは原則として、符号付き整数(通常の算術変換の影響を受ける)に対する加算、減算、または乗算演算から発生する可能性があります。たとえば、この関数は次のようになります。

int square(int x) {
    return x * x;  /* overflows for some values of x */
}

合理的であり、引数の値が十分小さい場合は適切な処理を行いますが、引数の値が大きい場合はその動作は定義されていません。関数を呼び出すプログラムが結果として未定義の振る舞いを示すかどうかは、関数だけで判断することはできません。それは彼らがどの引数を渡すかによって異なります。

一方、オーバーフローセーフ符号付き整数算術の簡単な例を考えてみましょう:

int zero(int x) {
    return x - x;  /* Cannot overflow */
}

減算演算子のオペランド間の関係により、減算がオーバーフローしないことが保証されます。または、これをやや実用的な例として考えてみましょう。

int sizeDelta(FILE *f1, FILE *f2) {
    int count1 = 0;
    int count2 = 0;
    while (fgetc(f1) != EOF) count1++;  /* might overflow */
    while (fgetc(f2) != EOF) count2++;  /* might overflow */

    return count1 - count2; /* provided no UB to this point, will not overflow */
}

カウンタが個別にオーバーフローしない限り、最終減算のオペランドは両方とも負ではありません。そのような2つの値の間の差はすべてintとして表現できます。

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

int a; 
printf("%d", a);

変数aは、自動保存期間を持つintです。上の例のコードは、初期化されていない変数の値を出力しようとしています( aは初期化されていません)。初期化されていない自動変数には不確定な値があります。これらにアクセスすると未定義の動作につながる可能性があります。

注: staticキーワードのないグローバル変数を含む静的またはスレッドローカル記憶域を持つ変数は、ゼロまたは初期化された値に初期化されます。したがって、以下は合法です。

static int b;
printf("%d", b);

非常に一般的な間違いは、カウンターとして機能する変数を0に初期化しないことです。値を追加しますが、初期値はガーベジであるため、ターミナルのコンパイル時にポインタ警告を出すなどの未定義ビヘイビアーが呼び出されます。奇妙なシンボル

例:

#include <stdio.h>

int main(void) {
    int i, counter;
    for(i = 0; i < 10; ++i)
        counter += i;
    printf("%d\n", counter);
    return 0;
}

出力:

C02QT2UBFVH6-lm:~ gsamaras$ gcc main.c -Wall -o main
main.c:6:9: warning: variable 'counter' is uninitialized when used here [-Wuninitialized]
        counter += i;
        ^~~~~~~
main.c:4:19: note: initialize the variable 'counter' to silence this warning
    int i, counter;
                  ^
                   = 0
1 warning generated.
C02QT2UBFVH6-lm:~ gsamaras$ ./main
32812

上記の規則は、ポインタにも適用できます。たとえば、次の結果は未定義の動作になります

int main(void)
{
    int *p;
    p++; // Trying to increment an uninitialized pointer.
}

上記のコードは単独でエラーまたはセグメンテーションフォールトを引き起こさないかもしれないが、このポインターを後で逆参照しようとすると未定義の動作が起こることに注意してください。

変数へのポインタをその存続期間を超えて参照解除する

int* foo(int bar)
{
    int baz = 6;
    baz += bar;
    return &baz; /* (&baz) copied to new memory location outside of foo. */
} /* (1) The lifetime of baz and bar end here as they have automatic storage   
   * duration (local variables), thus the returned pointer is not valid! */

int main (void)
{
    int* p;

    p = foo(5);  /* (2) this expression's behavior is undefined */
    *p = *p - 6; /* (3) Undefined behaviour here */

    return 0;
}

いくつかのコンパイラがこれを有用に指摘しています。たとえば、 gccように警告します。

warning: function returns address of local variable [-Wreturn-local-addr]

そしてclangように警告します:

warning: address of stack memory associated with local variable 'baz' returned 
[-Wreturn-stack-address]

上記のコードのために。しかし、コンパイラは複雑なコードでは手助けできないかもしれません。

(1) static宣言staticれた変数への参照の復帰は、現在のスコープを離れた後に変数が破棄されないように定義された動作です。

(2)ISO / IEC 9899:2011 6.2.4§2によると、ポインタが指すオブジェクトがその寿命の終わりに達すると、ポインタの値は不定になります。

(3)関数fooによって返されたポインタを参照解除することは、参照されるメモリが不確定な値を保持するため、未定義の動作です。

ゼロ除算

int x = 0;
int y = 5 / x;  /* integer division */

または

double x = 0.0;
double y = 5.0 / x;  /* floating point division */

または

int x = 0;
int y = 5 % x;  /* modulo operation */

各例の2行目では、2番目のオペランド(x)の値が0であるため、動作は未定義です。

浮動小数点演算のほとんどの実装標準規格 (IEEE 754など)に従うことに注意してください。この場合、C標準では演算が定義されていないとしても、ゼロ除算のような演算には一貫した結果( INFINITY )があります。

割り当てられたチャンクを超えてメモリにアクセスする

n要素を含むメモリへのポインタは、範囲memorymemory + (n - 1)場合にのみ逆参照することができます。その範囲外のポインタを参照解除すると、未定義の動作になります。例として、次のコードを考えてみましょう:

int array[3];
int *beyond_array = array + 3;
*beyond_array = 0; /* Accesses memory that has not been allocated. */

3行目は、3要素の長さの配列内の4番目の要素にアクセスし、未定義の動作につながります。同様に、次のコードフラグメントの2行目の動作も明確に定義されていません。

int array[3];
array[3] = 0;

配列の最後の要素を指すことは未定義の動作ではないことに注意してください( beyond_array = array + 3はここでよく定義されています)が、逆参照は( *beyond_arrayは未定義の動作です)。このルールは、動的に割り当てられたメモリ( mallocによって作成されたバッファなど)にも適用されます。

重複するメモリのコピー

多種多様な標準ライブラリ関数は、1つのメモリ領域から別のメモリ領域にバイトシーケンスをコピーするという効果があります。これらの関数のほとんどは、ソース領域と宛先領域が重複している場合、未定義の動作をします。

たとえば、これは...

#include <string.h> /* for memcpy() */

char str[19] = "This is an example";
memcpy(str + 7, str, 10);

...ソースとコピー先のメモリ領域が3バイトオーバーラップする10バイトをコピーしようとします。視覚化するには:

               overlapping area
               |
               _ _
              |   |
              v   v
T h i s   i s   a n   e x a m p l e \0
^             ^
|             |
|             destination
|
source

オーバーラップのため、結果の動作は未定義です。

この種の制限を持つ標準ライブラリ関数には、 memcpy()strcpy()strcat()sprintf() 、およびsscanf()ます。標準では、これらの機能やその他の機能について説明しています。

オーバーラップするオブジェクト間でコピーが行われる場合、動作は未定義です。

memmove()関数は、このルールの主要な例外です。この定義では、ソースデータが最初に一時バッファにコピーされ、宛先アドレスに書き込まれたかのように関数が動作するように指定されています。ソース領域と宛先領域が重複していることや例外領域がないことは例外ではないので、 memmove()はそのような場合に明確な動作をします。

違いは、 効率を反映しています。一般性トレードオフ。これらの機能のようなコピーは、通常、メモリの分離された領域間で行われ、開発時にメモリコピーの特定のインスタンスがそのカテゴリに含まれるかどうかを知ることができます。ノンオーバーラップを仮定すると、仮定が成立しない場合に正しい結果を確実に生成しない比較的効率的な実装が得られる。ほとんどのCライブラリ関数は、より効率的な実装を許可され、そしてmemmove() 、ソースと宛先が又はオーバーラップを行うことができる場合にサービスを提供する、ギャップを埋めます。しかし、すべてのケースで正しい結果を出すためには、追加のテストを実行しなければならず、かつ/または比較的効率の悪い実装を採用する必要があります。

メモリにバックアップされていない初期化されていないオブジェクトの読み取り

C11

オブジェクトを読み取ると、オブジェクトが1の場合、未定義の動作が発生します。

  • 初期化されていない
  • 自動保存期間で定義
  • そのアドレスは決して取られません

以下の例の変数aは、これらの条件をすべて満たしています。

void Function( void )
{
    int a;
    int b = a;
} 

1 (引用:ISO:IEC 9899:201X 6.3.2.1左辺値、配列、および関数指定子2)
左辺値がレジスタ記憶クラスで宣言された可能性のある記憶領域のオブジェクトを指定していて、そのオブジェクトが初期化されていない(初期化子で宣言されておらず、使用前に実行されていない) )、その動作は未定義です。

データ競争

C11

C11では複数の実行スレッドがサポートされ、データ競合の可能性が生じました。プログラムは、2つの異なるスレッドによって1つのオブジェクトがアクセスされた場合、少なくとも1つのアクセスは非アトミックであり、少なくとも1つはオブジェクトを変更し、プログラムのセマンティクスは2つのアクセスが重複しないことを保証しません時間的に。 2関連するアクセスの実際の同時実行性はデータ競争の条件ではないことに注意してください。データ競争は、異なるスレッドのメモリビューにおける(許容される)不整合から生じるより広いクラスの問題をカバーします。

この例を考えてみましょう。

#include <threads.h>

int a = 0;

int Function( void* ignore )
{
    a = 1;

    return 0;
}

int main( void )
{
    thrd_t id;
    thrd_create( &id , Function , NULL );

    int b = a;

    thrd_join( id , NULL );
}

メインスレッドはthrd_createを呼び出して、新しいスレッド実行関数Functionを開始します。 2番目のスレッドはa変更a 、メインスレッドはa読み取ります。これらのアクセスはどちらもアトミックではなく、2つのスレッドは、重複しないように個別または共同で何もしないため、データ競合が発生します。

このプログラムがデータ競争を避ける方法の中には、

  • メインスレッドは他のスレッドを開始a前にa読み取りを実行できます。
  • メインスレッドはthrd_join経由で他のスレッドが終了したことを確認しa後にa読み取りを実行できます。
  • スレッドはmutexを介してそれらのアクセスを同期させることができ、それぞれがそのmutexをロックしてからアクセスa 、ロックを解除することができます。

mutexオプションが示すように、データ競合を回避するために、メインスレッドがそれを読み込む前に、子スレッドがa変更するなど、特定の操作順序を保証する必要はありません。データの競合を避けるためには、所定の実行に対して、一方のアクセスが他方のアクセスの前に起こることを保証することで十分です。


1オブジェクトの変更または読み取り。

2 (ISO:IEC 9889:201x、セクション5.1.2.4「マルチスレッド実行とデータ競合」より引用)
プログラムの実行には、異なるスレッドに2つの競合するアクションが含まれていて、少なくとも1つがアトミックではなく、どちらもアトミックではない場合、データ競合が含まれます。そのようなデータ競合は、未定義の動作をもたらす。

解放されたポインタの値を読み込む

解放されたポインタの値を読み込んだだけでも(つまり、ポインタを逆参照しようとすることなく)、未定義の振る舞い(UB)です。

char *p = malloc(5);
free(p);
if (p == NULL) /* NOTE: even without dereferencing, this may have UB */
{

}

引用ISO / IEC 9899:2011 、セクション6.2.4§2:

[...]ポインターの値は、それが指し示すオブジェクト(または直前のもの)がその存続期間の終わりに達すると、不確定になります。

明らかに無害の比較や算術を含む、不確定なメモリの使用は、その値がその型のトラップ表現である可能性がある場合、未定義の振る舞いを持つ可能性があります。

文字列リテラルの変更

このコード例では、charポインタpは文字列リテラルのアドレスに初期化されています。文字列リテラルを変更しようとすると、未定義の動作が発生します。

char *p = "hello world";
p[0] = 'H'; // Undefined behavior

しかし、 char変更可能な配列を直接変更するか、ポインタを使って変更することは、初期化子がリテラル文字列であっても当然のことながら未定義の動作ではありません。以下は問題ありません:

char a[] = "hello, world";
char *p = a;

a[0] = 'H';
p[7] = 'W';

これは、配列が初期化されるたびに文字列リテラルが効果的に配列にコピーされるためです(静的持続時間を持つ変数、自動またはスレッド持続時間を持つ変数に対して配列が作成されるたびに、割り当て時間の変数は初期化されません)。配列の内容を変更しても問題ありません。

メモリを2回解放する

メモリを2回解放することは未定義の動作です。

int * x = malloc(sizeof(int));
*x = 9;
free(x);
free(x);

標準からの引用(7.20.3.2。C99の自由な機能):

それ以外の場合、引数がcalloc、malloc、またはrealloc関数によって返されたポインタに一致しない場合、またはfreeまたはreallocの呼び出しによって領域が割り当て解除された場合、動作は未定義です。

printfで不正な書式指定子を使う

printfの最初の引数に不正な書式指定子を使用すると、未定義の動作が発生します。たとえば、次のコードは未定義のビヘイビアを呼び出します。

long z = 'B';
printf("%c\n", z);

別の例があります

printf("%f\n",0);

コードの上の行は未定義の動作です。 %fは二重になります。ただし、0はint型です。

コンパイル時に適切なフラグをオンにすると、コンパイラは通常、このようなケースを避けることができます( clanggcc -Wformat )。最後の例から:

warning: format specifies type 'double' but the argument has type
      'int' [-Wformat]
    printf("%f\n",0);
            ~~    ^
            %d

ポインタ型間の変換により、結果が正しく整列しない

ポインタの位置が正しくないため、以下のような動作が未定義になる可能性があります。

 char *memory_block = calloc(sizeof(uint32_t) + 1, 1);
 uint32_t *intptr = (uint32_t*)(memory_block + 1);  /* possible undefined behavior */
 uint32_t mvalue = *intptr;

ポインタが変換されると、未定義の動作が発生します。 C11によれば、 2つのポインタ型の間変換が誤って整列された結果を生成する場合(6.3.2.3)、動作は未定義です。ここでuint32_tは、たとえば2または4のアラインメントが必要な場合があります。

一方、 callocは、任意のオブジェクト型に対して適切に整列されたポインタを返す必要があります。これmemory_block適切に含むように整列されるuint32_tその最初の部分では。次に、 uint32_tが2または4のアライメントを必要とするシステムでは、 memory_block + 1奇数アドレスになり、したがって正しく整列されません。

C標準は、すでにキャスト操作が未定義であることを要求していることに注意してください。これは、アドレスがセグメント化されているプラ​​ットフォームでは、バイトアドレスmemory_block + 1が整数ポインタとして適切な表現さえできないことがあるため、課される。

char *を他の型へのポインタにキャストすることは、ファイルのヘッダやネットワークパケットなどのパックされた構造をデコードするために、整列要件を気にすることがないことがあります。

memcpyを使用すると、ポインタの位置がずれることによる未定義の動作を回避できます。

memcpy(&mvalue, memory_block + 1, sizeof mvalue);

uint32_t*へのポインター変換は行われず、バイトは1つずつコピーされます。

この例のコピー操作では、 mvalue有効な値になるのは、 mvalue理由によるものです。

  • callocを使用したので、バイトは適切に初期化されています。私たちの場合、すべてのバイトは値0持ちますが、他の適切な初期化が行います。
  • uint32_tは正確な幅の型で、パディングビットはありません
  • 任意のビットパターンは、任意の符号なしタイプの有効な表現です。

ポインタの加算または減算が正しく結合されていない

次のコードは動作が未定義です。

char buffer[6] = "hello";
char *ptr1 = buffer - 1;  /* undefined behavior */
char *ptr2 = buffer + 5;  /* OK, pointing to the '\0' inside the array */
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char *ptr4 = buffer + 7;  /* undefined behavior */

C11によれば、配列オブジェクトと整数型へのポインタの加算または減算が、同じ配列オブジェクトを指し示さない、またはそれを超えた結果を生成する場合、その動作は定義されていません(6.5.6 )。

さらに、配列をちょうど指すポインタを逆参照することは当然のことながら未定義の動作です:

char buffer[6] = "hello";
char *ptr3 = buffer + 6;  /* OK, pointing to just beyond */
char value = *ptr3;       /* undefined behavior */

ポインタを使用してconst変数を変更する

int main (void)
{
    const int foo_readonly = 10;
    int *foo_ptr;

    foo_ptr = (int *)&foo_readonly; /* (1) This casts away the const qualifier */
    *foo_ptr = 20; /* This is undefined behavior */

    return 0;
}

引用ISO / IEC 9899:201x 、セクション6.7.3§2:

非const修飾型の左辺値を使用してconst修飾型で定義されたオブジェクトを変更しようとすると、その動作は未定義です。 [...]


(1) GCCでは、次の警告がスローされることがあります: warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]

printf%s変換へのヌルポインタの受け渡し

printf%s変換は、対応する引数が文字型の配列の最初の要素へのポインタであることを示します。ヌルポインタは文字型の配列の最初の要素を指していないため、次の動作は定義されていません。

char *foo = NULL;
printf("%s", foo); /* undefined behavior */

しかし、未定義の動作が必ずしもプログラムがクラッシュすることを意味するわけではありません。ヌルポインターが参照解除されたときに通常発生するクラッシュを回避するためのシステムもあります。例えば、Glibcは印刷することが知られている

(null)

上記のコードの場合ただし、書式文字列に改行を追加すると、クラッシュすることになります。

char *foo = 0;
printf("%s\n", foo); /* undefined behavior */

この場合、GCCにはprintf("%s\n", argument);を回す最適化があるために起こりprintf("%s\n", argument);呼び出しにputsputs(argument) 、およびputs Glibcの中にヌルポインタを処理しません。この動作はすべて標準に準拠しています。

ヌルポインタ空文字列とは異なります 。したがって、以下は有効で、未定義の動作はありません。それは単に改行を出力します

char *foo = "";
printf("%s\n", foo);

識別子の一貫性のないリンケージ

extern int var;
static int var; /* Undefined behaviour */

C11、§6.2.2、7は言う:

翻訳単位内で同じ識別子が内部および外部の両方のリンケージとともに表示される場合、その動作は不確定です。

以前の識別子の宣言が可視であれば、それは前の宣言のリンケージを持つことに注意してください。 C11、§6.2.2、4はそれを可能にする:

その識別子の事前宣言が見える範囲で外部記憶域クラスで宣言された識別子については、31)前宣言が内部または外部連鎖を指定する場合、後の宣言での識別子の連動は、前の宣言で明示された連鎖。前の宣言が見えない場合、または前の宣言がリンケージを指定していない場合、識別子は外部リンケージを持ちます。

/* 1. This is NOT undefined */
static int var;
extern int var; 


/* 2. This is NOT undefined */
static int var;
static int var; 

/* 3. This is NOT undefined */
extern int var;
extern int var; 

入力ストリームでのfflushの使用

POSIXとCの標準では、入力ストリームでのfflush使用は未定義の動作であることを明示しています。 fflushは出力ストリームに対してのみ定義されます。

#include <stdio.h>

int main()
{
    int i;
    char input[4096];

    scanf("%i", &i);
    fflush(stdin); // <-- undefined behavior
    gets(input);

    return 0;
}

未読文字を入力ストリームから破棄する標準的な方法はありません。一方、いくつかの実装では、 stdinバッファをクリアするためにfflushを使用します。マイクロソフトでは、入力ストリームでfflushの動作を定義しています。ストリームが入力用に開いている場合、 fflushはバッファの内容をクリアします。 POSIX.1-2008によれば、入力ファイルがシーク可能でない限り、 fflushの動作は未定義です。

詳細は、 fflush(stdin)使用を参照してください。

負のカウントを使用するか、またはタイプの幅を超えるビットシフト

シフトカウント値が負の値の場合、 左シフト右シフトの両方の操作は定義されていません1

int x = 5 << -3; /* undefined */
int x = 5 >> -3; /* undefined */

左シフト負の値で実行される場合 、それは未定義です:

int x = -5 << 3; /* undefined */

左シフト正の値で実行され、数学的値の結果がその型で表現できない場合、それは未定義です1

/* Assuming an int is 32-bits wide, the value '5 * 2^72' doesn't fit 
 * in an int. So, this is undefined. */
       
int x = 5 << 72;

負の値 (.eg -5 >> 3 )の右シフト 未定義ではなく実装定義であることに注意してください。


1引用ISO / IEC 9899:201x 、セクション6.5.7:

右オペランドの値が負であるか、または昇格された左オペランドの幅以上である場合、その動作は未定義です。

getenv、strerror、およびsetlocale関数によって返される文字列を変更する

標準関数getenv()strerror()およびsetlocale()によって返される文字列を変更することは定義されていません。実装では、これらの文字列に静的ストレージを使用することがあります。

getenv()関数、C11、§7.22.4.7、4は 、次のように述べています。

getenv関数は、一致したリストメンバーに関連付けられた文字列へのポインタを返します。に指された文字列は、プログラムによって変更されてはならないが、後続のgetenv関数の呼び出しによって上書きされることがある。

strerror()関数、C11、§7.23.6.3、4はこう言っています:

strerror関数は文字列へのポインタを返します。その内容はローカルで指定されています。に指された配列は、プログラムによって変更されてはならないが、その後のstrerror関数の呼び出しによって上書きされる。

setlocale()関数、C11、§7.11.1.1、8はこう言っています:

setlocale関数によって返される文字列へのポインタは、その文字列値とそれに関連するカテゴリを持つ後続の呼び出しがプログラムのロケールのその部分を復元するようなものです。に指された文字列は、プログラムによって変更されてはならないが、その後のsetlocale関数の呼び出しによって上書きされることがある。

同様に、 localeconv()関数は変更されないstruct lconvへのポインタを返します。

localeconv()関数、C11、§7.11.2.1、8はこう言っています:

localeconv関数は、塗りつぶしオブジェクトへのポインタを返します。戻り値が指す構造体は、プログラムによって変更されてはならないが、その後のlocaleconv関数の呼び出しによって上書きされることがある。

`_Noreturn`または` noreturn`関数指定子で宣言された関数からの戻り値

C11

関数指定子_NoreturnはC11で導入されました。ヘッダー<stdnoreturn.h>は、 _Noreturn展開されるマクロnoreturnを提供します。したがって、 <stdnoreturn.h> _Noreturnまたはnoreturnを使用しても_Noreturn

_Noreturn (またはnoreturn )で宣言された関数は、呼び出し元に戻ることはできません。このような関数呼び出し側に返っても、動作は未定義です。

次の例では、 func()noreturn指定子で宣言されていますが、呼び出し元に戻ります。

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void func(void);

void func(void)
{
    printf("In func()...\n");
} /* Undefined behavior as func() returns */

int main(void)
{
    func();
    return 0;
}

gccclangは上記のプログラムに対して警告を出します:

$ gcc test.c
test.c: In function ‘func’:
test.c:9:1: warning: ‘noreturn’ function does return
 }
 ^
$ clang test.c
test.c:9:1: warning: function declared 'noreturn' should not return [-Winvalid-noreturn]
}
^

よく定義された振る舞いを持つnoreturnを使った例:

#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>

noreturn void my_exit(void);

/* calls exit() and doesn't return to its caller. */
void my_exit(void)
{
    printf("Exiting...\n");
    exit(0);
}

int main(void)
{
    my_exit();
    return 0;
}


Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow