59

コードサンプル:

struct name
{
    int a, b;
};

int main()
{
    &(((struct name *)NULL)->b);
}

これにより、未定義の動作が発生しますか? 「null を逆参照する」かどうかは議論できますが、C11 では「逆参照」という用語は定義されていません。

6.5.3.2/4 は*、null ポインターで使用すると未定義の動作が発生することを明確に示しています。ただし、 for と同じことを言っておらず、 beingと->定義していません。演算子ごとに個別の定義があります。a -> b(*a).b

->6.5.2.3/4のセマンティクスは次のように述べています。

-> 演算子と識別子が後に続く後置式は、構造体または共用体オブジェクトのメンバーを指定します。値は、最初の式が指すオブジェクトの名前付きメンバーの値であり、左辺値です。

ただし、NULLオブジェクトを指していないため、2 番目の文は指定不足のようです。

6.5.3.2/1 も関連する可能性があります。

制約:

単項演算子のオペランドは&、関数指定子、 単項演算子[]または単項演算*子の結果、またはビット フィールドではなく、レジスタ ストレージ クラス指定子で宣言されていないオブジェクトを指定する左辺値のいずれかでなければなりません。

ただし、太字のテキストには欠陥があり、6.3.2.1/1 ( lvalueの定義) に従って、オブジェクトを指定する可能性のある lvalue を読み取る必要があると思います- C99 は lvalue の定義をめちゃくちゃにしたため、C11 はそれを書き直さなければなりませんでした。セクションが抜けました。

6.3.2.1/1 は言う:

左辺値は、潜在的にオブジェクトを指定する (void 以外のオブジェクト型を持つ) 式です。左辺値が評価時にオブジェクトを指定しない場合、動作は未定義です

ただし、&演​​算子そのオペランドを評価します。(保存された値にはアクセスしませんが、それは異なります)。

この長い一連の推論は、コードが UB を引き起こしていることを示唆しているように見えますが、それはかなり希薄であり、標準の作成者が何を意図したかは私には明らかではありません。実際に彼らが何かを意図していた場合、私たちに議論を任せるのではなく:)

4

6 に答える 6

18

はい、この の使用に->は、英語の undefined という用語の直接的な意味で未定義の動作があります。

動作は、最初の式がオブジェクトを指している場合にのみ定義され、それ以外の場合は定義されません (=未定義)。一般に、未定義という用語をこれ以上検索するべきではありません。これは、標準がコードに意味を提供していないことを意味します。(定義されていない状況を明示的に指す場合もありますが、これは用語の一般的な意味を変更しません。)

これは、コンパイラ ビルダーが問題を処理できるようにするために導入された緩みです。提示しているコードであっても、動作を定義している場合があります特に、コンパイラの実装では、そのようなコードまたは類似のコードをoffsetofマクロに使用してもまったく問題ありません。このコードを制約違反にすると、コンパイラ実装のパスがブロックされます。

于 2014-11-13T11:07:53.073 に答える
12

間接演算子から始めましょう*:

6.5.3.2 p4: 単項 * 演算子は間接参照を示します。オペランドが関数を指している場合、結果は関数指定子になります。オブジェクトを指している場合、結果はオブジェクトを指定する左辺値です。オペランドの型が「型へのポインター」の場合、結果の型は「型」になります。ポインターに無効な値が割り当てられている場合、単項演算子の動作*は未定義です。102)

*E (E はヌル ポインター) は未定義の動作です。

次のような脚注があります。

102)したがって、&*Eは E (たとえ E が null ポインターであっても) と同等であり、&(E1[E2]) は ((E1)+(E2)) と同等です。E が単項 & 演算子の有効なオペランドである関数指定子または左辺値である場合、*&E は関数指定子または E と等しい左辺値であることが常に真です。*P が左辺値であり、T が関数の名前である場合オブジェクト ポインター型、*(T)P は、T が指す型と互換性のある型を持つ左辺値です。

これは、E が NULL である &*E が定義されていることを意味しますが、問題は、E がヌル ポインターであり、その型がメンバー m を持つ構造体である &(*E).m についても同じことが当てはまるかどうかです。 ?

C 標準では、その動作は定義されていません。

