为什么解引用空指针是未定义行为?

29
根据ISO C++标准,解引用空指针是未定义行为。我很好奇,为什么会这样?为什么标准决定将其声明为未定义行为?这个决定背后的理由是什么?是编译器的依赖性吗?似乎不是,因为据我所知,根据C99标准,这是明确定义的。是机器的依赖性吗?有什么想法吗?

15
无论你信不信,x86架构中的地址0是可用的,因此有时你实际上需要对一个“空”指针进行取消引用。 - Earlz
7
如果不是未定义的话,那么行为应该是什么? - drb
10
例如,“鼻妖”(nasal demons)...... - Marcus Borkenhagen
7
@Rob:这并不正确。根据6.5.3.2/4,"如果一个无效的值被赋给指针,一元操作符的行为是未定义的。",其中一条脚注包括"在通过一元操作符对指针解引用时,无效的值包括空指针"。 - Mike Seymour
6
空指针并不一定指向地址0。 - user802003
显示剩余7条评论
13个回答

46

定义解引用空指针的一致行为需要编译器在大多数CPU体系结构上在每次解引用之前检查空指针。对于一个旨在追求速度的语言来说,这是一个无法承受的负担。

此外,这只修复了更大问题的一小部分 - 除了空指针之外,还有许多其他方式可以导致无效指针。


1
你假设NULL必须是特殊的,而据我所知,OP的问题更多的是,为什么它应该是特殊的? - user541686
2
@Mehrdad:它是如何假定NULL是特殊的?它在解引用方面与未初始化的指针或不再指向现有对象的指针一样不特殊。 - James McNellis
1
@James:嗯,因为编译器没有必要检查NULL指针(或其他无效指针),如果它不是特殊的指针,它会像任何其他指针一样解引用它。只有当它是特殊的指针时,编译器才需要检查。 - user541686
1
@Mehrdad,这就是我第二段的重点 - NULL指针不是特殊的,也不应该是。 - Mark Ransom
1
@Mehrdad "为什么它应该是特殊的?" 因为它确实是一个特殊值。没有其他有效的指针值,既不是对象的地址,也不是某个数组的超出末尾位置。另一方面,解引用空指针不是一个特殊情况。 - curiousguy

23
主要原因是在编写最初的C标准时,已经有一些实现允许空指针解引用,但给出了冲突的结果。
在PDP-11上,地址0始终包含值0,因此对空指针进行解引用也会返回值0。许多使用这些机器的人认为,由于它们是最初编写C的机器/用于编程的机器,因此这应该被视为所有机器上C的规范行为(即使最初发生得非常偶然)。
在其他一些机器上(例如Interdata),地址0被用于正常用途,因此它可能包含其他值。还有一些硬件,其中地址0实际上是一些内存映射硬件,因此读取/写入它会执行特殊操作--根本不等同于读取/写入普通内存。
这些阵营无法就应该发生什么达成一致,因此将其定义为未定义行为。
编辑:我想我应该补充说,在编写C++标准时,它的未定义行为已经在C中得到了很好的确认,而且(显然)没有人认为有一个很好的理由在这一点上创建冲突,所以他们保持了相同的做法。

3
值得注意的是,在C89发布之前,它没有强制要求任何行为,但许多C实现定义了许多事物的行为。如果一些C编译器为某些操作定义了行为,而另一些没有,则将行为未定义仅保留了现状。直到最近,标准未定义某些内容被解释为表明即使针对在C标准之前定义行为的平台的代码,也不应使用标准中不存在的任何功能。 - supercat

11

要给出明确的行为,唯一的方法是在每个指针解引用和每个指针算术操作中添加一个运行时检查。在某些情况下,这种开销是无法接受的,并且会使C++不适用于通常使用的高性能应用程序。

C ++允许您创建自己的智能指针类型(或使用库提供的类型),在安全性比性能更重要的情况下可以包含此类检查。

在C语言中,按照C99标准第6.5.3.2 / 4条款规定,对空指针进行解引用也是未定义的。


4
不是真的。定义的行为可以简单地是“只要不访问值,就可以解引用空指针。如果访问了结果lvalue的值,行为是未定义的”。这不需要任何检查。 - Johannes Schaub - litb
1
@Johannes:是的,你说得对;我把“dereferencing”解释为“访问已解引用的值”,这并不严格准确。 - Mike Seymour
@Johannes Schaub - litb:我在这里发布了您另一个答案的摘录作为答案。如果您想将其作为自己的答案添加,请随意这样做。如果是这样,我会删除标记为社区维基的那个答案。 - Alok Save
@Als 我不会发重复的帖子,但我已经给你点赞了。感谢你传播这些信息。玩得开心 :) - Johannes Schaub - litb
@Johannes Schaub - litb:好的 :) 无论如何,我在发布时标记了那个社区Wiki! - Alok Save
@Johannes,我能想到的唯一解除指针引用但不访问值的方法是将其赋值给一个引用。在调试模式下,编译器捕获创建空指针引用的尝试可能有一定价值,因此保留未定义的行为具有某些好处-编译器可以超越标准。 - Mark Ransom

8

这个 来自 @Johannes Schaub - litb 的回答提出了一个有趣的理由,看起来相当令人信服。


