&((struct name *)NULL – > b)在C11中导致未定义的行为?
代码示例:
struct name { int a, b; }; int main() { &(((struct name *)NULL)->b); }
这是否会导致未定义的行为? 我们可以辩论它是否“解除null”,然而C11并没有定义“dereference”这个术语。
6.5.3.2/4清楚地说,在空指针上使用*
会导致未定义的行为; 但是它并没有对->
表示同样的效果,也没有将a -> b
定义为(*a).b
; 它对每个运营商都有单独的定义。
6.5.2.3/4中的->
的语义说:
后缀expression式后跟 – >运算符,一个标识符指定一个结构或联合对象的成员。 该值是第一个expression式所指向的对象的指定成员的值,并且是一个左值。
但是, NULL
不指向一个对象,所以第二个句子似乎没有指定。
相关的也许是6.5.3.2/1:
约束:
一元
&
运算符的操作数应该是函数标识符,[]
或一元运算符的结果,或者是一个左值,它指定一个不是位域的对象 ,并且不用寄存器存储类说明符声明。
但是我觉得粗体文本是有缺陷的,应该读取可能指定一个对象的左值,按照6.3.2.1/1( 左值的定义) – C99弄乱了左值的定义,所以C11不得不重写它,也许这个节错过了。
6.3.2.1/1确实说:
左值是一个expression式(具有非空的对象types),可能指定一个对象; 如果左值在评估时没有指定对象,则行为是不确定的
然而&
运算符确实评估它的操作数。 (它不访问存储的值,但是不同)。
这个长长的推理链似乎暗示了代码会导致UB,但是它相当脆弱,我不清楚标准的作者所打算的。 如果实际上他们打算做什么,而不是让我们来辩论:)
从律师的angular度来看,expression式&(((struct name *)NULL)->b);
应该导致UB,因为你找不到一个UB没有的path。 恕我直言,根本原因是,你在一个expression式上应用->
运算符,而不是指向一个对象。
从编译器的angular度来看,假设编译器程序员没有过于复杂,很明显expression式返回的值与offsetof(name, b)
,我敢肯定, 只要编译时没有错误,任何现有的编译器会给出这个结果。
正如所写,我们不能责怪一个编译器,会注意到在内部使用operator ->
expression式,而不是指向一个对象(因为它是空的),并发出警告或错误。
我的结论是,除非有一个特别的段落说只要它的地址是合法的,否则引用一个空指针,这个expression式是不合法的。
是的,这个使用->
在未定义的英文术语的直接意义上有未定义的行为。
行为只在第一个expression式指向一个对象时才被定义,否则不定义(=未定义)。 一般来说,你不应该在未定义的术语中search更多,这意味着:标准没有提供代码的含义。 (有时它明确地指出了它没有定义的情况,但这并不改变该术语的一般含义。)
这是为了帮助编译器构build者处理事情而引入的一种松懈。 他们可能会定义一个行为,即使是你正在呈现的代码。 特别是,对于编译器实现来说,使用这样的代码或类似的macros来offsetof
macros是完全正确的。 使这个代码违反约束会阻止编译器实现的path。
我们从间接运算符*
:
6.5.3.2 p4:一元*运算符表示间接。 如果操作数指向一个函数,则结果是一个函数指示符; 如果它指向一个对象,结果是一个指定对象的左值。 如果操作数具有types“指向types的指针”,则结果的types为“types”。 如果指针指定了无效值,则一元运算符的行为是未定义的。 102)
E,其中E是一个空指针,是未定义的行为。
有一个脚注说:
102) 因此,
&*E
相当于E(即使E是一个空指针) ,而E(E1 [E2]) 等于 ((E1)+(E2))。 如果E是一个函数指示符或左值是一元&运算符的有效操作数,那么总是如此,*&E是一个函数标识符或等于E的左值。如果* P是一个左值,并且T是一个对象指针types,*(T)P是一个左值,与T指向的types兼容。
这意味着定义了E为NULL的&* E,但是问题是对于&(* E).m是否也是如此,其中E是一个空指针,它的types是一个具有成员m的结构?
C标准没有定义这种行为。
如果确定了,会出现新的问题,其中之一列在下面。 C标准是正确的,以保持它未定义,并提供一个macros内部处理的问题。
6.3.2.3指针
- 值为0的整数常量expression式,或者转换为void *types的expression式称为空指针常量。 如果一个空指针常量被转换为一个指针types,那么被调用的空指针所产生的指针将被保证与指向任何对象或函数的指针进行比较。
这意味着将值为0的整数常量expression式转换为空指针常量。
但是空指针常量的值不是定义为0.该值是实现定义的。
7.19通用定义
- macros是NULL,扩展为实现定义的空指针常量
这意味着C允许一个实现,其中空指针将有一个值,其中所有的位都被设置,并且使用该值的成员访问会导致一个未定义行为的溢出
另一个问题是你如何评估&(* E).m? 括号是否适用,并首先进行评估。 保持它undefined解决了这个问题。
首先,让我们确定我们需要一个指向对象的指针:
6.5.2.3结构和联盟成员
4后缀expression式,后跟
->
运算符,标识符指定结构或联合对象的成员 。 值是第一个expression式所指向的对象的已命名成员的值,并且是一个左值.96)如果第一个expression式是指向限定types的指针,则结果具有如此限定的指定成员。
不幸的是,没有空指针指向一个对象。
6.3.2.3指针
3一个整数常量expression式的值为0,或者这样的一个expression式types为
void *
,被称为一个空指针常量 .66)如果一个空指针常量被转换为一个指针types,那么结果指针被称为空指针 , 保证比较不等于指向任何对象或函数的指针 。
结果:未定义的行为。
作为一个侧面说明,还有一些其他的东西需要咀嚼:
6.3.2.3指针
4将空指针转换为另一个指针types会产生该types的空指针。 任何两个空指针应该相等。
5一个整数可以转换为任何指针types。 除了之前指定的,结果是实现定义的,可能不正确alignment,可能不指向引用types的实体,并且可能是陷阱表示。
6任何指针types都可以转换为整数types。 除了以前指定的,结果是实现定义的。 如果结果不能用整数types表示,则行为是不确定的。 结果不需要在任何整数types的值的范围内。67)用于将指针转换为整数或整数的映射函数旨在与执行环境的寻址结构一致。
所以即使这次UB应该是良性的,也可能会导致一些完全意外的数字。
不,我们把这个分开:
&(((struct name *)NULL)->b);
是相同的:
struct name * ptr = NULL; &(ptr->b);
第一行显然是有效的和明确的。
在第二行中,我们计算相对于地址0x0
的字段地址,这也是完全合法的。 例如,Amiga在地址0x4
有指向内核的指针。 所以你可以使用这样的方法调用内核函数。
实际上,在Cmacros的offsetof
( 维基百科 )上使用了相同的方法:
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
所以这里的困惑是围绕NULL指针是可怕的事实。 但从编译器和标准的angular度来看,expression式在C中是合法的(C ++是一个不同的野兽,因为你可以重载&
运算符)。
C标准中的任何内容都不会对系统能够对expression做出什么要求。 当标准写入时,它会在运行时导致下面的一系列事件是完全合理的:
- 代码在寻址单元中加载一个空指针
- 代码要求寻址单元添加字段
b
的偏移量。 - 寻址单元在尝试向空指针中添加一个整数时触发一个陷阱(尽pipe很多系统不会捕获它,但它应该是一个运行时陷阱)
- 系统在通过从未设置的陷阱向量分派之后开始执行基本上随机的代码,因为设置代码将浪费内存,因为寻址陷阱不应该发生。
未定义行为在当时意味着什么。
请注意,自C早期出现以来,大多数编译器将把位于一个常量地址的对象成员的地址视为编译时常量,但我不认为这样的行为是强制的,也没有任何东西被添加到标准中,要求在运行时间计算不需要的情况下定义涉及空指针的编译时地址计算。