web-dev-qa-db-ja.com

ヌルポインタのインクリメントは明確に定義されていますか?

ポインタ演算を行うときの未定義/未指定の動作の例はたくさんあります-ポインタは同じ配列内(または終わりを過ぎたもの)または同じオブジェクト内を指す必要があり、上記に基づいて比較/操作を実行できるタイミングの制限、など。

次の操作は明確に定義されていますか?

int* p = 0;
p++;
47
Luchian Grigore

§5.2.6/ 1:

オペランドオブジェクトの値は、オブジェクトのタイプがbool [..]でない限り、1を追加することによって変更されます。

そして、ポインタを含む加法式は§5.7/ 5で定義されています。

ポインタオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素を1つ過ぎている場合、評価によってオーバーフローが発生することはありません。 それ以外の場合、動作は定義されていません。

36
Columbo

「未定義の振る舞い」が何を意味するのかについての理解はかなり低いようです。

C、C++、およびObjective-Cなどの関連言語には、次の4種類の動作があります。言語標準で定義された動作があります。実装で定義された動作があります。これは、言語標準では、実装で動作を定義する必要があることを明示的に示しています。不特定の振る舞いがあり、言語標準ではいくつかの振る舞いが可能であるとされています。そして、未定義の振る舞いがあります言語標準は結果について何も述べていません。言語標準は結果について何も述べていないため、未定義の動作で何かが発生する可能性があります。

ここでの一部の人々は、「未定義の振る舞い」は「何か悪いことが起こる」ことを意味すると思い込んでいます。それは間違っている。それは「何かが起こり得る」という意味であり、「何か悪いことが起こらなければならない」ではなく、「何か悪いことが起こり得る」を含みます。実際には、「プログラムをテストしても悪いことは何も起こりませんが、顧客に出荷されるとすぐに、すべての地獄が解き放たれます」という意味です。何かが起こる可能性があるため、コンパイラーは実際にはコードに未定義の動作がないと想定できます-それがtrueまたはfalseのいずれかであるため、この場合は何かが発生する可能性があります。つまり、コンパイラーの誤った想定が原因で発生することはすべてまだです。正しい。

Pが3つの要素の配列を指し、p + 4が計算された場合、悪いことは何も起こらないと誰かが主張しました。違う。これが最適化コンパイラです。これがあなたのコードだと言ってください:

int f (int x)
{
    int a [3], b [4];
    int* p = (x == 0 ? &a [0] : &b [0]);
    p + 4;
    return x == 0 ? 0 : 1000000 / x;
}

Pがa [0]を指している場合、p + 4の評価は未定義の動作ですが、b [0]を指している場合はそうではありません。したがって、コンパイラは、pがb [0]を指していると想定できます。したがって、x == 0は未定義の動作につながるため、コンパイラはx!= 0と見なすことができます。したがって、コンパイラはreturnステートメントのx == 0チェックを削除し、1000000/xを返すことができます。つまり、0を返す代わりにf(0)を呼び出すと、プログラムがクラッシュします。

もう1つの仮定は、nullポインターをインクリメントしてから再度デクリメントすると、結果は再びnullポインターになるというものでした。また違う。一部のハードウェアでnullポインターのインクリメントがクラッシュする可能性は別として、これについてはどうでしょうか。nullポインターのインクリメントは未定義の動作であるため、コンパイラーはポインターがnullかどうかをチェックし、nullポインターでない場合にのみポインターをインクリメントします。 、したがって、p +1は再びnullポインタです。通常、デクリメントについても同じことを行いますが、巧妙なコンパイラであるため、結果がnullポインタの場合、p + 1は常に未定義の動作であることに気付きます。したがって、p +1はnullポインタではないと見なすことができます。したがって、nullポインタチェックを省略できます。つまり、pがnullポインターの場合、(p + 1)-1はnullポインターではありません。

15
gnasher729

ポインターの操作(インクリメント、加算など)は、通常、ポインターの初期値と結果の両方が同じ配列の要素(または最後の要素を過ぎた要素)を指している場合にのみ有効です。それ以外の場合、結果は未定義です。インクリメントや加算など、これを言うさまざまな演算子の標準にはさまざまな条項があります。

