为什么 C 语言中有两种表达 NULL 的方式?

Bra*_*nes 69 c null null-pointer language-lawyer

根据 C11 标准的 \xc2\xa76.3.2.3 \xc2\xb63,C 中的空指针常量可以由实现定义为整数常量表达式0或转换为 的此类表达式void *。在 C 语言中,空指针常量由宏定义NULL

\n

我的实现(GCC 9.4.0)NULL通过stddef.h以下方式定义:

\n
#define NULL ((void *)0)\n#define NULL 0\n
Run Code Online (Sandbox Code Playgroud)\n

为什么上述两个表达式在 的上下文中被认为在语义上等效NULL?更具体地说,为什么存在两种而不是一种表达同一概念的方式?

\n

pts*_*pts 54

让我们考虑一下这个示例代码:

#include <stddef.h>
int *f(void) { return NULL; }
int g(int x) { return x == NULL ? 3 : 4; }
Run Code Online (Sandbox Code Playgroud)

我们希望f编译时不产生警告,并且希望g引发错误或警告(因为int变量x与指针进行比较)。

在 C 中,给了我们两者( g#define NULL ((void*)0)的 GCC 警告, f的干净编译)。

但是,在 C++ 中,会导致f#define NULL ((void*)0)编译错误。因此,为了使其能够在 C++ 中编译,<stddef.h>仅适用于 C++(不适用于 C)。不幸的是,这也阻止了g报告警告。为了解决这个问题,C++11 使用内置函数而不是,因此,C++ 编译器会报告g错误,并干净地编译f#define NULL 0nullptrNULL

  • @Haris,“nullptr”和“nullptr_t”已添加到最新的 C23 草案中。请参阅 https://www9.open-std.org/JTC1/SC22/WG14/www/docs/n3054.pdf (10认同)
  • 但这并不能解释为什么 C 允许“0”作为空指针常量。 (7认同)
  • 我希望```nullptr```将被添加到C的下一个版本中。 (6认同)
  • @JonathanLeffler 这个特定问题对我来说总是感觉像是 (1) `NULL` 表示指针值和 (2) `NUL` 表示 [***C0 控制代码*** 名称] 中字符 0 的名称之间的混淆。 (https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Basic_ASCII_control_codes),同一组还给我们带来了“ACK”、“NAK”、“BEL”、“BS”和“FF”等。(这些现在在从 U+2400“SYMBOL FOR NULL”开始的 Unicode“Control_Pictures”块中进一步记录为图形符号,但这在这里并不重要。) (6认同)
  • @pts 跑,不要走,到 https://c-faq.com/null/machexamp.html 。 (4认同)
  • @mmmmmm PDP-11 上确实出现了 0 表示 NULL,但并不是因为它是无效的内存地址。0 在当时是一个完全有效的内存地址,IIRC 直到 20 世纪 80 年代 BSD Unix 实现了虚拟内存,它才成为一种选项(最终成为标准做法),以安排包含地址 0 的页面不被映射到。 (3认同)
  • @mmmmmm 声称在实际空指针非零的机器上使用 0 表示空指针是一个错误,这暴露了一种误解,IMO。打个比方:在浮点中,“1.0”的位模式看起来与二进制“0001”完全不同,并且存在一些机器,其中“0.0”的位模式不是全位0。然而,“float f = 0.0;”(以及类似的“float f = 0;”)显然必须在高级别上按预期工作,这意味着编译器必须在必要时在幕后生成非零位模式在初始化的数据段中。 (3认同)
  • 在 C++ 中使用 `#define NULL nullptr` 会有什么问题吗? (2认同)

Lun*_*din 34

((void *)0)具有更强的类型,可以带来更好的编译器或静态分析器诊断。例如,因为标准 C 中不允许指针和普通整数之间的隐式转换。

0可能是出于历史原因而被允许的,从标准之前的时间开始,C 中的所有内容几乎都只是整数,并且允许指针和整数之间的疯狂隐式转换,尽管可能会导致未定义的行为。

古代 K&R 第一版提供了一些见解(7.14 赋值运算符):

编译器当前允许将指针分配给整数、将整数分配给指针以及将指针分配给其他类型的指针。赋值是纯粹的复制操作,没有任何转换。这种用法是不可移植的,并且可能会产生在使用时导致寻址异常的指针。但是,可以保证将常量 0 分配给指针将产生一个与指向任何对象的指针不同的空指针。

  • @tstanisl 没有人(包括 K&amp;R 第一版)声称这一点。空指针和空指针常量是不同的东西。 (7认同)
  • `int *x = 0;` 并不意味着 `x` 仅由归零的位组成。从任何其他常量整数文字或任何变量进行转换都会引发警告。我认为“NULL”的真正原因是像“printf”中的隐式转换。 (2认同)
  • 您可能会补充说,当传递给 vararg 函数(例如“execl”)时,“0”和“((void *)0)”并不等效。`int` 和 `void *`(或 `char*`)具有不同宽度或参数传递约定的系统应将 `NULL` 定义为 `((void *)0)` 以避免 `execl("/bin" 上的未定义行为/ls", NULL)` 和类似的调用。 (2认同)

Ste*_*mit 16

C 语言中没有什么比空指针更令人困惑的了。C FAQ 列表占据了整个部分来讨论该主题以及永远出现的无数误解。我们可以看到,这些误解永远不会消失,因为其中一些误解甚至在 2022 年的本线程中也被重复使用。

\n

基本事实如下:

\n
    \n
  1. C有空指针的概念,这是一个明确指向任何地方的独特指针值。
  2. \n
  3. 请求空指针 \xe2\x80\x94空指针常量\xe2\x80\x94 的源代码构造从根本上涉及 token 0
  4. \n
  5. 由于令牌0还有其他用途,因此可能会出现歧义(更不用说混淆)。
  6. \n
  7. 为了帮助减少混乱和歧义,多年来,标记0作为空指针常量一直隐藏在预处理器宏后面NULL
  8. \n
  9. NULL为了提供某种类型安全性并进一步减少错误,包含指针强制转换的宏定义很有吸引力。
  10. \n
  11. 然而,最不幸的是,一路上出现了足够多的混乱,以至于适当缓解这一切几乎变得不可能。特别是,有如此多的现有代码表示类似的内容strbuf[len] = NULL;(明显但基本上是错误的尝试以空值终止字符串),以至于在某些圈子中相信不可能使用包括NULL显式强制转换在内的扩展来实际定义或假设的未来(或 C++ 中现有的) new 关键字nullptr
  12. \n
\n

另请参见为什么不调用 nullptr NULL?

\n

脚注(将此点称为 3\xc2\xbd):尽管在 C 源代码中将空指针 \xe2\x80\x94 表示为整数常量 0 \xe2\x80\x94,但空指针 \xe2\x80\x94 也可能具有内部值不全0。每当讨论这个主题时,这一事实都会大大增加混乱,但它并没有从根本上改变定义。

\n

  • 仅供记录,`int x = ((void*)0);` 确实在 C 中编译,只是带有警告(即使没有 `-Wall`,GCC 中默认情况下也是如此)。https://godbolt.org/z/dMYq8ceTT 因此,GCC 当前对“NULL”的定义与在非指针上下文中滥用“NULL”的混乱遗留代码兼容,就像在 C++ 中一样,除了警告之外。但是,如果编译器将 NULL 定义为 C23 `nullptr`,这样的代码就会中断,因此不幸的是,编译器可能会选择不这样做,就像您链接的 C++ 问题一样,[Why not call nullptr NULL?](https://stackoverflow. com/q/32302615) (2认同)
  • @PeterCordes 不存在“仅编译警告”之类的事情。警告的意思是“这里有致命的错误或无效的 C!现在你需要修复它”,它并不意味着“这里有一些装饰细节,你可以担心未雨绸缪”。至于编译器在发现明显不正确的C时有义务做什么,给出警告就可以了。[C 编译器发现错误时必须做什么?](https://software.codidact.com/posts/277340) (2认同)

Ded*_*tor 13

在 C 中只有一种表达方式NULL,即单个 4 字符标记。
但等一下,当深入了解它的定义时,它会变得更有趣。

NULL必须定义为空指针常量,即值为 0 或此类转换为的整数常量void*
由于整数常量只是整数类型的表达式,带有一些限制来保证静态计算,因此任何想要的值都有无限的可能性。

在所有这些可能性中,只有值为 0 的整数文字也是C++ 中的空指针常量(无论其价值如何)。

造成这种变化的原因是历史和先例(每个人的void*做法都不同,迟到了,现有的代码/实现胜过一切),并通过保留它的向后兼容性得到了加强。

6.3.2.3 指针

[...] 值为 0 的整型常量表达式,或此类转换为类型的表达式void *,称为空指针常量
67) 如果将空指针常量转换为指针类型,则生成的指针(称为空指针)保证与任何对象或函数的指针比较不相等。[...]

6.6 常量表达式

[...] 描述
2 常量表达式可以在翻译期间而不是运行时求值,因此可以在常量所在的任何地方使用。
约束 3 常量表达式不得包含赋值、递增、递减、函数调用或逗号运算符,除非它们包含在未计算的子表达式中。117)
4 每个常量表达式的计算结果应为范围内的常量其类型的可表示值。
语义
5 在多种上下文中需要计算结果为常量的表达式。如果在翻译环境中计算浮点表达式,则算术范围和精度应至少与在执行环境中计算表达式一样大。118)
6 整数常量表达式119) 应具有整数类型,并且仅具有操作数为整型常量、枚举常量、字符常量、结果为整型常量的 sizeof 表达式、_Alignof 表达式以及作为强制转换的直接操作数的浮点常量。
整数常量表达式中的强制转换运算符只能将算术类型转换为整数类型,除非作为 sizeof 或 _Alignof 运算符的操作数的一部分。

  • @Lundin 任何值为 0 的整数常量表达式,或此类转换为“void*”。这允许算术、逻辑、三元运算符、任意数量的括号、枚举、未计算的子表达式……这并不意味着推动它是一个好主意。 (5认同)
  • “但是,即使我们将我们限制为标准 C,也可以通过实现来定义 NULL 的无限种方式”不,只有两种。`0` 或 `(void*)`。当 7.19 说“NULL 扩展为实现定义的空指针常量”时,它意味着这两个中的任何一个(或它们的风格,例如“0L”),因为它们是唯一的空指针常量。然而_空指针_的二进制表示可以是任何东西。[空指针和NULL有什么区别?](https://software.codidact.com/posts/278657) (2认同)

Dav*_*lor 8

C 最初是在空指针常量和整数常量0具有相同表示形式的机器上开发的。后来,一些供应商将该语言移植到大型机,其中不同的特殊值在用作指针时触发了硬件陷阱,并希望将该值用于NULL. 这些公司发现,现有的代码在整数和指针之间进行了类型双关,他们必须将其识别0为可以隐式转换为空指针常量的特殊常量。ANSI C 合并了这种行为,同时引入了作为void*隐式转换为任何类型的对象指针的指针。这可以NULL用作更安全的替代品0

\n

我\xe2\x80\x99见过一些代码(可能是半开玩笑的)通过测试检测到其中一台机器if ((char*)1 == 0)

\n

  • 将参数传递给非原型函数或可变参数函数时会出现一个相关问题。如果一个函数接受可变数量的指针参数,并且后面必须跟一个空指针,则传递 0 作为最后一个参数将在指针和 int 共享相同表示的平台上工作,但在例如以下平台上可能会失败整数使用一个 16 位堆栈槽进行传递,而指针则使用两个 16 位堆栈槽进行传递。 (4认同)

chu*_*ica 8

为什么存在两种表达同一概念的方式而不是一种?

历史。

NULL开始是为了0鼓励更好的编程实践((void *)0)


首先,有两种以上的方法

#define NULL ((void *)0)
#define NULL 0
#define NULL 0L
#define NULL 0LL
#define NULL 0u
...
Run Code Online (Sandbox Code Playgroud)

之前void *(C89 之前)

在存在之前void *并被使用过。void#define NULL some_integer_type_of_zero

使该整数类型的大小与对象指针的大小相匹配非常有用。考虑以下内容。对于 16 位int和 32 位long,用于匹配对象指针宽度的零类型很有用。

考虑打印指针。

double x;
printf("%ld\n", &x);  // On systems where an object pointer was same size as long
printf("%ld\n", NULL);// Would like to use the same specifier for NULL
Run Code Online (Sandbox Code Playgroud)

使用32位对象指针,#define NULL 0L效果更好。

double x;
printf("%d\n", &x);  // On systems where an object pointer was same size as int
printf("%d\n", NULL);// Would like to use the same specifier for NULL
Run Code Online (Sandbox Code Playgroud)

使用16位对象指针,#define NULL 0效果更好。


C89

void,诞生之后void *,很自然的就让空指针常量成为了指针类型。这允许 的位模式(void*)0)为非零。这在某些架构中很有用。

