【问题标题】:Why is address zero used for the null pointer?为什么地址零用于空指针?
【发布时间】:2011-02-15 03:13:37
【问题描述】:

在 C(或 C++)中,如果指针的值为零,则它们是特殊的:建议我在释放内存后将指针设置为零,因为这意味着再次释放指针并不危险;当我调用 malloc 时,如果它无法获取内存,它会返回一个值为 0 的指针;我一直使用if (p != 0) 来确保传递的指针是有效的,等等。

但是既然内存寻址从 0 开始,那么 0 不就和其他地址一样是有效地址吗?如果是这种情况,如何使用 0 来处理空指针?为什么不是负数 null 而不是?


编辑:

一堆很好的答案。我将根据我自己的想法将其所表达的答案中所说的内容进行总结,并希望如果我有误解,社区会纠正我。

  • 就像编程中的其他一切一样,它是一种抽象。只是一个常数,与地址 0 没有真正的关系。C++0x 通过添加关键字nullptr 来强调这一点。

  • 它甚至不是地址抽象,它是 C 标准指定的常量,编译器可以将其转换为其他数字,只要它确保它永远不会等于“真实”地址,并且等于其他 null如果 0 不是用于平台的最佳值,则为指针。

  • 如果它不是一个抽象,就像早期的情况一样,地址 0 被系统使用,程序员不受限制。

  • 我承认,我的负数建议有点疯狂。使用有符号整数表示地址有点浪费,如果这意味着除了空指针(-1 或其他)之外,值空间在构成有效地址的正整数和浪费的负数之间平均分配。

  • 1234563或两位整数,即 [-2, 1]。但你可以选择 0 为空,1 为内存中唯一可访问的字节。)

我心中仍有一些未解决的问题。堆栈溢出问题 Pointer to a specific fixed address 告诉我,即使空指针的 0 是一种抽象,其他指针值也不一定。这导致我发布另一个堆栈溢出问题,Could I ever want to access the address zero?

【问题讨论】:

  • 您可以轻松地将 if (p != 0) 更改为 if (p),这是 C 和 C++ 中的常见习语,但如果您使用 Java,则必须改掉这个习惯。跨度>
  • 删除两次意味着您的代码错误。我建议不要在之后将指针设置为 null,这样您就可以崩溃并修复问题,而不是抑制它。在任何情况下,您都会错误地假设地址是某个整数。这不一定是真的,0 只是 表示 一些特定于实现的实际指针值。从概念上讲,“否定地址”没有意义。
  • @GMan:将指针设置为会导致崩溃的地址可能是个好主意,例如0xDEADBEEF
  • 永不消亡的问题!
  • @Noah:点设置为null -> 隐藏编程错误,不要设置为null -> 查找编程错误。我不了解你,但我希望我的代码正确。

标签: c++ c memory pointers


【解决方案1】:

2 分:

  • 只有源代码中的常量值 0 是空指针 - 编译器实现可以在运行代码中使用它想要或需要的任何值。一些平台有一个特殊的指针值,它是“无效的”,实现可能会用作空指针。 C FAQ 有一个问题"Seriously, have any actual machines really used nonzero null pointers, or different representations for pointers to different types?",它指出了几个平台使用 0 作为 C 源代码中的空指针这一属性,而在运行时以不同的方式表示。 C++ 标准有一条注释明确指出,转换“具有零值的整型常量表达式总是产生空指针,但转换恰好具有零值的其他表达式不需要产生空指针”。

  • 平台可以使用负值作为地址 - C 标准只需选择一些东西来指示空指针,然后选择零。老实说,我不确定是否考虑了其他哨兵值。

空指针的唯一要求是:

  • 保证比较不等于指向实际对象的指针
  • 任何两个空指针都会比较相等(C++ 对此进行了改进,只需要为指向相同类型的指针保留)

