コードサンプル:
struct name
{
int a, b;
};
int main()
{
&(((struct name *)NULL)->b);
}
これは未定義の動作を引き起こしますか?それが「nullを逆参照する」かどうかを議論することはできますが、C11は「逆参照」という用語を定義していません。
6.5.3.2/4は、nullポインタで*
を使用すると、未定義の動作が発生することを明確に示しています。ただし、->
については同じことを言っておらず、a -> b
を(*a).b
として定義していません。演算子ごとに個別の定義があります。
6.5.2.3/4の->
のセマンティクスは次のように述べています。
後置式の後に->演算子と識別子が続くと、構造体または共用体オブジェクトのメンバーを指定します。値は、最初の式が指すオブジェクトの名前付きメンバーの値であり、左辺値です。
ただし、NULL
はオブジェクトを指していないため、2番目の文は指定が不十分なようです。
6.5.3.2/1も関連する可能性があります。
制約:
単項
&
演算子のオペランドは、関数指定子、[]
または単項*
演算子の結果、またはオブジェクトを指定する左辺値のいずれかでなければなりません。これはビットフィールドではなく、レジスタストレージクラス指定子で宣言されていません。
ただし、太字のテキストには欠陥があり、次のように読む必要がありますオブジェクトを指定する可能性のある左辺値、6.3.2.1/1(左辺値の定義)に従って-C99が台無し左辺値の定義なので、C11はそれを書き直さなければならず、おそらくこのセクションは見落とされていました。
6.3.2.1/1は言う:
左辺値は、オブジェクトを指定する可能性のある式(void以外のオブジェクトタイプ)です。評価時に左辺値がオブジェクトを指定しない場合、動作は定義されていません
ただし、&
演算子doesはそのオペランドを評価します。 (保存された値にはアクセスしませんが、それは異なります)。
この長い推論の連鎖は、コードがUBを引き起こすことを示唆しているように見えますが、それはかなり希薄であり、標準の作成者が何を意図していたかは私にはわかりません。実際に彼らが何かを意図したのなら、それを私たちに任せて議論するのではなく:)
弁護士の観点からは、式&(((struct name *)NULL)->b);
はUBにつながるはずです。これは、UBが存在しないパスを見つけることができなかったためです。私見の根本的な原因は、オブジェクトを指していない式に_->
_演算子を適用したことです。
コンパイラーの観点から、コンパイラー・プログラマーが過度に複雑でなかったと仮定すると、式がoffsetof(name, b)
と同じ値を返すことは明らかであり、コンパイルされている場合)エラーなし既存のコンパイラはその結果を出します。
書かれているように、内部でオブジェクトを指すことができない(nullであるため)式で演算子_->
_を使用し、警告またはエラーを発行することに注意するコンパイラを非難することはできませんでした。
私の結論は、そのアドレスを取ることだけが合法であるという特別な段落があるまで、nullポインタを逆参照することは合法であるということです。この式は合法ではありません。
はい、この->
の使用は、英語の用語undefinedの直接的な意味で未定義の動作をします。
動作は、最初の式がオブジェクトを指している場合にのみ定義され、それ以外の場合は定義されません(=未定義)。一般に、未定義という用語でこれ以上検索するべきではありません。つまり、標準ではコードに意味がありません。 (定義されていない状況を明示的に指している場合もありますが、これによって用語の一般的な意味が変わることはありません。)
これは、コンパイラビルダーが物事を処理するのを助けるために導入された緩みです。 彼らあなたが提示しているコードに対してさえ、振る舞いを定義するかもしれません。特に、コンパイラの実装では、offsetof
マクロにそのようなコードなどを使用することはまったく問題ありません。このコードを制約違反にすると、コンパイラ実装のパスがブロックされます。
間接演算子*
から始めましょう:
6.5.3.2 p4:単項*演算子は間接参照を示します。オペランドが関数を指している場合、結果は関数指定子になります。オブジェクトを指している場合、結果はオブジェクトを指定する左辺値になります。 オペランドの型が「pointerto type」の場合、結果の型は「type」になります。ポインタに無効な値が割り当てられている場合、単項
*
演算子の動作は未定義です。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はNULLポインターであり、その型はメンバーmを持つ構造体です。 ?
C標準はその動作を定義していません。
それが定義された場合、新しい問題が発生します。その1つを以下に示します。 C標準は、それを未定義に保つために正しく、問題を内部的に処理するマクロoffsetofを提供します。
6.3.2.3ポインタ
- 値が0の整数定数式、またはvoid *型にキャストされたそのような式は、nullポインター定数と呼ばれます。 66)nullポインター定数がポインター型に変換される場合、nullポインターと呼ばれる結果のポインターは、任意のオブジェクトまたは関数へのポインターと等しくないと比較されることが保証されます。
これは、値が0の整数定数式がnullポインター定数に変換されることを意味します。
ただし、nullポインタ定数の値は0として定義されていません。値は実装で定義されています。
7.19一般的な定義
- マクロはNULLであり、実装定義のNULLポインター定数に展開されます。
つまり、Cは、nullポインターがすべてのビットが設定された値を持ち、その値でメンバーアクセスを使用すると、未定義の動作であるオーバーフローが発生する実装を許可します。
もう1つの問題は、&(* E).mをどのように評価するかです。括弧が適用され、最初に*
評価されますか。未定義のままにしておくと、この問題が解決します。
まず、オブジェクトへのポインタが必要であることを確認しましょう。
6.5.2.3構造と組合員
4後置式の後に
->
演算子と識別子が続きます構造体または共用体オブジェクトのメンバーを指定します。値は、最初の式が指すオブジェクトの名前付きメンバーの値であり、左辺値です。96)最初の式が修飾型へのポインターである場合、結果には、その型のそのように修飾されたバージョンが含まれます。指定メンバー。
残念ながら、nullポインタがオブジェクトを指すことはありません。
6.3.2.3ポインタ
3値が0の整数定数式、またはタイプ
void *
にキャストされたそのような式は、nullポインター定数。66)と呼ばれます。ヌルポインタ定数がポインタ型に変換される場合、nullポインタと呼ばれる結果のポインタは等しくないものと比較することが保証されます任意のオブジェクトまたは関数へのポインタ。
結果:未定義の振る舞い。
補足として、噛み砕くべき他のいくつかの事柄:
6.3.2.3ポインタ
4 nullポインターを別のポインター型に変換すると、その型のnullポインターが生成されます。任意の2つのヌルポインタは等しく比較されます。
5整数は任意のポインタータイプに変換できます。以前に指定された場合を除き、結果は実装定義であり、正しく整列されていない可能性があり、参照されたタイプのエンティティを指していない可能性があり、トラップ表現である可能性があります67)。
6任意のポインタ型を整数型に変換できます。以前に指定された場合を除き、結果は実装によって定義されます。結果を整数型で表現できない場合、動作は未定義です。結果は、整数型の値の範囲内である必要はありません。67)ポインタを整数に、または整数をポインタに変換するためのマッピング関数は、実行環境のアドレス指定構造と一致することを目的としています。
したがって、UBがたまたま良性であったとしても、まったく予期しない数になる可能性があります。
いいえ。これを分解しましょう。
&(((struct name *)NULL)->b);
と同じです:
struct name * ptr = NULL;
&(ptr->b);
最初の行は明らかに有効であり、明確に定義されています。
2行目では、アドレス0x0
を基準にしてフィールドのアドレスを計算します。これも完全に合法です。たとえば、Amigaには、アドレス0x4
にカーネルへのポインタがありました。したがって、このようなメソッドを使用してカーネル関数を呼び出すことができます。
実際、同じアプローチがCマクロoffsetof
( wikipedia )で使用されています。
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
したがって、ここでの混乱は、NULLポインターが怖いという事実を中心に展開しています。ただし、コンパイラと標準の観点から、式はCでは有効です(&
演算子をオーバーロードできるため、C++は別の獣です)。
C標準には、システムが式で何ができるかについての要件を課すものはありません。標準が作成されたとき、実行時に次の一連のイベントを発生させることは完全に合理的でした。
b
のオフセットを追加するように要求します。未定義の振る舞いが当時意味していたことの本質。
Cの初期から登場したコンパイラのほとんどは、定数アドレスにあるオブジェクトのメンバーのアドレスをコンパイル時定数と見なしますが、そのような動作が義務付けられたとは思わないことに注意してください。また、ランタイム計算で定義されない場合に、ヌルポインタを含むコンパイル時アドレス計算を定義することを義務付けるものも標準に追加されていません。