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



根据ISO c++,解引用空指针是未定义的行为。我的好奇是,为什么?为什么标准决定将其声明为未定义行为?这一决定背后的理由是什么?编译器的依赖吗?似乎没有,因为根据C99标准,据我所知,它是很好的定义。机器依赖性?什么好主意吗?

在大多数CPU体系结构中,为解引用NULL指针定义一致的行为需要编译器在每次解引用之前检查NULL指针。对于一门为速度而设计的语言来说,这是一个不可接受的负担。

它也只修复了一个更大问题的一小部分-有许多方法可以使无效指针超出NULL指针。

主要原因是在他们编写最初的C标准时,有许多实现允许它,但给出了相互矛盾的结果。

在PDP-11上,地址0总是包含值0,因此对空指针解引用也会得到值0。相当多使用过这些机器的人认为,既然这些机器是C语言编写/用于编程的原始机器,那么这应该被认为是C语言在所有机器上的规范行为(尽管它最初是偶然发生的)。

在其他一些机器上(想到Interdata,尽管我的内存很容易出错)地址0被正常使用,因此它可以包含其他值。还有一些硬件上的地址0实际上是一些内存映射的硬件,所以读/写它做了一些特殊的事情——根本不等同于读/写正常的内存。

各阵营无法就该怎么做达成一致,所以他们将其定义为未定义行为。

编辑:我想我应该补充一下,在编写c++标准的时候,它的未定义行为已经在C中很好地建立起来了,而且(显然)没有人认为有一个很好的理由在这一点上产生冲突,所以他们保持不变。

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

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

根据C99标准的6.5.3.2/4条款,在C语言中也没有定义对空指针的解引用。

这个来自@Johannes Schaub - litb的回答,提出了一个有趣的理论基础,看起来很有说服力。


仅仅对空指针解引用的形式问题是无法确定结果左值表达式的身份:当对该表达式求值时,对指针解引用产生的每个这样的表达式都必须明确地指向对象或函数。如果对空指针解引用,则没有这个左值标识的对象或函数。这是标准用来禁止null引用的参数。

另一个增加混淆的问题是typeid操作符的语义使这种痛苦的一部分得到了很好的定义。它说,如果给它一个左值,这是由于解引用一个空指针,结果是抛出一个bad_typeid异常。虽然,这是一个有限的领域,存在一个例外(没有双关语)的问题,找到一个身份。其他情况下也存在类似的未定义行为的异常(尽管没有那么微妙,并且在受影响的部分上有引用)。

委员会讨论了通过定义一种没有对象或函数标识的左值来全局解决这个问题:所谓的空左值。然而,这个概念仍然存在问题,他们决定不采用


注意:
将此标记为社区维基,因为答案&功劳应该归原来的海报。我只是把原始答案的相关部分粘贴在这里。

真正的问题是,你期望什么行为?

根据定义,空指针是一个表示对象不存在的奇异值。对指针解引用的结果是获得对所指向对象的引用。

那么你如何得到一个好的推荐信…

你没有。因此未定义行为

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

因此,为了避免给编译器带来任何可能的额外负担,他们没有定义该行为。

有时您需要一个无效的指针(也参见Windows上的MmBadPointer),来表示"nothing"。

如果一切都是有效的,那么这就不可能。所以他们使NULL无效,并且不允许对它进行解引用。

这是一个简单的测试& &;例子:

  1. 分配指针:

?什么值是在指针时,它被创建?
? 指针指向什么?
? 当我解引用这个点的当前状态时会发生什么?

  1. 表示链表结束。在链表中,一个节点指向另一个节点,最后一个节点除外。
    最后一个节点的指针值是多少?
    当您解引用最后一个节点的"next"字段时会发生什么?

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

在其他地方已经提出的参数是,在没有大量开销的情况下,为空指针引用定义良好的行为是不可能的,我认为这是正确的。这是因为AFAIU在这里的"定义良好的"也意味着"可移植的"。如果不对nullptr引用进行特殊处理,最终生成的指令可能只是尝试读取地址0,但这会在不同的处理器上产生不同的行为,因此无法良好定义。

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

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

特别是,当程序触发未定义行为时,编译器可以做几乎任何事情(例如,可能扔掉你的整个程序?)并且仍然被认为是正确的,这有点问题。在实践中,你会期望编译器只编译一个读地址为0的空指针解引用,但随着现代优化器变得更好,但也对未定义行为更敏感,我认为,它们有时会做一些最终更彻底地破坏程序的事情。例如:

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(其中包含复位向量),所以我很惊讶发生了这种情况。

所以,与其说是一个答案,不如说是对这个问题的一些额外的看法。

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

语言定义指出,对于每种指针类型,都有一个特殊值——"空指针",它与所有其他指针值区别开来,并且"保证与指向任何对象或函数的指针进行不相等比较"。也就是说,空指针绝对不指向任何地方;它不是任何对象或函数的地址

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

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

虽然从语言的角度来看,C/c++中对NULL指针的解引用确实会导致未定义的行为,但对于具有相应地址内存的目标,这种操作在编译器中定义得很好。在这种情况下,这种操作的结果包括简单地读取地址为0的内存。

而且,只要不绑定被引用的值,许多编译器将允许对NULL指针解引用。这样做是为了提供对不符合标准但广泛使用的代码的兼容性,比如

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

甚至还讨论过将此行为作为标准的一部分。

因为不能创建空引用。c++不允许这样做。因此,不能对空指针解引用。

未定义主要是因为没有逻辑方法来处理它。

实际上可以对空指针解引用。有人在这里做了:http://www.codeproject.com/KB/system/soviet_kernel_hack.aspx

最新更新