【讨论】:

  • +1 我怀疑选择 0 仅仅是出于历史原因。 (在大多数情况下,0 是一个起始地址和无效地址。)当然,通常这种假设并不总是正确的,但 0 工作得很好。
  • 空间也可能是一个促成因素。在最初开发 C 的时候,内存的成本比现在高得多。可以使用 XOR 指令方便地计算数字零,或者无需加载立即数。根据架构,这可能会节省空间。
  • @GMan - 你是对的。在早期的 CPU 上,内存地址 0 是特殊的,并且具有硬件保护以防止运行软件的访问(在某些情况下,它是复位向量的开始,修改它可能会阻止 CPU 复位或启动)。程序员在他们的软件中使用这种硬件保护作为错误检测的一种形式,让 CPU 的地址解码逻辑检查未初始化或无效的指针,而不必花费 CPU 指令来执行它。即使地址 0 的目的可能已经改变,该约定仍然保留至今。
  • Minix 16 位编译器使用 0xFFFF 表示 NULL。
  • 在许多嵌入式系统中,0 是有效地址。值 -1(所有位为 1)也是有效地址。当数据从地址 0 开始时,ROM 的校验和很难计算。:-(
【解决方案2】:

从历史上看,从 0 开始的地址空间总是 ROM,用于某些操作系统或低级中断处理例程,现在,由于一切都是虚拟的(包括地址空间),操作系统可以将任何分配映射到任何地址,所以它可以明确地不在地址 0 分配任何东西。

【讨论】:

  • 差不多就是这样。按照历史惯例,第一个地址用于中断处理程序,因此不能用于正常程序。此外,0 是“空”,可以解释为无值/无指针。
  • 地址 0 始终是无效地址并不正确,但我想这就是标准允许使用任何常量的原因。我仍然不确定如何保证 NULL 不是有效地址,除非用于存储指针值的位数大于 REM 地址总线上的位数。
【解决方案3】:

IIRC,“空指针”值不能保证为零。编译器将 0 转换为适用于系统的任何“空”值(实际上可能始终为零,但不一定)。每当您将指针与零进行比较时,都会应用相同的转换。因为您只能将指针相互比较并与这个特殊值 0 比较,所以它使程序员无法了解有关系统内存表示的任何信息。至于他们为什么选择 0 而不是 42 之类的,我猜这是因为大多数程序员从 0 开始计数 :) (另外,在大多数系统上,0 是第一个内存地址,他们希望它方便,因为在像我描述的那样练习翻译很少真正发生;语言只允许它们。

【讨论】:

  • @Justin:你误会了。常量 0 总是 是空指针。 @meador 的意思是空指针(由常量 0 表示)可能不对应于地址零。在某些平台上,创建空指针 (int* p = 0) 可能会创建一个包含值 0xdeadbeef 或它喜欢的任何其他值的指针。 0 是空指针,但空指针不一定是指向地址零的指针。 :)
  • NULL 指针是一个保留值,取决于编译器,它可以是任何位模式。 NULL 指针并不代表它指向地址 0。
  • 但是@Jalf,常量 0 不是总是空指针。当我们希望编译器为我们填充平台的 actual 空指针时,就是我们写的。实际上,空指针通常确实对应于地址零,我将 Joel 的问题解释为询问为什么会这样。毕竟,在那个地址应该有一个有效的内存字节,那么为什么不使用一个不存在的字节的不存在的地址而不是从播放中删除一个有效的字节呢? (我在写我想象中乔尔在想的东西,而不是我在问自己的问题。)
  • @Rob:有点。我知道你的意思,你是对的,但我也是。:) 常量整数 0 表示源代码级别的空指针。将空指针与 0 进行比较得出 true。将 0 分配给指针会将指针设置为空。 0 空指针。但是空指针的实际内存表示可能与零位模式不同。 (无论如何,我的评论是对 @Justin 现在已删除的评论的回应,而不是 @Joel 的问题。:)
  • @jalf @Rob 我认为您需要一些术语来澄清。 :) 从 §4.10/1 开始:“空指针常量 是整数类型的整数常量表达式右值,其计算结果为零。空指针常量可以转换为指针类型;结果是该类型的空指针值,可与指向对象的指针或指向函数类型的指针的所有其他值区分开来。"
