为什么地址零用于空指针?

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

但是既然内存寻址从0开始,那么0不就是一个有效的地址吗?如果是这种情况,0如何用于处理空指针?为什么负数不是零呢?


编辑:

一堆好答案。我将总结一下在回答中所说的话,因为我自己的头脑会解释它,并希望社区会纠正我,如果我误解了。

  • 和编程中的其他东西一样,它也是一个抽象概念。只是一个常数,与地址0没有实际关系。C + + 0x 通过添加关键字 nullptr来强调这一点。

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

  • 如果它不是一个抽象,在早期的情况下,地址0是系统使用的,对程序员来说是禁止的。

  • 我承认,我的负数建议是一个有点疯狂的头脑风暴。对地址使用有符号整数有点浪费,如果它意味着除了空指针(-1或其他)之外,值空间被均匀地分割在正整数之间,正整数是有效的地址,负数是浪费的。

  • 如果任何数字总是由数据类型表示,那么它就是0。(大概1也是。我想到的是一位整数,如果是无符号的,就是0或1,如果是有符号的,就是有符号的,或者是两位整数,就是[-2,1]。但是你可以选择0为 null,1为内存中唯一可访问的字节。)

我心里还有一些事情没有解决。Stack Overflow 问题 指向特定固定地址的指针告诉我,即使0 for null 指针是一个抽象,其他指针值也不一定是。这导致我发布另一个堆栈溢出问题,我曾经想要访问地址0吗

42816 次浏览

IIRC,“空指针”值不能保证为零。编译器将0转换为适合系统的任何“ null”值(实际上可能总是0,但不一定)。无论何时将指针与零进行比较,都应用相同的转换。因为只能比较指针之间以及指针与这个特殊值 -0之间的比较,所以它使程序员无法了解系统的内存表示。至于为什么他们选择0而不是42或者类似的数字,我猜是因为大多数程序员从0开始计数:)(另外,在大多数系统中,0是第一个内存地址,他们希望它方便,因为在实践中,像我描述的翻译很少实际发生; 语言只是允许他们)。

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

2分:

  • 只有源代码中的常量值0是空指针——编译器实现可以在运行代码中使用它想要或需要的任何值。一些平台有一个特殊的指针值,它是“无效的”,实现可能使用它作为空指针。C FAQ 有一个问题,“说真的,有没有任何实际的机器真正使用非零空指针,或者不同类型的指针的不同表示形式?”,它指出几个平台使用这个属性0作为 C 源代码中的空指针,但在运行时表示方式不同。C + + 标准有一个注释,明确指出“转换值为零的整数常量表达式总是产生一个空指针,但转换其他碰巧具有值为零的表达式不需要产生一个空指针”。

  • 一个负值对于平台来说可能和地址一样有用—— C 标准只需要选择一些东西来表示一个空指针,然后选择了零。老实说,我不确定是否考虑了其他的哨兵值。

空指针的唯一要求是:

  • 它保证比较不等于指向实际对象的指针
  • 任何两个空指针的比较结果都是相等的(C + + 对此进行了细化,以便只需保留指向同一类型的指针)

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

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

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

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

历史上,应用程序的低内存被系统资源占用。在那些日子里,0变成了默认的空值。

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

但是既然内存寻址从0开始,那么0不就是一个有效的地址吗?

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

为什么负数不是零呢?

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

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

但为什么是零?每个系统都共享一个地址。通常低地址是为了操作系统的目的而保留的,因此这个值工作得很好,并且是应用程序的禁区。将整数值偶然赋值给指针的结果可能与其他任何事情一样为零。

操作系统很少允许你写地址0。将特定于操作系统的内容放在低内存中是很常见的; 即 IDT、页表等。(这些表必须位于 RAM 中,并且将它们放在底部比尝试确定 RAM 的顶部位置更容易。)没有一个正常的操作系统会让你随意地编辑系统表。

K & R 做 C 的时候可能没有考虑到这一点,但是它(加上0 = = null 很容易记住的事实)使得0成为一个流行的选择。

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

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

值得庆幸的是,C + + 0x 有一个新的关键字: nullptr,表示一个已知的无效指针,这个无效指针对整数表达式也没有映射到位零。虽然有一些系统,你可以用 C + + 的目标,允许解除引用地址0没有呕吐,所以程序员要小心。

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

无论是在 C 中还是在 C + + 中,指针都不能“值为零”。指针不是算术对象。它们不能有像“零”或“负”这样的数值或任何类似的性质。因此,您关于“指针... 有值为零”的陈述完全没有意义。

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