(NULLにゼロを加算したりNULLからゼロを減算したりするなどの例外がいくつかありますが、ここでは適用されません)。

NULLポインターは何も指さないため、NULLポインターをインクリメントすると、未定義の動作が発生します(「それ以外の場合」の句が適用されます)。

13
Peter

実際には未定義であることが判明しました。これが当てはまるシステムがあります

int *p = NULL;
if (*(int *)&p == 0xFFFF)

したがって、++ pは未定義のオーバーフロールールをトリップします(sizeof(int *)== 2)であることがわかります)。ポインタは符号なし整数であることが保証されていないため、符号なしラップルールは適用されません。

1
Joshua

コロンボが言ったように、それはUBです。そして、言語弁護士の観点から、これは決定的な答えです。

ただし、私が知っているすべてのC++コンパイラの実装では、同じ結果が得られます。

int *p = 0;
intptr_t ip = (intptr_t) p + 1;

cout << ip - sizeof(int) << endl;

0を返します。これは、pの値が32ビットの実装では4、64ビットの実装では8であることを意味します。

別の言い方をすると:

int *p = 0;
intptr_t ip = (intptr_t) p; // well defined behaviour
ip += sizeof(int); // integer addition : well defined behaviour 
int *p2 = (int *) ip;      // formally UB
p++;               // formally UB
assert ( p2 == p) ;  // works on all major implementation
0
Serge Ballesta

からISO IEC14882-2011§5.2.6

後置++式の値は、そのオペランドの値です。 [注:取得された値は元の値のコピーです—文末脚注]オペランドは変更可能な左辺値でなければなりません。オペランドの型は、算術型または完全なオブジェクト型へのポインタでなければなりません。

Nullptrは、完全なオブジェクトタイプへのポインタであるためです。したがって、これが未定義の動作である理由がわかりません。

前に述べたように、同じ文書は§5.2.6/ 1にも述べています:

ポインタオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素を1つ過ぎている場合、評価によってオーバーフローが発生することはありません。それ以外の場合、動作は定義されていません。

この表現は少し曖昧に見えます。私の解釈では、未定義の部分はオブジェクトの評価である可能性が非常に高いです。そして、私は誰もこれが事実であることに異議を唱えることはないと思います。ただし、ポインタ演算には完全なオブジェクトのみが必要なようです。

もちろん、配列オブジェクトへのポインタの接尾辞[]演算子と減算または乗算は、実際に同じ配列を指している場合にのみ明確に定義されます。最も重要なのは、1つのオブジェクトで連続して定義された2つの配列が、単一の配列であるかのように繰り返すことができると考えたくなるかもしれないからです。

したがって、私の結論は、操作は明確に定義されているが、評価は明確ではないということです。

0
laurisvr

C標準では、標準で定義された手段を介して作成されたオブジェクトが、nullポインターと等しいアドレスを持つことができないことを要求しています。実装では、標準で定義された手段で作成されていないオブジェクトの存在が許可される場合がありますが、標準では、そのようなオブジェクトが(ハードウェア設計の問題のために)nullポインタと同じアドレスを持っているかどうかについては何も述べていません。 。

実装が、アドレスがnullに等しいマルチバイトオブジェクトの存在を文書化する場合、その実装でchar *p = (char*)0;と言うと、pはそのオブジェクトの最初のバイトへのポインタを保持します。 [これはnullポインタに等しいと比較されます]、およびp++は、2番目のバイトを指すようにします。ただし、実装がそのようなオブジェクトの存在を文書化するか、そのようなオブジェクトが存在するかのようにポインタ演算を実行するように指定しない限り、特定の動作を期待する理由はありません。ゼロまたは他のnullポインターを加算または減算する以外に、nullポインターに対してあらゆる種類の演算を実行する試みを意図的にトラップする実装があると、安全対策として役立ちます。また、意図された有用な目的でnullポインターをインクリメントするコードは互換性がありません。さらに悪いことに、一部の「巧妙な」コンパイラは、nullを保持していてもインクリメントされるポインタの場合に、nullチェックを省略できると判断する可能性があります。これにより、あらゆる種類の大混乱が発生します。

0
supercat