【解决方案4】:

您一定误解了指针上下文中常量零的含义。

无论是在 C 中还是在 C++ 中,指针都不能“具有零值”。指针不是算术对象。它们不能具有像“零”或“负”或任何类似性质的数值。所以你关于“指针......值为零”的说法根本没有意义。

在 C 和 C++ 中,指针可以具有保留的 空指针值。空指针值的实际表示与任何“零”无关。它绝对可以是任何适合给定平台的东西。确实,在大多数平台上,空指针值在物理上由实际的零地址值表示。但是,如果在某些平台上,地址 0 实际用于某种目的(即您可能需要在地址 0 处创建对象),则该平台上的空指针值很可能会有所不同。例如,它可以物理表示为0xFFFFFFFF 地址值或0xBAADBAAD 地址值。

尽管如此,无论在给定平台上如何表示空指针值,在您的代码中,您仍将继续通过常量0 指定空指针。为了将空指针值分配给给定的指针,您将继续使用像p = 0 这样的表达式。编译器有责任实现您想要的并将其转换为正确的空指针值表示形式,即将其转换为将0xFFFFFFFF 的地址值放入指针p 的代码,例如。

简而言之,您在源代码中使用0 来生成空指针值并不意味着空指针值以某种方式与地址0 相关联。您在源代码中使用的0 只是“语法糖”,与空指针值“指向”的实际物理地址完全无关。

【讨论】:

  • 指针不是算术对象 指针算术在 C 和 C++ 中定义得很好。部分要求是两个指针都指向同一个组合。空指针不指向任何复合,因此在指针算术表达式中使用它是非法的。例如,不能保证(p1 - nullptr) - (p2 - nullptr) == (p1 - p2).
  • @Ben Voigt:语言规范定义了算术类型的概念。我要说的是指针类型不属于算术类型的范畴。 指针算术是一个完全不同且完全不相关的故事,只是语言上的巧合。
  • 阅读算术对象的人应该如何知道它的意思是“算术类型”而不是“算术运算符”(其中一些是可用的在指针上)或“在指针算术的意义上”。就语言巧合而言,算术对象指针算术的共同字母多于算术类型。同时,该标准确实谈到了指针值。原贴可能是指指针的整数表示而不是指针值NULL 明确不需要用 0 表示。
  • 好吧,例如,C/C++ 术语中的术语 scalar objects 只是 标量类型的对象 的简写(就像 POD 对象 = POD 类型的对象)。我以完全相同的方式使用术语算术对象,意思是算术类型的对象。我希望“某人”能这样理解。不知道的人总是可以要求澄清。
  • 我在一个系统上工作,其中(就硬件而言)null 是 0xffffffff 而 0 是一个完全有效的地址
【解决方案5】:

但是既然内存寻址从 0 开始,那么 0 不是和其他地址一样是有效地址吗?

在某些/许多/所有操作系统上,内存地址 0 在某些方面是特殊的。例如,它通常映射到无效/不存在的内存,如果您尝试访问它会导致异常。

为什么不是负数 null 呢?

我认为指针值通常被视为无符号数字:否则例如 32 位指针只能寻址 2 GB 的内存,而不是 4 GB。

【讨论】:

  • 我在一个地址零是有效地址且没有内存保护的设备上进行了编码。空指针也全为零;如果您不小心写入了一个空指针,那么您就破坏了位于零地址的操作系统设置;欢闹通常不会随之而来。
  • 是:在非保护模式的 x86 CPU 上,例如,地址 0 是interrupt vector table
  • @ChrisW:在非保护模式 x86 上,地址零尤其是被零除的中断向量,某些程序可能有完全正当的理由来编写它。
  • 即使在可用存储从物理地址零开始的平台上,C 实现也可以轻松地使用地址零来保存地址从未被占用的对象,或者简单地保留内存的第一个字没用过。在大多数平台上,与零比较相比,与其他任何东西比较相比,保存指令,因此即使浪费存储的第一个字也比使用非零地址为空值更便宜。请注意,没有要求 C 标准未涵盖的事物的地址(例如 I/O 端口或中断向量)比较不等于 null,也不要求...
  • ...系统进程空指针的访问方式与其他任何不同,因此即使在访问物理位置​​零的系统上,全位零通常也是“空”的良好地址有用且有意义。
