代码示例:
struct name
{
int a, b;
};
int main()
{
&(((struct name *)NULL)->b);
}
这是否会导致未定义的行为?我们可以讨论它是否"取消引用null",但是C11并没有定义"取消引用"这个术语。
6.5.3.2/4明确指出,在空指针上使用*
会导致未定义的行为;然而它没有对CCD_ 2说同样的话,并且它也没有将CCD_ 3定义为CCD_;它对每个运算符都有单独的定义。
6.5.2.3/4中->
的语义表示:
后缀表达式后跟->运算符和标识符,用于指定成员结构或联合对象的。该值是对象的命名成员的值第一表达式所指向的并且是左值。
然而,NULL
并不指向宾语,因此第二句似乎没有具体说明。
同样相关的可能是6.5.3.2/1:
限制:
一元
&
运算符的操作数应为函数指示符[]
或一元*
运算符,或指定对象的左值,该对象不是位字段未使用寄存器存储类说明符声明。
然而,我觉得粗体文本有缺陷,应该按照6.3.2.1/1(lvalue的定义)读取可能指定对象的lvalue——C99弄乱了lvalue的定义,所以C11不得不重写它,也许这一部分被遗漏了。
6.3.2.1/1确实说明:
左值是一个表达式(对象类型不是void),它可能指定对象;如果评估时左值未指定对象,则行为未定义
但是CCD_ 10运算符确实计算其操作数。(它不访问存储的值,但这是不同的)。
这一长串的推理似乎表明代码导致了UB,但它相当脆弱,我不清楚标准的作者的意图。如果事实上他们有任何意图,而不是让我们争论:)
&(((struct name *)NULL)->b);
应该指向UB,因为你找不到一条没有UB的路径。IMHO根本原因是在某个时刻对不指向对象的表达式应用->
运算符。
从编译器的角度来看,假设编译器程序员没有过度复杂,很明显,该表达式返回的值与offsetof(name, b)
相同,我非常确信如果编译时没有错误,任何现有的编译器都会给出这个结果。
正如所写的那样,我们不能责怪编译器,因为编译器会注意到,在内部,您对表达式使用运算符->
,而不能指向对象(因为它是null)并发出警告或错误。
我的结论是,除非有一个特别的段落说,只要它只取它的地址,它就可以合法地引用一个空指针,否则这个表达式是不合法的。
是的,->
的使用具有英语术语undefined的直接意义上的未定义行为。
只有当第一个表达式指向对象时才定义行为,否则不定义(=未定义)。一般来说,你不应该在术语undefined中搜索更多,这意味着:标准没有为你的代码提供意义。(有时它明确指出了它没有定义的情况,但这不会改变这个术语的一般含义。)
这是为了帮助编译器构建者处理问题而引入的一种松弛它们可能定义了一种行为,即使是针对您正在呈现的代码。特别是,对于编译器实现,将这样的代码或类似代码用于offsetof
宏是完全可以的。如果将此代码设置为违反约束,则会阻塞编译器实现的路径。
让我们从间接运算符*
:开始
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标准正确地保持了它的未定义,并提供了一个内部处理问题的宏偏移。
6.3.2.3指针
- 值为0的整数常量表达式,或转换为类型的此类表达式void*称为空指针常量。66)如果空指针常量被转换为指针类型,得到的指针,称为空指针,保证比较不相等指向指向任何对象或函数的指针
这意味着值为0的整数常量表达式将转换为空指针常量。
但是空指针常量的值没有定义为0。该值由实现定义。
7.19 的通用定义
- 宏是NULL它扩展到实现定义的空指针常量
这意味着C允许一种实现,其中空指针将具有一个所有位都设置好的值,并且对该值使用成员访问将导致溢出,这是未定义的行为
另一个问题是如何评估&(*E).m?括号是否适用,并且首先评估->
0。保持未定义可以解决这个问题。
首先,让我们确定我们需要一个指向对象的指针:
6.5.2.3结构和工会成员
4后缀表达式后面跟有
->
运算符,标识符表示成员结构或并集对象的。该值是对象的命名成员的值96)如果第一个表达式是指向一个限定类型,结果具有指定类型的so限定版本成员
不幸的是,没有空指针指向对象。
6.3.2.3指针
3值为0的整数常量表达式,或转换为类型的此类表达式
void *
被称为空指针常数。66)如果空指针常数被转换为指针类型,结果指针,称为空指针,保证比较不相等指向指向任何对象或函数的指针。
结果:未定义的行为
顺便说一下,还有一些其他需要考虑的问题:
6.3.2.3指针
4将一个空指针转换为另一个指针类型会产生该类型的空指针。任何两个空指针应比较相等
5整数可以转换为任何指针类型。除先前规定外结果是定义了实现,可能没有正确对齐,可能没有指向引用类型的实体,并且可能是陷阱表示。67)
6任何指针类型都可以转换为整数类型。除先前规定外结果就是定义了实现。如果结果不能用整数类型表示,行为是未定义的。结果不必在任何整数的值范围内类型67)用于将指针转换为整数或将整数转换为指针的映射函数旨在与执行环境的寻址结构一致。
因此,即使UB这次恰好是良性的,它仍然可能导致一些完全出乎意料的数字。
C标准中的任何内容都不会对系统如何使用该表达式提出任何要求。在编写标准时,在运行时引发以下事件序列是完全合理的:
- 代码将空指针加载到寻址单元
- 代码要求寻址单元添加字段
b
的偏移量 - 当试图将整数添加到空指针时,寻址单元会触发一个陷阱(为了健壮性,应该是一个运行时陷阱,即使许多系统没有捕捉到它)
- 系统在通过从未设置的陷阱向量调度后开始执行基本上是随机的代码,因为设置陷阱的代码会浪费内存,因为寻址陷阱不应该发生
"未定义行为"在当时意味着什么。
请注意,自C早期以来出现的大多数编译器都会将位于常量地址的对象成员的地址视为编译时常量,但我认为当时并没有强制要求这样的行为,也没有向标准中添加任何内容,该标准将要求在运行时计算不会定义的情况下定义涉及空指针的编译时地址计算。
编号。让我们分开来看:
&(((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++是另一种野兽,因为您可以重载&
运算符)。