仅取消引用空指针的正式问题在于确定结果lvalue表达式的身份是不可能的:当评估该表达式时,从取消引用指针得到的每个这样的表达式必须明确地引用对象或函数。如果您取消引用空指针,则没有此lvalue标识的对象或函数。这是标准用来禁止空引用的论据。
增加混乱的另一个问题是typeid运算符的语义使部分痛苦得到了定义。它表示,如果给出从取消引用空指针而导致的lvalue,结果是抛出一个bad_typeid异常。尽管如此,在存在与上述找到身份问题的例外情况下,还存在其他类似的未定义行为例外情况(尽管远不那么微妙,并且对受影响章节进行了参考)。
委员会讨论通过定义一种没有对象或函数身份的lvalue,即所谓的空lvalue,全球解决此问题。然而,这个概念仍然存在问题,他们决定不采用它

注意:
将此标记为社区wiki,因为答案和信用应归原始发布者所有。我只是在此粘贴原始答案的相关部分。


在我看来,通过更好地定义“C对象”和地址的含义,认识到一个N字节的C对象有N+1个相关地址,其中前N个标识一个字节,最后N个跟随一个字节,许多问题可以得到解决。这个定义可以推广到零字节对象,它们有一个单一的地址,既不标识也不跟随任何存储字节,并且可能与任何其他零字节对象的地址匹配或不匹配。 - supercat

5

实际问题是,你期望得到什么行为?

空指针是一种特殊的值,代表没有对象存在。解除引用指针的结果是获得指向所指对象的引用。

那么,如何从指向虚空的指针中获得好的引用呢?

你不能。因此会出现未定义的行为。


5
抛出异常?引发信号?调用 abort()?可以定义很多明智的方法,问题是为什么要让它未定义? - Mike Seymour
@Mike Seymour:看来我们对问题的解释不一样 :) 预先检查(dereference)会很耗费时间。另一方面,在Unix上,操作系统已经在执行检查,因此可以理论上连接一个信号处理程序并执行您引用的其中一种操作...但我认为这并非在所有地方都可行。特别是在没有操作系统的嵌入式平台上。指定行为将瘫痪这些平台。 - Matthieu M.
@MikeSeymour 在没有 throw 的情况下抛出异常是不明智的做法。(是的,你可以对Java做出结论。) - curiousguy

1
在其他地方已经提出过这样的论点:如果没有大量的开销,很难为空指针引用定义良好的行为,我认为这是正确的。这是因为据我所知,“定义良好”在这里也意味着“可移植”。如果您不会特别处理nullptr引用,那么您最终将生成尝试读取地址0的指令,但这在不同的处理器上会产生不同的行为,因此这将不是定义良好的。

所以,我想这就是为什么将nullptr(可能还有其他无效指针)取消引用标记为未定义的原因。

我确实想知道为什么这是未定义的,而不是未指定或实现定义的,这些与未定义的行为有所不同,但需要更多的一致性。

特别是,当程序触发未定义的行为时,编译器可以做任何事情(例如,也许丢弃整个程序?),仍然被认为是正确的,这有点棘手。在实践中,您会期望编译器只是将null指针取消引用编译为地址零的读取,但随着现代优化器变得更好,但也更加敏感于未定义的行为,我认为它们有时会做一些使程序更彻底地崩溃的事情。例如,考虑以下内容:

matthijs@grubby:~$ cat test.c
unsigned foo () {
        unsigned *foo = 0;
        return *foo;
}

matthijs@grubby:~$ arm-none-eabi-gcc  -c test.c -Os && objdump -d test.o 

test.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <foo>:
   0:   e3a03000        mov     r3, #0
   4:   e5933000        ldr     r3, [r3]
   8:   e7f000f0        udf     #0

这个程序只是取消引用和访问空指针,导致生成“未定义指令”(在运行时停止程序)。
当这是一个意外的空指针取消引用时,这可能是可以接受的,但在这种情况下,我实际上正在编写一个需要读取地址0(其中包含复位向量)的引导加载程序,所以我感到非常惊讶这种情况会发生。
因此,这不是一个答案,而是一些额外的观点。

1

我怀疑这是因为如果行为被定义清楚,编译器必须在指针被解引用的任何地方插入代码。如果它是实现定义的,则可能的一种行为仍然可能是硬崩溃。如果未指定,则某些系统的编译器可能会有额外的不当负担,或者它们可能会生成导致硬崩溃的代码。

因此,为了避免对编译器造成任何可能的额外负担,他们将该行为定义为未定义状态。


1
有时候你需要一个无效指针(在Windows上也可以看到MmBadPointer),来表示“什么都没有”。
如果一切都是有效的,那就不可能了。所以他们把NULL设为无效,并禁止你对其进行解引用。

1

这里是一个简单的测试和示例:

  1. 分配一个指针:

    int * pointer;

? 当指针被创建时,它的值是什么?
? 指针指向什么?
? 当我在其当前状态下取消引用此指针时会发生什么?

  1. 标记链表的末尾。 在链表中,一个节点指向另一个节点,除了最后一个节点。
    最后一个节点的指针的值是多少?
    当您取消引用最后一个节点的“next”字段时会发生什么?

需要有一个值来指示指针没有指向任何东西或处于无效状态。这就是NULL指针概念发挥作用的地方。链表可以使用NULL指针来指示列表的末尾。


0

根据原始C标准,NULL可以是任何值 - 不一定是零

语言定义规定,对于每种指针类型,都有一个特殊值 - “空指针” - 它可与所有其他指针值区分开来,并且“保证与指向任何对象或函数的指针不相等”。也就是说,空指针明确地指向无处; 它不是任何对象或函数的地址。

每种指针类型都有一个空指针,并且不同类型的空指针的内部值可能是不同的。

(来源http://c-faq.com/null/null1.html)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接