每个空指针常量都是一个空指针吗?

18

来自C17草案(6.3.2.3 ¶3):

值为0的整数常量表达式,或将这样的表达式强制转换为类型void *,被称为空指针常量67)。如果将空指针常量转换为指针类型,则所得到的指针,称为空指针,保证与任何对象或函数的指针比较时都不相等。

67)NULL<stddef.h>(和其他头文件)中被定义为一个空指针常量[...]。

由此可见,以下是空指针常量00UL(void *)0(void *)0ULNULL

它还跟着以下的空指针(int *)0(int *)0UL(int *)(void *)0(int *)(void *)0UL(int *)NULL。有趣的是,这些都不是“空指针常量”;请参阅here
以下的空指针常量空指针(因为void *是一个指针类型,00UL是空指针常量):(void *)0(void *)0UL。关于这一点,根据C17草案(6.2.5 ¶19-20):

void类型包括一组空值;它是一个不完整的对象类型,无法完成。
[...]
指针类型可以从函数类型或对象类型派生出来,称为引用类型。[...] 指针类型是一个完整的对象类型。

void本身不是指针类型,它是一个不完全的对象类型。但是void *是一个指针类型。

但是以下似乎是空指针常量,但并非空指针(因为没有将其强制转换为指针类型):00ULNULL。(准确地说,虽然标准只要求将NULL定义为“空指针常量”,但也可以将其定义为既是空指针常量又是空指针的情况。但是似乎标准并不要求NULL以同时是空指针的方式进行定义。)

每个空指针常量都是空指针吗?NULL真的不是空指针吗?)

最后(有点玩笑地说):如果某些空指针常量不是空指针,它们在技术上是否属于一种“非空指针”?(这个措辞出现在标准的一些地方。)请注意,从语言学角度来看,我们有所谓的“括号悖论”;我们可以将其解读为“[非空]指针”或“非[空指针]”。

11
我觉得这就像是计算机科学专业的哲学专业在试图定义“存在的含义”一样。我会站在“空指针常量0、0L和NULL不是空指针”的一边,因为在它们被转换之前没有任何暗示它们指向任何数据。相比之下,NULL更容易被认为是空指针,因为它作为一个值意味着它有容纳数据的能力,这与指针的预期类似,而0和0L则对于初始化变量来说是完全有效的。 - Gumpf
7
我不明白。您说“以下是不是空指针的空指针常量”,然后接着说“每个空指针常量都是空指针吗?”。您已经回答了这个问题。 - KamilCuk
3
从语言学角度来看,我个人总是将连字符读作比空格“更紧密地绑定”。因此,“非空指针”对我来说总是看起来像是在说“一个非空的指针”,而不是“不是空指针的东西”。我承认这并不是普遍遵循的规则,但如果我想要“不是空指针的东西”的意思,我总会找到另一种表达方式,特别是在技术写作中。 - Ben
6
不要忘记 nullptr 来自 C23关键字 nullptr 表示预定义的空指针常量。它是 nullptr_t 类型的非左值。nullptr 可以转换为指针类型或 bool 类型,其中结果分别为该类型的空指针值或 false。 - Ayxan Haqverdili
5
这就是为什么在将NULL传递给可变参数函数时,例如execl()中的最后一个参数,应该将其转换为指针类型的原因。 - Barmar
显示剩余3条评论
6个回答

24
每个空指针常量都是空指针吗?
TL;DR:不是。
正如您已经观察到的,具有值0的整数常量表达式是空指针常量,尽管它们没有指针类型。您还引用了规范对空指针的定义:“一个转换为指针类型的空指针常量[]”。这意味着这种一般形式的空指针常量...
(void *)(<integer constant expression with value 0>)

...满足“空指针”的定义。整数常量表达式本身就是一个空指针常量,所以强制转换使整个表达式成为一个空指针(除了它本身已经是一个空指针常量)。

另一方面,以值为0的整数常量表达式形式表示的空指针常量不满足“空指针”的定义,语言规范中也没有其他条款将其定义为空指针。例如:00x00UL1 + 2 + 3 - 6

似乎标准并不要求将NULL定义为同时是空指针的方式。

正确。

每个空指针常量都是空指针吗?

