Bra*_*nes 69 c null null-pointer language-lawyer
根据 C11 标准的 \xc2\xa76.3.2.3 \xc2\xb63,C 中的空指针常量可以由实现定义为整数常量表达式0或转换为 的此类表达式void *。在 C 语言中,空指针常量由宏定义NULL。
我的实现(GCC 9.4.0)NULL通过stddef.h以下方式定义:
#define NULL ((void *)0)\n#define NULL 0\nRun Code Online (Sandbox Code Playgroud)\n为什么上述两个表达式在 的上下文中被认为在语义上等效NULL?更具体地说,为什么存在两种而不是一种表达同一概念的方式?
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
Lun*_*din 34
((void *)0)具有更强的类型,可以带来更好的编译器或静态分析器诊断。例如,因为标准 C 中不允许指针和普通整数之间的隐式转换。
0可能是出于历史原因而被允许的,从标准之前的时间开始,C 中的所有内容几乎都只是整数,并且允许指针和整数之间的疯狂隐式转换,尽管可能会导致未定义的行为。
古代 K&R 第一版提供了一些见解(7.14 赋值运算符):
编译器当前允许将指针分配给整数、将整数分配给指针以及将指针分配给其他类型的指针。赋值是纯粹的复制操作,没有任何转换。这种用法是不可移植的,并且可能会产生在使用时导致寻址异常的指针。但是,可以保证将常量 0 分配给指针将产生一个与指向任何对象的指针不同的空指针。
Ste*_*mit 16
C 语言中没有什么比空指针更令人困惑的了。C FAQ 列表占据了整个部分来讨论该主题以及永远出现的无数误解。我们可以看到,这些误解永远不会消失,因为其中一些误解甚至在 2022 年的本线程中也被重复使用。
\n基本事实如下:
\n0。0还有其他用途,因此可能会出现歧义(更不用说混淆)。0作为空指针常量一直隐藏在预处理器宏后面NULL。NULL为了提供某种类型安全性并进一步减少错误,包含指针强制转换的宏定义很有吸引力。strbuf[len] = NULL;(明显但基本上是错误的尝试以空值终止字符串),以至于在某些圈子中相信不可能使用包括NULL显式强制转换在内的扩展来实际定义或假设的未来(或 C++ 中现有的) new 关键字nullptr。脚注(将此点称为 3\xc2\xbd):尽管在 C 源代码中将空指针 \xe2\x80\x94 表示为整数常量 0 \xe2\x80\x94,但空指针 \xe2\x80\x94 也可能具有内部值不全为0。每当讨论这个主题时,这一事实都会大大增加混乱,但它并没有从根本上改变定义。
\nDed*_*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 运算符的操作数的一部分。
C 最初是在空指针常量和整数常量0具有相同表示形式的机器上开发的。后来,一些供应商将该语言移植到大型机,其中不同的特殊值在用作指针时触发了硬件陷阱,并希望将该值用于NULL. 这些公司发现,现有的代码在整数和指针之间进行了类型双关,他们必须将其识别0为可以隐式转换为空指针常量的特殊常量。ANSI C 合并了这种行为,同时引入了作为void*隐式转换为任何类型的对象指针的指针。这可以NULL用作更安全的替代品0。
我\xe2\x80\x99见过一些代码(可能是半开玩笑的)通过测试检测到其中一台机器if ((char*)1 == 0)。
为什么存在两种表达同一概念的方式而不是一种?
历史。
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...
\n\n根据 C11 标准的 \xc2\xa76.3.2.3 \xc2\xb63,C 中的空指针常量可以由实现定义为整数常量表达式
\n0或此类表达式转换为void *。
不,这是对语言规范的误导性解释。引用段落的实际语言是
\n\n\n值为 0 的整数常量表达式,或此类表达式强制转换为类型
\nvoid *称为空指针常量。[...]
实现无法在这些替代方案之间进行选择。 两个都都是 C 语言中空指针常量的形式。为此目的,它们可以互换使用。
\n而且,不仅特定的整型常量表达式0可以起到这个作用,任何值为0的整型常量表达式都可以。例如,1 + 2 + 3 + 4 - 10就是这样的表达方式。
此外,不要将空指针常量与宏混淆NULL。后者是通过符合扩展为空指针常量的实现来定义的,但这并不意味着 的替换文本NULL是唯一的空指针常量。
\n\n我的实现(GCC 9.4.0)定义
\nNULL在stddef.h以下方式定义:Run Code Online (Sandbox Code Playgroud)\n#define NULL ((void *)0)\n#define NULL 0\n
当然,不能同时进行。
\n\n\n为什么上述两个表达式在 NULL 上下文中被认为在语义上是等价的?
\n
再次出现逆转。这不是“上下文NULL”。它是指针上下文。宏没有什么特别特别的地方NULL来区分它出现的上下文和其替换文本直接出现的上下文。
我猜您是在询问第 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如果我们接受值为 0 的整型常量表达式作为空指针常量(源代码实体),并且希望将其转换为运行时空指针值,那么我们选择哪种指针类型?指向不同对象类型的指针不一定具有相同的表示形式,因此这实际上很重要。对我来说,类型void *似乎是自然的选择,这与以下事实是一致的:在所有指针类型中,void *无需强制转换即可将其转换为其他对象指针类型。
但是,在 is0被解释为空指针常量的上下文中,将其强制转换为void *无操作,因此与在这样的上下文中(void *) 0表达的内容完全相同。0
在 ANSI 委员会工作时,许多现有的 C 实现接受整数到指针的转换而无需强制转换,尽管大多数此类转换的含义是特定于实现和/或上下文的,但人们广泛接受将常量转换为0指针产生一个空指针。这种用法是将整数常量转换为指针的最常见用法。委员会希望对类型转换施加更严格的规则,但又不想破坏所有使用的现有代码0表示空指针的常量的现有代码。
所以他们破解了规范。
\n他们发明了一种特殊的常量,即空指针常量,并提供了围绕它的规则,使其与现有用途兼容。无论词法形式如何,空指针常量都可以隐式转换为任何指针类型,从而生成该类型的空指针(值)。否则,不会定义隐式整数到指针的转换。
\n但委员会更倾向于空指针常量实际上应该具有无需转换的指针类型(无论0是否有指针上下文,都不会),因此他们提供了“强制转换为类型void *”选项作为空指针常量定义的一部分。当时,这是一个具有前瞻性的举措,但现在普遍的共识似乎是,这是一个正确的目标方向。
为什么我们仍然有“值为 0 的整数常量表达式”?向后兼容性。与传统习惯用法的一致性,例如{0}任何类型对象的通用初始值设定项。抵制变革。也许还有其他原因。
| 归档时间: |
|
| 查看次数: |
8438 次 |
| 最近记录: |