当空指针不是所有位都为零时,如何正确编写C/C++代码

70

正如comp.lang.c FAQ所说,存在空指针不是所有位都为零的体系结构。因此,问题是什么实际上检查了以下结构:

void* p = get_some_pointer();
if (!p)
    return;

我是在将p与机器相关的空指针进行比较,还是和算术零进行比较?

应该写:

void* p = get_some_pointer();
if (NULL == p)
    return;

是我太神经质了,还是需要准备这些体系结构呢?


33
这是一个关于 C 还是 C++ 的问题吗?在 C++ 中,你应该总是使用 nullptr - BeyelerStudios
4
只要你写标准的C代码,就不需要关心空指针的实际位表示形式,因为如果定义了if(!p),那么这应该就能回答你的问题。对于其他任何奇怪的实现方式也是一样的 - 遵循标准,让编译器来解决如何将机器变成你想要的样子。请注意,空指针的实际位表示形式是实现细节,不需要过多关注。 - Voo
5
顺便提一下,在C++中,NULL是算术零(即使底层地址不是物理内存地址零)。这意味着在C++中,您可以将NULL0视为相同的,并且它们实际上是无法区分的。请注意,这也适用于C,只需做一个类型转换即可。 - Konrad Rudolph
4
这会导致使用memset清零时可能会得到非空指针。值得一提的是,出现此问题的奇特硬件类型很可能会违反关于现代架构的其他常见假设。更重要的是,虽然我欣赏在实践中编写可移植代码的愿望,但在这些极端平台上,非常规的C/C++代码实际上永远不会起作用,除非它已经在奇异的硬件上进行了测试,至少这是我的经验。 - doynax
2
如果需要两种不同语言的答案,则应该有两个单独的问题,这样做的正确方式在C和C++中并不一定相同(特别是在C++中使用nullptr)。 - Vality
显示剩余11条评论
5个回答

104
根据C规范:
一个整数常量表达式,其值为0,或者将这样的表达式转换为void*类型的指针被称为null指针常量。如果将null指针常量转换为指针类型,则生成的指针称为null指针,保证与任何对象或函数的指针不相等。
因此,0是一个null指针常量。如果我们将其转换为指针类型,我们将得到一个null指针,对于某些体系结构可能非全零位。接下来让我们看看规范关于比较指针和null指针常量的内容:
如果一个操作数是指针而另一个是null指针常量,则将null指针常量转换为指针的类型。
考虑(p == 0):首先,0被转换为null指针,然后将p与一种依赖于体系结构的实际位值的null指针常量进行比较。 接下来,看看规范对取反运算符的说明:
逻辑取反运算符!的结果为0,如果其操作数的值与0不相等,则为1。结果具有int类型。表达式!E等同于(0 == E)。
这意味着(!p)等同于(p == 0),根据规范,这测试p是否等于机器定义的null指针常量。
因此,即使在null指针常量不是全零位的体系结构上,您也可以安全地编写if (!p)。
至于C ++,null指针常量定义为:
null指针常量是一个整数类型的integral常量表达式(5.19)prvalue,其评估为零或std :: nullptr_t类型的prvalue。可以将null指针常量转换为指针类型。类型为指针或函数指针的每个其他值都不同,结果是该类型的空指针值。

对于C++来说,这与我们所拥有的内容非常接近,再加上nullptr语法糖。运算符==的行为由以下规定:

此外,成员指针可以进行比较,或者进行成员指针和空指针常量的比较。通过进行成员指针转换(4.11)和限定符转换(4.4),将它们转换为共同的类型。如果一个操作数是空指针常量,则共同类型是另一个操作数的类型。否则,共同类型是成员指针类型,类似于其中一种操作数的类型(4.4),其cv限定符签名(4.4)是操作数类型的cv限定符签名的并集。[注意:这意味着任何成员指针都可以与空指针常量进行比较。-结束语]

这导致将0转换为指针类型(与C相同)。对于否定运算符:

逻辑否定运算符!的操作数在上下文中被转换为布尔类型(第4节);如果转换后的操作数为true,则其值为true,否则为false。结果的类型是布尔类型。

这意味着!p的结果取决于如何从指针到bool进行转换。标准规定:

零值、空指针值或空成员指针值被转换为false;

因此,在C++中,if (p==NULL)if (!p)执行的操作相同。


9
以我之见,这是到目前为止唯一“完整”的答案,因为它还指出了标准定义了!EE == 0相同(C11草案6.5.3.3/7)等。 - alk
1
你是否在引用C++标准中误解了取反运算符?它不应该具有类型为“int”的结果。根据C++11 5.3.1/9,“结果的类型是'bool'”。 - Ruslan
非常详细的答案,很棒。 - user1
这个答案让我相信 if (! p) 是被定义良好的。但是 if (p) 呢?这可能看起来很荒谬,但是这个答案中的引用实际上并没有保证它的语义。我相信 §6.8.4.1p2 才是关键。 - Konrad Rudolph

32

在实际机器中,空指针是否是全零比特并不重要。假设p是一个指针:

if (!p) 

使用 == nullptr 检测指针变量 p 是否为空指针是合法的方法,而且它与以下表达式等价:

