有一个结构:
struct S {
char m0;
double m1;
short m2;
char m3;
};
这是它的工作原理:
std::cout << &(((S*)0)->m1) << std::endl;
这就是它抛出异常的方式нарушение прав доступа...:
std::cout << (((S*)0)->m1) << std::endl;
为什么会这样?毕竟,在第一个子表达式中还可以访问未分配的内存。
实际上,在第二种情况下,您访问了内存(查看值但地址不正确),但在第一种情况下,您没有:计算字段的地址,即 它只是做简单的算术(考虑对齐),没有真正的内存访问。
我不知道这种行为有多正确(如果从标准的角度来看有任何 UB),但实际上它只是
offsetof对一个不会威胁任何东西的领域的计算。从 stddef.h 到 VC++:
我怀疑是否有一个编译器会在这种情况下实际执行取消引用...
求值时,将 form 的表达式
E1->E2转换为等效的 form(*(E1)).E2。expr.ref / 2:我们得到这个表达式被
((S*)0)->m1解释为一个表达式( *((S*)0) ).m1。那些。这是对象类型空指针被取消引用的地方,这是未定义的行为。本声明基于语言标准的以下条款。expr.unary.op / 1:
取消引用指针会产生一个引用对象的左值,但空指针不指向任何对象。
dcl.ref/注2:
它明确指出取消引用空对象指针会导致未定义的行为。
enSO的相关问题:
因此,两个表达式的行为
&(((S*)0)->m1)和(((S*)0)->m1)都是未定义的,因为 他们的计算需要取消引用空对象指针。并且编译器在优化过程中可以避免实际访问指针所指向的“对象”并不重要。语言标准不需要未定义的行为。程序可能会成功编译并表现出预期的行为,也可能会在运行时失败,或者可能会观察到其他一些行为。
事实上,取消引用不指向对象的指针的情况要复杂一些。让我们看下面的例子:
在这个例子和问题的例子之间
有一定的相似性——实际上,指针所指向的“对象”的值并不重要。
在这里
&a[10],我们不关心数组最后一个元素后面的值是什么——我们需要一个指向数组最后一个元素后面的元素的指针。同样,这里&(((S*)0)->m1)字段的具体值对我们来说并不重要m1,我们感兴趣的是指针。按照直观的期望重新定义上述代码片段的行为会很好。
此外,语言标准存在一些不一致之处。运算符
typeid声明定义了取消引用空指针的结果。expr.typeid / 3:在实际上不需要访问该值的情况下,已经进行了几次尝试以使不指向对象的指针的取消引用合法化。
在 C 中,运算符
&和*在某些情况下会相互抵消(请参阅:数组指针微妙之处)。在 C++ 语言中,他们尝试引入一种特殊的左值 -空左值,但这些尝试仍停留在草稿阶段(请参阅:Isindirection through a null pointer undefined behavior?)。
在一种情况下,语言标准需要对实现进行相当严格的检查,以检查表达式中是否存在未定义的行为——这些是常量表达式。
考虑以下代码:
此代码编译并成功运行(g++,clang)。
但是,如果您尝试取消引用空指针(即使您实际上不需要访问该值),您也会收到编译错误。
克++:
铿锵声:
与未定义行为的情况一样,这里的错误是期望一些特定的结果。甚至没有理由相信表单的构造
((S*)0)->m1一定会导致程序中零地址的取消引用。这种取消引用存在于代码中,但它将在程序中变成什么是未知的。它可以被执行,也可以被跳过(因为编译器有权假设程序中没有空指针的解引用),或者它会破坏代码生成并导致完全意想不到的后果。