printf("%p\n", NULL);
Run Code Online (Sandbox Code Playgroud)

对于 16 位对象指针,可以按#define NULL ((void*)0)上述方式工作。
使用 32 位对象指针,#define NULL ((void*)0)可以工作。
使用 64 位对象指针,#define NULL ((void*)0)可以工作。
对于 16 位int#define NULL ((void*)0)可以工作。
对于 32 位int#define NULL ((void*)0)可以工作。
我们现在已经独立于尺寸了int/long/object pointer((void*)0)在所有情况下都有效。

使用作为参数#define NULL 0传递时会产生问题,因此需要为高度可移植的代码做一些令人厌烦的事情。NULL...printf("%p\n", (void*)NULL);

使用#define NULL ((void*)0),类似的代码char n = NULL; 更有可能引发警告,与“#define NULL 0”不同


C99

随着 的出现_Generic,无论好坏,我们都可以区分为NULL, void *, int, long...


Joh*_*ger 7

\n

根据 C11 标准的 \xc2\xa76.3.2.3 \xc2\xb63,C 中的空指针常量可以由实现定义为整数常量表达式0或此类表达式转换为void *

\n
\n

不,这是对语言规范的误导性解释。引用段落的实际语言是

\n
\n

值为 0 的整数常量表达式,或此类表达式强制转换为类型void *称为空指针常量。[...]

