59

代码示例:

struct name
{
    int a, b;
};

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

这会导致未定义的行为吗?我们可以辩论它是否“取消引用 null”,但是 C11 没有定义术语“取消引用”。

6.5.3.2/4 明确表示*在空指针上使用会导致未定义的行为;但是它并没有说同样的->意思,也没有定义a -> b为存在(*a).b;它对每个运算符都有单独的定义。

->6.5.2.3/4 中的语义说:

后缀表达式后跟 -> 运算符和标识符指定结构或联合对象的成员。该值是第一个表达式指向的对象的命名成员的值,并且是一个左值。

但是,NULL不指向一个对象,所以第二句话似乎没有指定。

同样相关的可能是 6.5.3.2/1:

约束:

一元运算&符的操作数应该是一个函数指示符、一个 []或一元运算符的结果*,或者是一个左值,它指定一个不是位域且未使用寄存器存储类说明符声明的对象。

但是我觉得粗体文本是有缺陷的,应该读取可能指定对象的左值,根据 6.3.2.1/1(左值的定义 ——C99 弄乱了左值的定义,所以 C11 不得不重写它,也许这个部分错过了。

6.3.2.1/1 确实说:

左值是一个表达式(对象类型不是 void),它可能指定一个对象;如果左值在评估时未指定对象,则行为未定义

但是&运算符确实评估其操作数。(它不访问存储的值,但这是不同的)。

这条长长的推理链似乎表明代码导致了 UB,但是它相当脆弱,而且我不清楚标准的编写者的意图是什么。如果事实上他们有任何意图,而不是让我们来辩论:)

4

6 回答 6

24

从律师的角度来看,该表达式&(((struct name *)NULL)->b);应该导致 UB,因为您找不到没有 UB 的路径。恕我直言,根本原因是此刻您将->运算符应用于不指向对象的表达式。

从编译器的角度来看,假设编译器程序员没有过于复杂,很明显该表达式返回的值与预期的值相同offsetof(name, b),而且我很确定只要它编译时没有错误,任何现有的编译器都会给出该结果。

正如所写,我们不能责怪编译器会注意到在内部部分中您->在表达式上使用运算符而不是不能指向对象(因为它为空)并发出警告或错误。

我的结论是,除非有一个特殊的段落说,如果它只是获取它的地址是合法的,那么取消引用一个空指针是合法的,这个表达式是不合法的 C。

于 2014-11-13T11:10:23.043 回答
18

是的,在英语术语 undefined 的直接意义上,这种使用->具有未定义的行为。

仅当第一个表达式指向对象时才定义行为,否则未定义 (=undefined)。一般来说,您不应该在未定义一词中进行更多搜索,这意味着:标准没有为您的代码提供含义。(有时它明确指出它没有定义的情况,但这不会改变该术语的一般含义。)

这是为了帮助编译器构建者处理事情而引入的松弛。他们可能定义了一种行为,即使对于您呈现的代码也是如此。特别是,对于编译器实现,将此类代码或类似代码用于offsetof宏是非常好的。使此代码违反约束将阻止编译器实现的路径。

于 2014-11-13T11:07:53.073 回答
12

让我们从间接运算符开始*

6.5.3.2 p4:一元 * 运算符表示间接。如果操作数指向一个函数,则结果是一个函数指示符;如果它指向一个对象,则结果是一个指定该对象的左值。如果操作数的类型为“pointer to type”,则结果的类型为“type”。如果为指针分配了无效值,则一元运算符的行为*未定义。102)

*E,其中 E 是一个空指针,是未定义的行为。

有一个脚注指出:

102)因此,&*E等价于 E(即使 E 是空指针),而 &(E1[E2]) 等价于 ((E1)+(E2))。如果 E 是函数指示符或作为一元 & 运算符的有效操作数的左值,则 *&E 是函数指示符或等于 E 的左值。如果 *P 是左值且 T 是对象指针类型,*(T)P 是一个左值,其类型与 T 指向的类型兼容。

这意味着定义了 &*E,其中 E 为 NULL,但问题是对于 &(*E).m 是否也是如此,其中 E 是空指针,其类型是具有成员 m 的结构?

