可以符合C实现#define NULL是古怪的东西

我在问这个问题是因为在这个线程中引发的讨论。

尝试在别人的回复下使用评论进行认真的来回讨论并不容易或有趣。 所以我想听听C专家们的想法,而不是一次只限于500个字符。

C标准对于NULL和空指针常量有几句宝贵的意思。 我只能find两个相关的部分。 第一:

3.2.2.3指针

值为0的整型常量expression式,或者types为void *的强制转换expression式称为空指针常量。 如果将一个空指针常量赋值给一个指针,或者将其与一个指针进行比较,则该常量将转换为该types的指针。 这样一个叫做空指针的指针保证将不等于指针的任何对象或函数进行比较。

第二:

4.1.5通用定义

macros是

 NULL 

它扩展为一个实现定义的空指针常量;

问题是, NULL可以扩展到一个实现定义的空指针常量,它与3.2.2.3中枚举的不同。

特别是可以定义为:

 #define NULL __builtin_magic_null_pointer 

甚至:

 #define NULL ((void*)-1) 

我对3.2.2.3的理解是它指定了一个整型常量expression式0和一个types为void *的0型整型常量expression式必须是实现可识别的空指针常量的forms,但它不是本来是一个详尽的清单。 我相信,只要没有其他规则被破坏,实现可以自由地将其他源结构识别为空指针常量。

举个例子,这是可以certificate的

 #define NULL (-1) 

不是一个合法的定义,因为在

 if (NULL) do_stuff(); 

do_stuff()不能被调用,而用

 if (-1) do_stuff(); 

必须调用do_stuff() ; 因为它们是等价的,所以这不能成为NULL的合法定义。

但是标准说整数到指针的转换(反之亦然)是实现定义的,因此它可以定义将-1转换为指针,作为产生空指针的转换。 在这种情况下

 if ((void*)-1) 

会评价为假,一切都会好的。

那么其他人怎么看?

我要求大家特别记住2.1.2.3 Program execution描述的“如果”规则。 这是巨大的,有些迂回,所以我不会在这里粘贴它,但是它基本上说,一个实现只需要产生与标准描述的抽象机器所需的相同的可观察的副作用。 它表示,只要程序的可观察的副作用不被它们改变,任何优化,转换或编译器想要对程序执行的任何操作都是完全合法的。

所以如果你想certificate一个NULL的特定定义是不合法的,你需要想出一个可以certificate它的程序。 像我这样的人公然违反标准中的其他条款,或者可以合法地检测编译器为了使奇怪的NULL定义起作用而必须做的任何魔术。

Steve Jessopfind了一个程序的例子来检测NULL是否被定义为3.2.2.3中的两个空指针常量forms之一,它是对常量进行string化:

 #define stringize_helper(x) #x #define stringize(x) stringize_helper(x) 

使用这个macros,可以

 puts(stringize(NULL)); 

和“检测”NULL不扩展到3.2.2.3中的一种forms。 这足以使其他定义变得非法吗? 我只是不知道。

谢谢!

在C99标准中,§7.17.3指出NULL “扩展为实现定义的空指针常量 ”。 同时,第6.3.2.3.3节定义空指针常量为“一个整数常量expression式,值为0,或者这样的expression式强制types为void * ”。 由于空指针常量没有其他定义,所以NULL的一致性定义必须展开为值为零的整型常量expression式(或者将其转换为void * )。

进一步引用C常见问题5.5 (强调增加):

C标准的第4.1.5节指出NULL“扩展为一个实现定义的空指针常量”,这意味着实现可以select使用哪种forms的0以及是否使用`void *`cast; 见问题5.6和5.7。 这里的“实现定义” 并不意味着NULL可能被定义为匹配某个实现特定的非零内部空指针值

这很有道理; 由于标准要求在指针上下文中需要一个零整数常量来编译成一个空指针(不pipe机器的内部表示是否为零), NULL被定义为零的情况必须被处理。 程序员不需要inputNULL来获得空指针; 这只是一个风格的约定(并且可能有助于捕获错误,例如,当在非指针上下文中使用定义为(void *)0NULL )时。

编辑:这里混淆的一个来源似乎是标准使用的简洁语言,即它没有明确说没有其他值可能被认为是一个空指针常量。 但是,当标准说“…被称为空指针常量”时,这意味着给定的定义被称为空指针常量。 当(根据定义)标准定义什么符合时,它不需要明确地遵循每个定义。

这扩大了一些其他的答案,并提出了一些其他人错过的观点。

引用N1570是2011 ISO C标准草案。 自1989年的ANSI C标准(相当于1990年的ISO C标准)以来,我不相信这方面有任何重大的变化。 像“7.19p3”这样的参考文献是指第7.19条第3款。(这个问题的引用似乎是1989年的ANSI标准,它描述了第3节中的语言和第4节中的库;所有版本的ISO标准在第6节描述语言,在第7节描述图书馆)

7.19p3要求macrosNULL扩展为“一个实现定义的空指针常量”。

6.3.2.3p3说:

值为0的整数常量expression式,或者转换为void *types的expression式称为空指针常量

