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

zc0qhyus  于 2022-12-22  发布在  其他
关注(0)|答案(6)|浏览(205)

根据C11标准§6.3.2.3 ¶3,C语言中的空指针常量可以由一个实现定义为整型常量表达式0或这样的表达式转换为void *
我的实现(GCC 9.4.0)以下列方式在stddef.h中定义NULL

#define NULL ((void *)0)
#define NULL 0

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

cclgggtu

cclgggtu1#

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

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

我们希望 f 编译时没有警告,而希望 g 产生错误或警告(因为int变量 x 与指针进行了比较)。
在C语言中,#define NULL ((void*)0)提供了这两种功能(GCC警告 g,干净编译 f)。
但是,在C中,#define NULL ((void*)0)会导致 f 的编译错误。因此,要使其在C中编译,<stddef.h> 具有仅用于C#define NULL 0(不适用于C)。不幸的是,这也阻止了 g 的警告报告。为了解决这个问题,C11使用内置的nullptr代替NULL,并且,C++编译器会报告 g 错误,并且会干净地编译 f

but5z9lq

but5z9lq2#

((void *)0)具有更强的类型化,并且可以导致更好的编译器或静态分析器诊断。例如,由于指针和普通整数之间的隐式转换在标准C中是不允许的。
0可能是由于历史原因而被允许的,在标准之前的时间,C中的一切几乎都是整数,指针和整数之间的野生隐式转换是允许的,尽管可能会导致未定义的行为。
古老的K&R第1版提供了一些见解(7.14赋值运算符):
编译器当前允许将指针赋给整数、将整数赋给指针以及将指针赋给另一种类型的指针。赋值是纯复制操作,没有转换。这种用法是不可移植的,并且可能产生在使用时导致寻址异常的指针。但是,保证将常数0分配给指针将产生可与指向任何对象的指针区分开的空指针。

xqk2d5yq

xqk2d5yq3#

在C语言中没有两种表达NULL的方法,它是一个4字符的标记。
但是NULL有无数种方式可以由实现定义,即使我们将自己限制在标准C语言中,而实现也不一定非要这样做。
在这些方法中,只有值为0的整型常量在C++中也是 * 空指针常量 *。
这种变化的原因是历史和先例(每个人都有不同的做法,void*姗姗来迟,现有的代码/实现胜过一切),并通过向后兼容性来加强。

6.3.2.3 Pointers

[...]值为0的整型常量表达式或转换为void *类型的表达式称为 * 空指针常量 *。
67)如果一个空指针常量被转换为指针类型,那么得到的指针,称为 * null pointer *,保证与指向任何对象或函数的指针相比都是不相等的。

6.6常量表达式

[...]* * 说明**
2常量表达式可以在翻译期间而不是运行时求值,因此可以在常量所在的任何地方使用。

    • 约束**3常量表达式不应包含赋值、递增、递减、函数调用或逗号运算符,除非它们包含在未求值的子表达式中。117)

4每个常量表达式的计算结果应是在其类型的可表示值范围内的常量。

    • 语义**

5在几个上下文中需要计算为常量的表达式。如果在翻译环境中计算浮点表达式,则算术范围和精度应至少与在执行环境中计算表达式时一样大。118)
6一个整型常量表达式119)应该是整型的,并且只能有整型常量、枚举常量、字符常量、结果是整型常量的sizeof表达式、_Alignof表达式以及作为强制转换的立即数操作数的浮点常量。
整型常量表达式中的强制转换运算符只能将算术类型转换为整型类型,但作为sizeof或_Alignof运算符的操作数的一部分时除外。

f0brbegy

f0brbegy4#

在C语言中,没有什么比空指针更让人困惑的了。C FAQ list用了一个entire section来描述这个主题,以及无数永远存在的误解。我们可以看到,这些误解永远不会消失,因为其中一些误解甚至在2022年的这个线程中也会被循环使用。
基本事实如下:

  1. C有一个 * 空指针 * 的概念,一个明确地不指向任何地方的特殊指针值。
    1.请求空指针的源代码结构--一个 * 空指针常量 *--基本上涉及到标记0
    1.因为标记0还有其他用途,所以可能会产生歧义(更不用说混淆了)。
    1.为了帮助减少混淆和歧义,多年来,标记0作为空指针常量一直隐藏在预处理器宏NULL后面。
    1.为了提供某种类型安全性并进一步减少错误,让NULL的宏定义包含一个指针强制转换是很有吸引力的。
    1.然而,最不幸的是,在这一过程中出现了足够多的混乱,以至于几乎不可能适当地减轻这一切。有很多现存的代码都是strbuf[len] = NULL;这样的(这是一个明显但基本上错误的以空结尾字符串的尝试)在某些圈子中,人们认为实际上不可能用包括显式投射或假设未来的展开来定义NULL(或C++中的现有)新关键字nullptr
    另请参见Why not call nullptr NULL?
    脚注(称之为第三点半):空指针也有可能--尽管在C源代码中被表示为整数常量0--有一个内部值是 * not * all-bits-0。这一事实大大增加了讨论这个主题时的困惑,但它并没有从根本上改变定义。