绝对不是(见上文),但对于大多数目的而言,这并不重要。

NULL真的不是空指针吗?)

这取决于您的C实现。语言规范允许任何一种答案。实际上,在您可能遇到的大多数实现中,它是一个空指针。

如果某些空指针常量不是空指针,它们在技术上会成为一种“非空指针”吗? 不会。那些不是空指针的空指针常量实际上根本不是指针,而是整数。

10
每个空指针常量都是空指针吗?
不是的,原因就在你引用的文本中:
如果将空指针常量转换为指针类型,则生成的指针(称为空指针)保证与任何对象或函数的指针比较时不相等。
空指针常量并不自动成为指针,就像任何整数常量都不会自动成为指针一样。必须将常量值转换为指针类型才能产生空指针。
生成的空指针不必为零值。它只需要是不能成为任何对象或函数地址的值即可。该值可以是0x00000000(在我熟悉的实现中是这样的),也可以是0xFFFFFFFF,也可以是0xDEADBEEF,也可以是其他值。

1
大多数现代架构确实鼓励将空指针设置为零值,这样一个类型不可知的零填充缓冲区重新解释为指针(或包含指针字段的结构体)将成为有效的空指针。该语言允许其他值(在这种情况下,如果代码省略了构造函数或者对规则过于宽松,可能会出现问题,尤其是在这些架构中)。即使在现代时代,某些架构选择非零空指针底层值也有一些很好的理由,尽管这需要更多的工作。 - Miral
1
“结果为空指针不一定是零值”这句话在一个非常重要的方面会误导新手:无论空指针的位表示如何,C标准都要求它与零相等,并且在if&&等条件语句中被视为false。 - zwol
1
C标准要求空指针与空指针常量相等,但不要求它与“零”概念相关的任何其他内容相等。 - supercat
1
@zwol:(uintptr_t)somePointer == 0 或者 ptr == (void*)someUintPtr 怎么样?这里的 someUintPtr 是一个非常量表达式,类型为 uintptr_t。请注意,如果 someUintPtr 是通过将空指针转换为 uintptr_t 而形成的,则通过将该值转换回 (void*) 形成的指针必须与空指针比较相等,但是 uintptr_t 值可能不为零,在将空指针转换为 uintptr_t 将产生非零值的实现中,将值为 0 的 uintptr_t 转换为指针可能不会产生空指针。 - supercat
1
@supercat - 我很久以前就测试过这个,我几乎可以确定发生的是 - 指针的非零值将被保留 - 并存储在uintptr_t中。只是与空指针常量的比较将检查该值而不是0(在内部和所有情况下 - 它是实现的一部分)。我可能会发布一个问题,并自己回答这个话题(带有所述的实现 - 顺便说一句,它也不过时 - 任何人都可以轻松验证)。 - AnArrayOfFunctions
显示剩余8条评论

5

不是的。实际上,没有任何空指针常量是空指针!这是因为常量和指针是不同类型的实体。

空指针常量是具有特定形式的常量表达式。表达式是一系列标记,而空指针常量被定义为具有特定形式的标记序列。

空指针是一个值。在C语言中,每种类型都有其潜在的值集合。对于每种指针类型,该集合中的一个或多个值是空指针。C标准没有正式定义值的概念。正式的语义需要这样做(并且正式定义指针的值变得相当复杂,这就是为什么C标准是一份没有数学写作的英文文档的原因)。

在上下文中,表达式求值为一个值(可能会引起副作用)。所有类型为指针类型的空指针常量都会求值为一个空指针。一些空指针常量(例如01L - 'z' / 'z')具有整数类型,它们不会求值为一个空指针:它们会求值为一个空整数(即值为0的整数——C标准没有使用“空整数”这个表达式,因为它并没有什么特别之处需要一个特定的名称)。

C标准保证,如果e是一个具有整数类型且值为0的常量表达式,那么将该值转换为指针类型的任何表达式都会求值为一个空指针。请注意,这个保证并不适用于任意表达式:(void*) f()可能不是一个空指针,即使f被定义为int f(void) { return 0; }

C标准允许NULL具有整数类型或指针类型。如果它具有指针类型,则表达式NULL求值为一个空指针。如果它具有整数类型,则不会。