由于空指针常量用斜体表示,这就是术语的定义(3p1指定了约定) – 这意味着除了指定的内容之外,其余的内容都可以是空指针常量。 (这个标准并不总是严格遵循这个约定的定义,但是假设在这种情况下这样做是没有问题的。)

所以如果我们“古怪的”,我们需要看看什么可以是一个“整数常量expression式”。

短语空指针常量需要被视为一个单词,而不是一个其含义取决于其构成单词的短语。 特别是,整数常量0是一个空指针常量,无论它出现的上下文如何; 它不需要导致一个空指针 ,它的types是int ,而不是任何指针types。

“值为0的整数常量expression式”可以是许多事情中的任何一种(如果我们忽略容量限制,则是无限多的)。 字面量0是最明显的。 其他的可能性是'-'-'-''\0''-'-'-' 。 (从措词上来看,“0值”是不是特别指向inttypes的值,但我认为0L也是一个有效的空指针常量。)

另一个相关条款是6.6p10:

一个实现可能接受其他forms的常量expression式。

对于我而言,这并不完全清楚,这是否意味着允许多大的范围。 例如,编译器可能支持二进制文字作为扩展; 那么0b0将是一个有效的空指针常量。 它也可能允许C ++风格的引用const对象,所以给定

 const int x = 0; 

x的引用可能是一个常量expression式(它不在标准C中)。

所以很明显, 0是一个空指针常量,而且它是NULLmacros的有效定义。

同样清楚的是(void*)0是一个空指针常量,但是它不是 NULL的有效定义,因为7.1.2p5:

在本节中描述的任何类似macros的定义应该扩展为必要时由括号完全保护的代码,以便将它作为任意expression式进行分组,就好像它是单个标识符一样。

如果NULL扩展为(void*)0 ,那么expression式sizeof NULL将是一个语法错误。

那么((void*)0)呢? 那么,我99.9%肯定它的目的是作为NULL的有效定义,但描述括号expression式的6.5.1说:

括号expression式是主要expression式。 它的types和价值与那些没有亲和力的expression相同。 它是一个左值,函数指示符,或者一个voidexpression式,如果这个未expression式分别是一个左值,函数指示符或者一个voidexpression式。

并不是说一个带括号的空指针常量是一个空指针常量。 尽pipe如此,据我所知,所有的C编译器合理地假定一个带括号的空指针常量是一个空指针常量,使((void*)0NULL的有效定义。

如果空指针不是以全零位表示,而是以某种其他位模式表示,例如相当于0xFFFFFFFF 。 然后(void*)0xFFFFFFFF ,即使它碰巧评估为空指针也不是空指针常量,只是因为它不满足该术语的定义。

那么标准允许哪些其他变化呢?

由于实现可以接受其他forms的常量expression式,因此编译器可以将__null定义为types为int的常量expression式,值为0 ,允许__null((void*)__null)作为NULL的定义。 它可以使__null本身成为指针types的常量,但是它不能将__null用作NULL的定义,因为它不符合6.3.2.3p3中的定义。

一个编译器可以完成同样的事情,没有编译器的魔力,就像这样:

 enum { __null }; #define NULL __null 

这里__nullinttypes的整型常量expression式,值为0 ,所以它可以在任何可以使用常量0地方使用。

用像__null这样的符号定义NULL的优点是,如果在非指针常量中使用了NULL ,编译器就可以发出一个(可能是可选的)警告。 例如,这个:

 char c = NULL; /* PLEASE DON'T DO THIS */ 

如果NULL被定义为0 ,则是完全合法的; 将NULL扩展为一些可识别的标记(如__null会使编译器更容易检测到这个可疑的结构。

那么,我find了一个方法来certificate这一点

 #define NULL ((void*)-1) 

不是NULL的合法定义。

 int main(void) { void (*fp)() = NULL; } 

用NULL初始化一个函数指针是合法和正确的,然而…

 int main(void) { void (*fp)() = (void*)-1; } 

…是需要诊断的约束违规。 所以这样了。

NULL__builtin_magic_null_pointer定义不会遇到这个问题。 我还想知道是否有人能够想出一个不可能的原因。

年后,但没有人提出这一点:假设实际上确实select使用

 #define NULL __builtin_null 

我读的C99就是这样,只要特殊关键字__builtin_null行为如果它是“值为0的整型常量expression式”或“值为0的整型常量expression式,转换为void * ”,就可以。 特别是,如果实现select这些选项的前者,那么

 int x = __builtin_null; int y = __builtin_null + 1; 

是一个有效的翻译单元,将xy分别设置为整数值0和1。 如果select后者,当然,两者都是约束违规(分别为6.5.16.1,6.5.6; void *不是根据6.2.5p19的“指向对象types的指针”; 6.7.8p11将分配约束用于初始化)。 而且我也不会直接明白为什么一个实现可以做到这一点,如果不是为了提供更好的诊断信息来“滥用”NULL,所以看起来可能会失败更多的代码。

值为0的整型常量expression式, 或者types为void *的强制转换expression式称为空指针常量

NULL,扩展为实现定义的空指针常量 ;

因此也是

NULL == 0

要么

NULL ==(void *)0

空指针常量 必须计算0,否则像!ptr这样的expression式将无法按预期工作。

NULL macros展开为0值expression式; AFAIK,它总是有。