\n
\n

实现无法在这些替代方案之间进行选择。 两个都都是 C 语言中空指针常量的形式。为此目的,它们可以互换使用。

\n

而且,不仅特定的整型常量表达式0可以起到这个作用,任何值为0的整型常量表达式都可以。例如,1 + 2 + 3 + 4 - 10就是这样的表达方式。

\n

此外,不要将空指针常量与宏混淆NULL。后者是通过符合扩展为空指针常量的实现来定义的,但这并不意味着 的替换文本NULL唯一的空指针常量。

\n
\n

我的实现(GCC 9.4.0)定义NULLstddef.h以下方式定义:

\n
#define NULL ((void *)0)\n#define NULL 0\n
Run Code Online (Sandbox Code Playgroud)\n
\n

当然,不能同时进行。

\n
\n

为什么上述两个表达式在 NULL 上下文中被认为在语义上是等价的?

\n
\n

再次出现逆转。这不是“上下文NULL”。它是指针上下文。宏没有什么特别特别的地方NULL来区分它出现的上下文和其替换文本直接出现的上下文。

\n

我猜您是在询问第 6.3.2.3/3 段的基本原理,而不是“因为 6.3.2.3/3”。C11 没有公开的理由。C99有一个,它主要也为 C90 服务,但它没有解决这个问题。