5vf7fwbs

5vf7fwbs5#

C最初是在空指针常量和整型常量0具有相同表示的机器上开发的。后来,一些供应商将该语言移植到大型机上,在大型机上,当用作指针时,不同的特殊值会触发硬件陷阱,并希望将该值用于NULL。这些公司发现,如此多的现有代码在整型和指针之间使用类型双关语,他们必须将0识别为可以隐式转换为空指针常量的特殊常量。ANSI C在引入void*作为可以隐式转换为任何类型的对象指针的指针的同时纳入了这一行为。这使得NULL可以用作0的更安全的替代品。
我看到过一些代码(可能是开玩笑的)通过测试if ((char*)1 == 0)检测到其中一台机器。

zi8p0yeb

zi8p0yeb6#

根据C11标准§6.3.2.3 ¶3,C语言中的空指针常量可以通过实现定义为整型常量表达式0或这样的表达式强制转换为void *
不,这是对语言规范的误导性解释。引用段落的实际语言是
值为0的整型常量表达式或转换为void *类型的表达式称为空指针常量。
实现不能在这两种选择中做出选择。Both 都是C语言中的空指针常量的形式。它们可以互换使用。
此外,不仅特定的整数常量表达式0可以充当该角色,值为0的 * 任何 * 整数常量表达式都可以充当该角色,例如1 + 2 + 3 + 4 - 10就是这样的表达式。
另外,不要把空指针常量和宏NULL混淆,后者是由一致性实现定义的,可以扩展为空指针常量,但这并不意味着NULL的替换文本是 * 唯一 * 的空指针常量。
我的实现(GCC 9.4.0)以下列方式在stddef.h中定义NULL

#define NULL ((void *)0)
#define NULL 0

当然,不是同时发生。
为什么在NULL的上下文中,上述两个表达式被认为是语义等价的?
再次颠倒一下。它不是“NULL的上下文“。它是 pointer context。宏NULL本身没有什么特别的地方来区分它出现的上下文和它的替换文本直接出现的上下文。
我猜你是在问段落www.example.com的 * 理由 *6.3.2.3/3,而不是“因为6.3.2.3/3”。C11没有公开的理由。有one for C99,它也主要用于C90,但它没有解决这个问题。
然而,需要注意的是,void(也就是void *)是开发原始C语言规范(“ANSI C”/ C89 / C90)的委员会的发明,在此之前不可能有“整型常量表达式转换为void *类型“。
更具体地说,为什么同一个概念有两种表达方式,而不是一种?
真的吗
如果我们接受值为0的整型常量表达式作为空指针常量(一个源代码实体),我们想把它转换成一个运行时空指针 value,那么我们选择哪种指针类型呢?指向不同对象类型的指针不一定有相同的表示,所以这一点很重要。类型void *对我来说似乎是自然的选择,这与以下事实一致:在所有指针类型中,void *可单独转换为其它对象指针类型而无需强制转换。
但是,在0被解释为空指针常量的上下文中,将其强制转换为void *是无效的,因此(void *) 0在这种上下文中表达的内容与0完全相同。

这里到底发生了什么

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

所以他们黑进了规范

他们发明了一种特殊的常量--空指针常量,并提供了使其与现有用法兼容的规则。空指针常量,无论词法形式如何,都可以隐式转换为任何指针类型,生成该类型的空指针(值)。否则,没有定义隐式整数到指针转换。
但是委员会倾向于空指针常量实际上应该具有指针类型 * 而不进行 * 转换(0没有,指针上下文或没有),所以他们提供了“cast to type void *“选项作为空指针常量定义的一部分。但现在的普遍共识似乎是,这是正确的目标方向。
为什么我们仍然有“值为0的整型常量表达式”?向后兼容性。与传统习惯用法(如{0})的一致性,作为任何类型对象的通用初始化器。对更改的抵抗。也许还有其他原因。

相关问题