取消引用等于 nullptr 的指针是否是标准的未定义行为?



一位博客作者提出了关于空指针撤销的讨论:

  • http://www.viva64.com/en/b/0306/

我在这里放了一些反论点:

  • http://bit.ly/1L98GL4

他引用标准的主要理由是:

;podh->line6'表达式在C语言中是未定义的行为当"podhd"是空指针时。

C99标准对"&amp操作员地址(6.5.3.2"地址和间接运算符"):

一元&运算符应为函数指示符,[]或一元*运算符的结果,或指定一个不是位字段且未用声明的对象寄存器存储类说明符。

表达式"podhd->line6"显然不是函数指示符,[]或*运算符的结果。它是一个左值表达式。然而当"podhd"指针为NULL时,表达式不会指定对象自6.3.2.3"指针"说:

如果将空指针常量转换为指针类型,则结果指针,称为空指针,保证可以进行比较不等于指向任何对象或函数的指针。

当"左值在求值时未指定对象时behavior未定义"(C99 6.3.2.1)值、数组和函数指示符"):

左值是具有对象类型或不完整类型的表达式除无效外;如果一个左值没有指定一个对象评估时,行为未定义。

因此,简而言之,相同的想法:

当对指针执行->时,它的求值结果为左值,其中不存在对象,因此行为是未定义的。

这个问题纯粹是基于语言的,我不是在问给定的系统是否允许用任何语言篡改地址0处的内容。

据我所见,在取消引用值等于nullptr的指针变量时没有任何限制,即使在某些情况下,由于上述段落的原因,指针与nullptr(或(void *) 0)常量的比较可能会在优化中消失,但这看起来是另一个问题,它并不能阻止取消引用值为nullptr的指针。请注意,我已经检查了其他SO问题和答案,我特别喜欢这组引号,以及上面的标准引号,并且我没有偶然发现从标准中明确推断出的东西,即如果指针ptrnullptr相比较,则取消引用它将是未定义的行为。

我得到的最多是,将常量(或其转换为任何指针类型)延迟就是UB,但没有说明与nullptr中的值位相等的变量。

我想清楚地将nullptr常量与一个指针变量分开,这个指针变量的值等于它。但解决这两种情况的答案是理想的。

我确实意识到,当与nullptr等进行比较时,优化可以快速进行,并且可以简单地基于此剥离代码。

如果结论是,如果ptr等于nullptr的值,那么它肯定是UB,那么另一个问题如下:

C和C++标准是否意味着地址空间中必须有一个特殊的值来表示空指针的值?

当您引用C时,取消引用空指针显然是标准引用(强调矿)中未定义的行为:

(C11,6.5.3.2p4)"如果为指针分配了无效值,一元*运算符的行为未定义.102)">

102):"一元*运算符取消引用指针的无效值包括空指针、与所指向对象类型不适当对齐的地址以及对象寿命结束后的地址。">

C99中完全相同的报价,C89/C90中类似。

C++

dcl.ref/5.

不应有对引用的引用,不应有引用数组,也不应有指向引用的指针。这个引用的声明应包含初始值设定项(8.5.3),除非声明包含显式外部说明符(7.1.1),是类定义中的类成员(9.2)声明,或者是声明参数或返回类型(8.3.5);参见3.1。引用应初始化为引用有效对象或作用[注意:特别是,null引用不能存在于定义良好的程序中,因为是唯一的方法创建这样的引用就是将其绑定到通过空指针间接获得的"对象",这会导致未定义的行为如9.6所述,引用不能直接绑定到位字段。--尾注]

这个注释很有趣,因为它明确表示取消引用空指针是未定义的。

我相信它在其他更相关的背景下说了这一点,但这已经足够好了。

关于NULL值可以在多大程度上被取消引用,我看到的答案是,由于C11 6.3.2.3p5和p6中定义的实现,它被故意以未指定的方式依赖于平台。这主要是为了支持用于开发平台引导代码的独立实现,正如OP在反驳链接中所指出的,但也有用于托管实现的应用程序

回复:
(C11,6.5.3.2p4)"如果指针被分配了无效值,则一元*运算符的行为未定义。102)">

102):"一元*运算符取消引用指针的无效值包括空指针、与所指向对象类型不适当对齐的地址以及对象寿命结束后的地址。">

事实上,这是因为脚注中的每种情况对于编译器所针对的特定平台都可能无效。如果存在缺陷,则其"无效值"应该用斜体表示,并由"定义的实现"限定。对于对齐情况,平台可以使用任何地址访问任何类型,因此没有对齐要求,尤其是在支持地址翻转的情况下;平台可以假设对象的生命周期只有在应用程序退出后才结束,通过malloc()为每个函数调用上的自动变量分配一个新的帧

对于空指针,在引导时,平台可能期望处理器使用的结构具有特定的物理地址,包括地址0,并在源代码中表示为对象指针,或者可能要求定义引导进程的函数使用0的基地址。如果标准不允许像'&podh->line6',如果平台要求podh的基地址为0,则需要汇编语言来访问该结构。类似地,软重启函数可能需要取消引用0值指针作为void函数调用。托管实现可以将0视为可执行映像的基,并在加载后将源代码中的NULL指针映射到该映像的标头,作为C虚拟机实例所需的逻辑地址0处的结构

标准调用指针的是进入虚拟机虚拟地址空间的更多句柄,其中对象句柄对允许它们执行哪些操作有更多要求。编译器如何针对特定处理器发出将这些句柄的要求考虑在内的代码,这一点尚不明确。毕竟,对一个处理器有效的东西可能对另一个处理器无效

对(void*)0的要求更多的是,编译器发出的代码可以保证源显式或通过引用NULL使用(void*0)0的表达式,存储的实际值将表明它不能通过任何映射代码指向任何有效的函数定义或对象。这不一定是0!类似地,对于(void*)0到(obj_type)和(func_type)的类型转换,只需要获得赋值,这些赋值的计算结果是编译器保证的地址,而不是用于对象或代码。与后者的区别在于,这些是未使用的,而不是无效的,因此能够以定义的方式取消引用

然后,测试指针相等性的代码将检查一个操作数是否是这些值中的一个,另一个是否是3个值中的另一个,而不仅仅是相同的位模式,因为这会给它们打分,RTTI是(null*)类型,不同于指向已定义实体的void、obj和func指针类型。该标准可以更明确——如果未命名,它是一个不同的类型,因为编译器只在内部使用它,但我认为"空指针"被斜体化是显而易见的。实际上,imo,在这些上下文中的"0"是编译器的额外关键字标记,因为它标识(null*)类型的额外要求,但由于这会使<标识符>

例如,当实现将虚拟机句柄的范围0到SIZE_MAX-4*sizeof(void*)定义为对代码和数据有效时,在发出的应用程序代码中,这个存储的值可以像0一样容易地为SIZE_MAX(对于(void*)0)。NULL宏甚至可以定义为
(void*)SIZE_MAX,编译器可以从上下文中判断出这与0具有相同的语义。强制转换代码负责指出它是指针<-->中的选定值指针强制转换,并提供适当的对象或函数指针。从指针投射<-->整数,隐式或显式,具有类似的检查和供应要求;尤其是在(u)intptr_t字段覆盖(type*)字段的并集中。可移植代码可以防止编译器使用显式*(ptr==NULL?(type*)0:ptr)表达式时不能正确执行此操作

最新更新