if (p == NULL)

你可能对另一篇C-FAQ文章感兴趣:这很奇怪,NULL保证为0,但空指针却不是?


以上适用于C和C++。请注意,在C++(11)中,建议使用nullptr表示空指针字面常量。


3
为什么链接文章中的陈述都不完整?它们都以“...”结尾,为什么? - Marson Mao
2
@MarsonMao 因为它们中的每一个都以下一条语句的引导开始。 - Ixrec

12

此答案适用于C语言。

不要混淆NULL和空指针。 NULL只是一个宏,保证是一个空指针常量。空指针常量保证为0(void*)0

C11 6.3.2.3中的说明:

值为0的整型常量表达式,或者这种类型强制转换成void* 的表达式称为空指针常量66)。如果将空指针常量转换为指针类型,则得到的指针称为null指针,并保证与任何对象或函数的指针比较不相等。

66) 宏NULL在stddef.h和其他头文件中定义为null指针常量;参见7.19。

7.19 中的说明:

宏有

NULL

它扩展为实现定义的null指针常量;

NULL的情况下,实现定义为0(void*)0NULL不能是其他任何东西。

然而,当把空指针常量赋给指针时,你会得到一个空指针,它的值可能不是零,尽管它与空指针常量比较相等。代码if(!p)NULL宏没有关系,它将空指针与算术值0进行比较。

因此,理论上,像int* p = NULL这样的代码可能会产生一个空指针p,其值与零不同。


这是一个关于编程问题的相关提问,拥有一些不错的答案。Related question - Lundin
2
一个空指针常量可以是一个计算结果为0的内部编译错误(ICE),例如NULL可以被定义为(1-1) - M.M
在NULL的情况下,实现定义要么是0,要么是void*(即指向空地址的指针)。 - M.M
1
“一个空指针,不一定等于零值。” - 一个空指针保证与 0 相等。 - M.M
2
一元运算符!的定义是,!xx == 0相同。由于在关于NULL指针的预期效果上,x == 0具有期望的效果,因此在NULL指针不等于0的平台上,!x确实会检查NULL - fuz
显示剩余3条评论

7

早期的STRATUS计算机在所有语言中将空指针作为1处理,这对于C语言造成了问题。

因此,他们的C编译器允许将指针的0和1进行比较,以返回true。

这样做可以实现:

void * ptr=some_func();
if (!ptr)
{
    return;
}

即使在调试器中看到ptr的值为1,也可能返回空指针null ptr

if ((void *)0 == (void *)1)
{
    printf("Welcome to STRATUS\n");
}

实际上会打印出“欢迎来到STRATUS”


2
空指针常量可以有任何表示形式,这不是历史遗留问题,也不是单个架构的扩展。这是两种语言的核心事实。毕竟,抽象化是这些语言的全部目的。 - Lightness Races in Orbit
@LightnessRacesinOrbit 嗯,那么您认为整数类型和指针类型之间的区别是什么呢?什么使得要求所有位都为零来表示零是可以接受的,但要求所有位都为零来表示空指针就不可以接受呢?我能想到的唯一显著的区别是,在过去的实际世界中存在着所有位都为零并不代表空指针的情况。对于整数类型,也有合法的理由使所有位都为零表示其他值,那么为什么您的论点在这里不适用呢? - user743382
@LightnessRacesinOrbit 这个以前不是必须的,但现在已经是了:http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_263.htm (这是针对 C 语言的。C++ 没有明确要求,但需要与符合标准的 C 实现具有二进制兼容性。) - user743382
@hvd:好的。有点伤心:( - Lightness Races in Orbit
1
对于好奇的人,我找到了一份描述Stratus计算机空指针的文档:http://ftp.stratus.com/vos/doc/reference/page_0_control.txt - Tor Klingberg
显示剩余5条评论

2

如果你的编译器足够好,那么只有两个问题(且只有两个问题)需要注意:

1: 静态默认初始化(未被分配)的指针不会具有NULL值。

2: 在结构体或数组上使用memset()函数,或者使用calloc()函数将不会把指针设为NULL。


3
关于第二点,你是正确的,但是关于第一点,你是错误的。C++03第3.6.2节规定,“具有静态存储期的对象应该被零初始化(8.5)”,第8.5段(5)定义了标量类型T的“零初始化”为将常数0转换为类型T。因此,即使NULL不是全零,静态持续时间指针也会被初始化为NULL。(C语言规范中也包含类似的措辞。) - Nemo
1
@MattMcNabb:我认为他的意思是“如果你的编译器有错误,可能会出现其他情况”。 - Harry Johnston
@Matt McNabb:这并不总是正确的,而且对于剩余的架构,编译器往往会实现旧版本的标准。 - Joshua
1
@Joshua:正如Matt所说,每个C和C++标准的每个版本都保证静态持续时间指针被正确初始化为NULL,就像你写了... = 0;一样。关于(1),你对于符合标准的编译器是错误的。 - Nemo
1
我敢肯定我的书上说它是二进制0,而且在一个没有NULL = 二进制零的平台上我曾经使用过的唯一一个也是如此。 - Joshua
显示剩余3条评论

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