C 标准没有定义这种行为。

如果它被定义,就会出现新的问题,下面列出了其中一个问题。C标准保持它未定义是正确的,并提供了一个宏offsetof在内部处理问题。

6.3.2.3 指针

  1. 值为 0 的整型常量表达式或转换为 void * 类型的表达式称为空指针常量。66) 如果将空指针常量转换为指针类型,则生成的指针(称为空指针)保证与指向任何对象或函数的指针不相等。

这意味着值为 0 的整数常量表达式将转换为空指针常量。

但是空指针常量的值不定义为 0。该值是实现定义的。

7.19 常用定义

  1. 宏是 NULL,它扩展为实现定义的空指针常量

这意味着 C 允许实现,其中空指针将具有设置所有位的值,并且对该值使用成员访问将导致溢出,这是未定义的行为

另一个问题是如何评估 &(*E).m?括号是否适用并首先*进行评估。保持未定义可以解决这个问题。

于 2014-11-13T15:23:42.227 回答
5

首先,让我们确定我们需要一个指向对象的指针:

6.5.2.3 结构和工会成员

4 后缀表达式后跟->运算符和标识符,表示结构或联合对象的成员。该值是第一个表达式指向的对象的命名成员的值,并且是一个左值。96) 如果第一个表达式是一个指向限定类型​​的指针,则结果具有该类型的限定版本指定成员。

不幸的是,没有空指针指向一个对象。

6.3.2.3 指针

3 值为 0 的整型常量表达式,或转换为 type 的此类表达式 void *,称为空指针常量.66) 如果将空指针常量转换为指针类型,则保证生成的指针称为空指针比较不等于指向任何对象或函数的指针

结果:未定义的行为。

作为旁注,还有一些其他的事情需要仔细研究:

6.3.2.3 指针

4 将空指针转换为另一种指针类型会产生该类型的空指针。任何两个空指针应该比较相等。
5 整数可以转换为任何指针类型。除非前面指定,结果是实现定义的,可能没有正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。67)
6 任何指针类型都可以转换为整数类型。除非前面指定,结果是实现定义的。如果结果不能以整数类型表示,则行为未定义。结果不必在任何整数类型的值范围内。

67) 将指针转换为整数或将整数转换为指针的映射函数旨在与执行环境的寻址结构保持一致。

因此,即使这次 UB 碰巧是良性的它仍然可能会导致一些完全出乎意料的数字。

于 2014-11-13T21:57:34.653 回答
0

C 标准中的任何内容都不会对系统可以对表达式执行的操作提出任何要求。在编写标准时,在运行时引发以下事件序列是完全合理的:

  1. 代码将空指针加载到寻址单元中
  2. 代码要求寻址单元添加 field 的偏移量b
  3. 寻址单元在尝试将整数添加到空指针时触发陷阱(为了稳健性,这应该是运行时陷阱,即使许多系统没有捕获它)
  4. 系统在通过从未设置的陷阱向量调度后开始执行基本上随机的代码,因为设置它的代码会浪费内存,因为不应该发生寻址陷阱。

未定义行为的本质在当时意味着什么。

请注意,自 C 早期以来出现的大多数编译器都会将位于常量地址的对象成员的地址视为编译时常量,但我不认为这种行为是强制性的,标准中也没有添加任何内容,要求在运行时计算不会定义的情况下定义涉及空指针的编译时地址计算。

于 2015-04-22T23:56:27.267 回答
-2

不,让我们把它分开:

&(((struct name *)NULL)->b);

是相同的:

struct name * ptr = NULL;
&(ptr->b);

第一行显然有效且定义明确。

在第二行中,我们计算一个字段相对于地址的地址0x0,这也是完全合法的。例如,Amiga 在地址中有指向内核的指针0x4。所以你可以使用这样的方法来调用内核函数。

事实上,在 C 宏offsetof维基百科)上使用了相同的方法:

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

所以这里的困惑围绕着 NULL 指针很可怕这一事实。但从编译器和标准的角度来看,该表达式在 C 中是合法的(C++ 是一种不同的野兽,因为您可以重载&运算符)。

于 2014-11-13T12:17:40.060 回答