\n

然而,应该指出的是,void(因此void *)是开发原始 C 语言规范(“ANSI C”/C89/C90)的委员会的发明。void *在此之前,不可能有“整数常量表达式转换为类型”。

\n
\n

更具体地说,为什么存在两种表达同一概念的方式而不是一种?

\n
\n

真的有吗?

\n

如果我们接受值为 0 的整型常量表达式作为空指针常量(源代码实体),并且希望将其转换为运行时空指针值那么我们选择哪种指针类型?指向不同对象类型的指针不一定具有相同的表示形式,因此这实际上很重要。对我来说,类型void *似乎是自然的选择,这与以下事实是一致的:在所有指针类型中,void *无需强制转换即可将其转换为其他对象指针类型。

\n

但是,在 is0被解释为空指针常量的上下文中,将其强制转换为void *无操作,因此与在这样的上下文中(void *) 0表达的内容完全相同。0

\n

这里到底发生了什么

\n

在 ANSI 委员会工作时,许多现有的 C 实现接受整数到指针的转换而无需强制转换,尽管大多数此类转换的含义是特定于实现和/或上下文的,但人们广泛接受将常量转换为0指针产生一个空指针。这种用法是将整数常量转换为指针的最常见用法委员会希望对类型转换施加更严格的规则,但又不想破坏所有使用的现有代码0表示空指针的常量的现有代码。

\n

所以他们破解了规范

\n

他们发明了一种特殊的常量,即空指针常量,并提供了围绕它的规则,使其与现有用途兼容。无论词法形式如何,空指针常量都可以隐式转换为任何指针类型,从而生成该类型的空指针(值)。否则,不会定义隐式整数到指针的转换。

\n

但委员会更倾向于空指针常量实际上应该具有无需转换的指针类型(无论0是否有指针上下文,都不会),因此他们提供了“强制转换为类型void *”选项作为空指针常量定义的一部分。当时,这是一个具有前瞻性的举措,但现在普遍的共识似乎是,这是一个正确的目标方向。

\n

为什么我们仍然有“值为 0 的整数常量表达式”?向后兼容性。与传统习惯用法的一致性,例如{0}任何类型对象的通用初始值设定项。抵制变革。也许还有其他原因。

\n