5

空指针常量可以是void *或某些整数类型。

在您的计算机上进行测试:

#include <stdio.h>
#include <stdlib.h>

#define NULL_TEST(n) _Generic((n), \
  void *: "void *", \
  int: "int", \
  long: "long", \
  default: "something else" \
)

int main(void) {
  printf("%s\n", NULL_TEST(NULL));
  printf("%s\n", NULL_TEST((void*)0));
  printf("%s\n", NULL_TEST(0));
  printf("%s\n", NULL_TEST(0L));
}

在我的电脑上,我得到了以下输出。你的输出可能与第一行不同。
void *
void *
int
long

4
“在你的平台上测试”很少是对于[标签:语言律师]问题的一个好答案。不过第一句话是正确的。 - Toby Speight
1
在这里为什么要使用printf而不是puts - Ayxan Haqverdili
1
@AyxanHaqverdili 十多年以来,我看到好的编译器为 printf("...\",string);puts(string); 生成相同的代码。这很容易让你在两者之间做出风格选择,因此无论你喜欢哪种方式,都可以信任编译器生成优秀的代码。其中一个与问题无关,我倾向于在其他 printf() 不需要时使用 puts() 进行编码,学习者更容易理解 printf()。由你决定。 - chux - Reinstate Monica

3

另一个有趣的问题是,'\0' 的类型为 int (6.4.4.3(10)),“由八进制整数形成的数字值指定所需字符或宽字符的值” ((5)),十六进制转义也是如此。因此,'\0''\x0' 都是空指针常量。此外,“作为强制转换的立即操作数的浮点操作数”(必须将算术类型强制转换为整数类型)是合法的“整数常量表达式”,因此 (int)0.0 是空指针常量。因此,enum 值、sizeof 的结果(尽管所有标准类型的大小至少为 1,但某些编译器具有零大小字段作为扩展),以及 _Alignof 的结果(尽管标准规定它只能返回正的 2 的幂,并且忽略对齐为 0),以及其操作数为整数类型的运算符的结果,例如 X^X!1

现代编译器中,一些将NULL定义为特殊关键字,例如gcc中的__null或在C++上交叉编译时的nullptr。这使得编译器可以捕获错误,如果程序使用NULL,其中一个整数常量或void*可能会隐式转换为不是指针的表达式,例如布尔值。


1
我认为sizeof_Alignof表达式都不能计算为0。但是,空指针常量可以包含这样的表达式作为更大的整数常量表达式的一部分。 - John Bollinger
@JohnBollinger 标准规定:“每个有效的对齐值都应该是非负整数的二次幂。” 此外,“除了位域,对象由一个或多个字节的连续序列组成”,位域的大小也不允许为0。然而,我相信编译器可以有扩展,使sizeof返回零。一种常见的扩展是没有唯一地址的字段,旨在具有零字节的存储空间,实现可能希望struct的所有元素的大小不超过struct的大小。 - Davislor
好的,但是由于这是一个关于语言规则的问题,如果你想深入探讨不符合规范的扩展,那么你至少应该注明你正在这样做。 - John Bollinger

2

C语言的设计使得在最初的目标平台上,指针和整数在大多数情况下可以互换使用。例如,对于char *p; int i;,编译器处理p=0;时会与处理i=0;基本相同,只是前者会将值0写入p的地址,而后者会将值0存储到i的地址。编译器不需要理解空指针的概念,因为用于将i设置为数字零的相同编译器逻辑也可以有效地将p设置为一个不与任何对象相关联且行为类似于值零的值。

C标准的编写方式不允许表达式的类型根据其所在的上下文而变化。虽然在p=0;中赋值运算符的右操作数可能是指针类型,在i=0;中右操作数可能是整型,但标准的设计要求它们都具有相同的类型。由于没有一种“正常”的类型可以在两个上下文中使用,C标准的作者为表达式创建了一个特殊的“类型”,这个类型应该在两个上下文中都能够使用。我认为“空指针”常量这个术语比必要的更加混乱,而“通用零”可能更清晰,因为零代表的不仅仅是数字零、空指针或全零位模式,而更普遍地代表了静态持续对象的默认值。

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