【解决方案6】:

我的猜测是选择了魔法值 0 来定义一个无效指针,因为它可以用更少的指令进行测试。一些机器语言在加载寄存器时会根据数据自动设置零和符号标志,这样您就可以通过简单的加载来测试空指针,然后分支指令而无需执行单独的比较指令。

(不过,大多数 ISA 只在 ALU 指令上设置标志,而不是加载。通常你不会通过计算生成指针,除非在编译器中解析 C source。但至少你没有'不需要一个任意的指针宽度常量来比较。)

在 Commodore Pet、Vic20 和 C64(这是我工作的第一台机器)上,RAM 从位置 0 开始,因此如果您真的想使用空指针进行读写是完全有效的。

【讨论】:

    【解决方案7】:

    我认为这只是一个约定。必须有一些值来标记无效指针。

    您只是丢失了一个字节的地址空间,这应该很少有问题。

    没有负指针。指针总是无符号的。此外,如果它们可能是负面的,您的约定将意味着您失去一半的地址空间。

    【讨论】:

    • 注意:您实际上并没有丢失地址空间;您可以通过执行以下操作获得指向地址 0 的指针:char *p = (char *)1; --p;。由于标准未定义空指针的行为,因此该系统可以让p 实际读写地址 0,递增以提供地址 1 等。
    • @MattMcNabb:地址零是有效硬件地址的实现可以完全合法地定义char x = ((char*)0); 读取地址零并将该值存储到x 中的行为。这样的代码会在任何未定义其行为的实现上产生未定义的行为,但标准说某事是未定义的行为这一事实绝不禁止实现提供自己的规范来说明它将做什么。
    • @supercat ITYM *(char *)0。确实如此,但在我的建议中,实现不需要定义*(char *)0 或任何其他空指针操作的行为。
    • @MattMcNabb:char *p = (char*)1; --p; 的行为只有在将指向对象第一个字节以外的指针转换为 @987654328 之后执行该序列时才会由标准定义@,并且该转换的结果恰好产生值 1,在这种特殊情况下,--p 的结果将产生一个指针,该指针指向该字节之前的字节,该字节的指针值在转换为 intptr_t 时产生了 @ 987654331@.
    【解决方案8】:

    虽然 C 使用 0 来表示空指针,但请记住,指针本身的值可能不是零。然而,大多数程序员只会使用空指针实际上为 0 的系统。

    但为什么是零?嗯,这是每个系统共享的一个地址。并且通常低地址是为操作系统目的而保留的,因此该值可以很好地用于应用程序的禁区。将整数值意外分配给指针很可能最终为零。

    【讨论】:

    • 这一切背后更可能的原因是:分发预初始化为零的内存很便宜,并且方便让该内存中的值表示有意义的东西,如整数 0、浮点 0.0和空指针。 C 中初始化为零/空的静态数据不必占用可执行文件中的任何空间,并且在加载时映射到零填充块。零也可能在机器语言中得到特殊处理:简单的零比较,如“如果等于零则分支”等。MIPS 甚至有一个虚拟寄存器,它只是一个零常数。
    【解决方案9】:

    过去,应用程序的低内存被系统资源占用。正是在那些日子里,零成为了默认的空值。

    虽然这对于现代系统来说不一定是正确的,但将指针值设置为除了分配给您的内存分配之外的任何内容仍然是一个坏主意。

    【讨论】:

      【解决方案10】:

      关于删除指针后不将指针设置为null以便将来删除“暴露错误”的论点......

      如果您真的非常担心这一点,那么一种更好的方法,即保证有效的方法是利用 assert():

      
      ...
      assert(ptr && "You're deleting this pointer twice, look for a bug?");
      delete ptr;
      ptr = 0;
      ...
      

      这需要一些额外的输入,并在调试构建期间进行一次额外的检查,但它肯定会给你想要的东西:注意 ptr 何时被“两次”删除。评论讨论中给出的替代方案,即不将指针设置为 null 以使您崩溃,根本不能保证成功。更糟糕的是,与上述不同的是,如果这些“错误”之一进入货架,它可能会导致用户崩溃(或更糟!)。最后,这个版本让您可以继续运行程序,看看实际发生了什么。

      我意识到这并不能回答所提出的问题,但我担心阅读 cmets 的人可能会得出这样的结论,即如果有可能将指针设为免费,则不将指针设置为 0 被认为是“良好做法” () 或删除两次。在那些少数情况下,使用未定义行为作为调试工具绝不是一个好习惯。没有人曾经不得不寻找最终由删除无效指针引起的错误会提出这个建议。这类错误需要数小时才能找到,并且几乎总是以完全出乎意料的方式影响程序,很难甚至不可能追溯到原始问题。

      【讨论】:

        【解决方案11】:

        许多操作系统使用全位零表示空指针的一个重要原因是,这意味着memset(struct_with_pointers, 0, sizeof struct_with_pointers) 和类似的东西会将struct_with_pointers 内的所有指针设置为空指针。 C 标准不保证这一点,但很多很多程序都假设它。

        【讨论】:

          【解决方案12】:

          在一台旧的 DEC 机器(我认为是 PDP-8)中,C 运行时会对内存的第一页进行内存保护,因此任何访问该块中内存的尝试都会引发异常。

          【讨论】:

          • PDP-8 没有 C 编译器。 PDP-11 没有内存保护,而 VAX 因默默地返回 0 到 NULL 指针取消引用而臭名昭著。我不确定这是指哪台机器。
          【解决方案13】:

          sentinel 值的选择是任意的,这实际上正在由下一版本的 C++(非正式地称为“C++0x”,将来最有可能被称为 ISO C++ 2011)解决,其中引入关键字nullptr 来表示空值指针。在 C++ 中,值 0 可以用作任何 POD 和任何具有默认构造函数的对象的初始化表达式,它具有在指针初始化的情况下分配标记值的特殊含义。至于为什么不选择负值,对于某个值 N,地址通常在 0 到 2N-1 之间。换句话说,地址通常被视为无符号值。如果将最大值用作标记​​值,那么它必须因系统而异,具体取决于内存的大小,而 0 始终是可表示的地址。它也用于历史原因,因为内存地址 0 通常在程序中不可用,现在大多数操作系统都将内核的一部分加载到内存的较低页面中,并且这些页面通常受到保护,如果被程序(保存内核)触摸(取消引用)会导致错误。

          【讨论】:

            【解决方案14】:

            它必须有一些价值。显然,您不想踩到用户可能合法使用的值。我推测由于 C 运行时为零初始化数据提供了 BSS 段,因此将零解释为未初始化的指针值在一定程度上是有意义的。

            【讨论】:

              【解决方案15】:

              操作系统很少允许您写入地址 0。通常会将特定于操作系统的内容保存在低内存中;即,IDT、页表等(这些表必须在 RAM 中,并且将它们粘贴在底部比尝试确定 RAM 的顶部在哪里更容易。)并且没有任何操作系统的正常思维会让你随意编辑系统表。

              K&R 在制作 C 时可能没有想到这一点,但它(以及 0==null 很容易记住这一事实)使 0 成为一种流行的选择。

              【讨论】:

              • 在保护模式下不是这样,事实上,在某些 Linux 配置上,您可以写入虚拟地址 0。
              【解决方案16】:

              0 是一个特殊值,在特定表达式中具有各种含义。在指针的情况下,正如已经多次指出的那样,使用它可能是因为当时它是说“在此处插入默认哨兵值”的最方便的方式。作为常量表达式,它与指针表达式上下文中的按位零(即所有位设置为零)的含义不同。在 C++ 中,有几种类型没有 NULL 的按位零表示,例如指针成员和指向成员函数的指针。

              谢天谢地,C++0x 为“表达式表示已知的无效指针,对于整数表达式也不映射到按位零”有一个新关键字:nullptr。虽然有一些系统可以使用 C++ 进行定位,它们允许取消引用地址 0 而不会引起麻烦,所以程序员要小心。

              【讨论】:

                【解决方案17】:

                这个帖子里已经有很多好的答案了;将值 0 用于空指针可能有很多不同的原因,但我要再添加两个:

                • 在 C++ 中,零初始化指针会将其设置为 null。
                • 在许多处理器上,将值设置为 0 或测试它是否等于 0 比测试任何其他常量更有效。

                【讨论】:

                  【解决方案18】:

                  这取决于 C/C++ 中指针的实现。 NULL 在赋值给指针时是等价的,没有具体的原因。

                  【讨论】:

                    【解决方案19】:

                    这有历史原因,但也有优化原因。

                    操作系统通常会为进程提供初始化为 0 的内存页面。如果程序想要将该内存页面的一部分解释为指针,那么它就是 0,因此程序很容易确定该指针未初始化。 (这在应用于未初始化的 Flash 页面时效果不佳)

                    另一个原因是,在许多处理器上,测试一个值与 0 的等价性非常非常容易。有时它是一种免费的比较,不需要任何额外的指令,通常可以在不需要提供零值的情况下完成。另一个寄存器或作为指令流中的文字进行比较。

                    对于大多数处理器来说,廉价的比较是有符号的小于 0 和等于 0。(有符号大于 0 和不等于 0 都暗示了)

                    由于需要将所有可能值中的 1 个值保留为错误或未初始化,那么您不妨将其设为与错误值进行等价测试的成本最低的那个。对于以 '\0' 结尾的字符串也是如此。

                    如果您尝试为此目的使用大于或小于 0 的值,那么您最终会将您的地址范围减半。

                    【讨论】:

                      【解决方案20】:

                      使用常量0 代替NULL 因为C 是由一些数万亿年前的穴居人创造的,NULLNILZIPNADDA 都会更有意义比0.

                      但是由于内存寻址开始于 0, 不 0 只是作为一个有效地址 还有其他的吗?

                      确实如此。尽管许多操作系统不允许您在地址零映射任何内容,即使在虚拟地址空间中也是如此(人们意识到 C 是一种不安全的语言,并且反映出空指针取消引用错误非常普遍,因此决定通过禁止“修复”它们映射到第 0 页的用户空间代码;因此,如果您调用回调但回调指针为 NULL,则最终不会执行一些任意代码。

                      0如何用于处理null 如果是这种情况,请指点?

                      因为与指针相比使用的0 将被替换为一些实现特定值,这是 malloc 失败时 malloc 的返回值。

                      为什么负数不是 null 代替?

                      这会更令人困惑。

                      【讨论】:

                      • 您关于“穴居人”等的观点可能是其根源,尽管我认为具体情况有所不同。演变成 C 的最早形式被设计为在一个特定架构上运行,其中 int 不仅与指针大小相同——在许多情况下,int 和指针可以互换使用。如果例程需要一个指针,而一个指针传入一个整数 57,则该例程将使用与数字 57 具有相同位模式的地址。在那些特定的机器上,表示空指针的位模式是 0,因此传递一个 int 0将传递一个空指针。
                      • 从那时起,C 语言已经发展到可以用于为具有不同数字和指针表示的大量其他机器编写程序。虽然非零数字常量很少用作指针,但常量数字零被广泛用于表示空指针。禁止这种用法会破坏现有代码,因此编译器需要将数字零转换为实现用来表示空指针的任何内容。
                      猜你喜欢
                      • 2020-08-29
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2016-02-08
                      • 1970-01-01
                      相关资源
                      最近更新 更多