定義された場合、新たな問題が発生します。そのうちの 1 つを以下に示します。C 標準は未定義のままにしておくのが適切であり、内部で問題を処理するマクロ offsetof を提供します。

6.3.2.3 ポインタ

  1. 値が 0 の整数定数式、または型 void * にキャストされたそのような式は、NULL ポインター定数と呼ばれます。66) null ポインター定数がポインター型に変換される場合、null ポインターと呼ばれる結果のポインターは、任意のオブジェクトまたは関数へのポインターと等しくないことが保証されます。

これは、値が 0 の整数定数式がヌル ポインター定数に変換されることを意味します。

ただし、null ポインター定数の値は 0 として定義されていません。値は実装定義です。

7.19 一般的な定義

  1. マクロは NULL であり、実装定義のヌル ポインター定数に展開されます。

つまり、C では、ヌル ポインターがすべてのビットが設定された値を持つ実装を許可し、その値に対してメンバー アクセスを使用すると、未定義の動作であるオーバーフローが発生します。

もう 1 つの問題は、&(*E).m をどのように評価するかということです。括弧が適用され、*最初に評価されますか。未定義のままにしておくと、この問題は解決します。

于 2014-11-13T15:23:42.227 に答える
5

まず、オブジェクトへのポインターが必要であることを確認しましょう。

6.5.2.3 構造体と共用体のメンバー

->4演算子と識別子が後に続く後置式は、構造体または共用体オブジェクトのメンバーを指定します。値は、最初の式が指し示すオブジェクトの名前付きメンバーの値であり、左辺値です.96) 最初の式が修飾された型へのポインターである場合、結果には、指定メンバー。

残念ながら、null ポインターがオブジェクトを指すことはありません。

6.3.2.3 ポインタ

3 値が 0 の整数定数式、または型にキャストされたそのような式 は、ヌル ポインター定数void *と呼ばれます。66) ヌル ポインター定数がポインター型に変換される場合、ヌル ポインターと呼ばれる結果ポインターオブジェクトまたは関数へのポインターと等しくないことを比較します

結果: 未定義の動作。

補足として、噛み砕くべき他のいくつかのこと:

6.3.2.3 ポインタ

4 null ポインターを別のポインター型に変換すると、その型の null ポインターが生成されます。任意の 2 つのヌル ポインターは、比較すると等しくなります。
5 整数は、任意のポインター型に変換できます。前に指定された場合を除き、結果は実装定義であり、正しく配置されていない可能性があり、参照された型のエンティティを指していない可能性があり、トラップ表現である可能性があります.67)
6 任意のポインター型を整数型に変換できます。前に指定された場合を除き、結果は実装定義です。結果が整数型で表現できない場合、動作は未定義です。結果は、任意の整数型の値の範囲内にある必要はありません。

67) ポインタを整数に、または整数をポインタに変換するためのマッピング関数は、実行環境のアドレッシング構造と一致するように意図されています。

したがって、今回の UB がたまたま良性であったとしても、まったく予想外の数値になる可能性があります。

于 2014-11-13T21:57:34.653 に答える
0

C 標準では、システムが式を使用して実行できることについて要件を課すものはありません。標準が作成された時点では、実行時に次の一連のイベントを引き起こすことは完全に合理的でした。

  1. コードはヌル ポインターをアドレッシング ユニットにロードします。
  2. コードは、アドレッシング ユニットに field のオフセットを追加するように要求しますb
  3. アドレス指定ユニットは、null ポインターに整数を追加しようとするとトラップをトリガーします (多くのシステムがキャッチしない場合でも、堅牢性のためにランタイム トラップにする必要があります)。
  4. システムは、設定されていないトラップ ベクトルを介してディスパッチされた後、本質的にランダムなコードの実行を開始します。これは、アドレス指定トラップが発生してはならないため、設定するコードがメモリの無駄になるためです。

当時の Undefined Behavior の本質そのものです。

C の初期の頃から登場したコンパイラのほとんどは、定数アドレスにあるオブジェクトのメンバーのアドレスをコンパイル時の定数と見なすことに注意してください。また、実行時の計算では定義されない場合に、NULL ポインターを含むコンパイル時のアドレス計算を定義することを義務付ける標準には何も追加されていません。

于 2015-04-22T23:56:27.267 に答える