尽管如此,无论空指针值在给定平台上是如何表示的,在您的代码中,您仍将继续使用常量 0来指定空指针。为了将空指针值赋给给定的指针,您将继续使用像 p = 0这样的表达式。编译器的责任是实现你想要的,并将其转换成适当的空指针值表示,例如,将其转换成将 0xFFFFFFFF的地址值转换成指针 p的代码。

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

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

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

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

对于大多数处理器来说,最便宜的比较是有符号小于0,等于0。(大于0且不等于0的有符号表示这两者)

因为在所有可能的值中,有一个值需要保留为坏值或未初始化的值,那么您不妨将其作为对坏值等价性进行最廉价测试的值。以’0’结尾的字符串也是如此。

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

我的猜测是,神奇的值0被选中来定义一个无效的指针,因为它可以用较少的指令进行测试。有些机器语言在加载寄存器时会根据数据自动设置零和符号标志,这样你就可以用一个简单的加载来测试空指针,然后在不执行单独的比较指令的情况下分支指令。

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

在 Commodore Pet,Vic20和 C64这些我工作的第一台机器上,RAM 从位置0开始,所以如果你真的想的话,使用空指针读写是完全有效的。

常数 0被用来代替 NULL,因为 C 是由几万亿年前的一些穴居人制造的,NULLNILZIP或者 NADDA都比 0更有意义。

但是由于内存寻址开始于 0不仅仅是一个有效的地址 还有别的吗?

确实。尽管很多操作系统不允许你在地址零映射任何东西,甚至在虚拟地址空间(人们意识到 C 是一种不安全的语言,并反映空指针解引用错误是非常普遍的,决定通过禁止用户空间代码映射到页0来“修复”它们; 因此,如果你调用一个回调,但回调指针是 NULL,你不会最终执行一些任意的代码)。

如何使用0来处理 null 如果是这样的话,你有什么建议吗?

因为与指针比较时使用的 0将被替换为某个 具体措施值,该值是 malloc 在 malloc 故障时的返回值。

为什么负数不为零 取而代之?

这会更让人困惑。

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

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

如果你真的非常非常担心这个问题,那么一个更好的方法,一个保证会奏效的方法,就是利用声明() :


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

这需要一些额外的输入,并在调试构建期间进行一次额外的检查,但是它肯定会给您您想要的: 注意当 ptr 被删除“两次”时。在注释讨论中给出的替代方案,不将指针设置为 null,因此您将得到一个崩溃,并不能保证成功。更糟糕的是,与上述情况不同,它可能导致崩溃(或者更糟!)如果这些“虫子”中的一个进入了架子,用户就会受到影响。最后,这个版本允许您继续运行程序,以查看实际发生的情况。

我意识到这并不能回答问题,但是我担心有些人在阅读评论时可能会得出这样的结论: 如果指针有可能被发送到 free ()或者被删除两次,那么不把它们设置为0就是“好的做法”。在这些少数情况下,使用未定义行为作为调试工具从来都不是一个好的做法。没有人会提出这样的建议,因为它最终是由删除无效指针引起的。这种类型的错误需要花费数小时才能找到,并且几乎总是以一种完全意想不到的方式影响程序,这种方式很难回溯到最初的问题。

在这个线程中已经有很多很好的答案; 可能有很多不同的理由来优先选择值 0作为空指针,但是我还要添加两个:

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

这取决于 C/C + + 中指针的实现。没有特定的理由说明为什么赋值中的 NULL 等效于指针。

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

空指针与空值不是一回事。例如,c 的相同 strchr 函数将返回一个 null 指针(控制台上为空) ,而传递该值将在控制台上返回(null)。

真正的作用:

char *ft_strchr(const char *s, int c)
{
int i;


if (!s)
return (NULL);
i = 0;
while (s[i])
{
if (s[i] == (char)c)
return ((char*)(s + i));
i++;
}
**if (s[i] == (char)c)
return ((char*)(s + i));**
return (NULL);
}


这将产生输出为空的东西: 最后一个是输出。

作为值(如 s [ i ])传递给我们一个 NULL (如: 在此输入图像描述)

char *ft_strchr(const char *s, int c)
{
int i;


if (!s)
return (NULL);
i = 0;
while (s[i])
{
if (s[i] == (char)c)
return ((char*)(s + i));
i++;
}
**if (s[i] == (char)c)
return (s[i]);**
return (NULL);
}