一致的C实现#define NULL可以是古怪的东西

时间:2010-04-08 10:52:24

标签: c null standards

我问的是因为this thread引发的讨论。

尝试在其他人的回复下使用评论进行严肃的来回讨论并不容易或有趣。所以我想听听我们的C专家的想法,一次不限制500个字符。

C标准对于NULL和空指针常量几乎没有什么可说的。我只能找到两个相关部分。第一:

  

3.2.2.3指针

     

与...的积分常量表达式   值0,或这样的表达式   强制转换为void *,称为null   指针常数。如果是空指针   常数被分配或比较   为了与指针相等,   常量转换为指针   那种类型。这样的指针,称为   空指针,保证比较   不等于指向任何对象的指针或   功能

和第二:

  

4.1.5通用定义

     

宏是

NULL
     

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

问题是,NULL可以扩展为实现定义的空指针常量,该常量与3.2.2.3中列举的那些不同吗?

特别是,它可以定义为:

#define NULL __builtin_magic_null_pointer

甚至:

#define NULL ((void*)-1)

我对3.2.2.3的解释是,它指定0的整数常量表达式和0转换为类型void *的整型常量表达式必须是实现识别的空指针常量的形式,但是它并不是一个详尽的清单。我相信实现可以自由地将其他源构造识别为空指针常量,只要没有其他规则被破坏。

例如,可以证明

#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中描述的“假设”规则。它是巨大的,有点迂回,所以我不会在这里粘贴它,但它实质上说,一个实现只需要产生与标准描述的抽象机器所需的相同的可观察副作用。它表示,只要程序的可观察副作用不被它们改变,任何优化,转换或编译器想要对程序执行的任何其他操作都是完全合法的。

因此,如果您希望证明NULL的特定定义不合法,您需要提出一个可以证明它的程序。任何一个像我的那个公然打破标准中的其他条款,或者可以合法地检测编译器必须做的任何魔法来使奇怪的NULL定义工作。

Steve Jessop找到了一个程序示例,用于检测NULL未被定义为3.2.2.3中两种形式的空指针常量之一,即将常量字符串化:

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

使用此宏,可以

puts(stringize(NULL));

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

谢谢!

6 个答案:

答案 0 :(得分:12)

在C99标准中,§7.17.3声明NULL“扩展为实现定义的空指针常量”。同时§6.3.2.3.3将空指针常量定义为“具有值0的整数常量表达式,或者这样的表达式转换为类型void *”。由于空指针常量没有其他定义,因此NULL的符合定义必须扩展为整数常量表达式,其值为零(或者此转换为void *)。

进一步引用C FAQ question 5.5(强调补充):

C标准的4.1.5节声明NULL“扩展为实现定义的空指针常量”,这意味着实现可以选择使用哪种形式的0以及是否使用`void *`cast;见问题5.6和5.7。这里的“实现定义”并不意味着NULL可能是#defined来匹配某些特定于实现的非零内部空指针值

这很有道理;因为标准要求指针上下文中的零整数常量编译到空指针(无论机器的内部表示是否具有零值),{{1}的情况}无论如何必须处理定义为零。程序员不需要输入NULL来获取空指针;它只是一种风格约定(可能有助于捕获错误,例如在非指针上下文中使用NULL定义为NULL时)。

编辑:这里混淆的一个原因似乎是标准使用的简洁语言,即它没有明确表示没有其他值可能被视为空指针常量。但是,当标准说“...被称为空指针常量”时,它意味着完全给定的定义被称为空指针常量。当(根据定义)标准定义了 符合的内容时,它不需要通过陈述不符合的内容来明确地遵循每个定义。

答案 1 :(得分:2)

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

引用文件是N1570 2011 ISO C标准草案。自1989年ANSI C标准(相当于1990 ISO C标准)以来,我不相信这个领域有任何重大变化。像" 7.19p3"是指第7.19小节第3段。(问题中的引用似乎是1989年ANSI标准,其中描述了第3节中的语言和第4节中的库; ISO标准的所有版本都描述了第6节中的语言和第7节中的图书馆。)

7.19p3要求宏NULL扩展为"实现定义的空指针常量"。

6.3.2.3p3说:

  

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

由于空指针常量以斜体显示,因此该术语的定义(3p1指定了该约定) - 这意味着其他任何指定的内容都没有。是一个空指针常量。 (标准并不总是严格遵循其定义的约定,但假设它在这种情况下也是如此。)

因此,如果我们有些古怪",我们需要看看什么是"整数常量表达式"。

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

"整数常量表达式,值为0"可以是许多事物中的任何一种(如果我们忽略容量限制,则无限多)。文字0是最明显的。其他可能性包括0x0000001-1'\0''-'-'-'。 (它不是100%明确的措辞是否"值0"特指{em>类型int 的值,但我认为共识是0L也是一个有效的空指针常量。)

另一个相关条款是6.6p10:

  

实现可以接受其他形式的常量表达式。

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

const int x = 0;

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

因此很明显0是一个空指针常量,并且它是NULL宏的有效定义。

同样明确的是(void*)0是一个空指针常量,但由于7.1,它的不是 NULL的有效定义。 2P5:

  

本节中描述的类对象宏的任何定义都应该   扩展到必要时受括号完全保护的代码,   这样它就可以将任意表达式分组,就好像它是一个单独的一样   标识符

如果NULL扩展为(void*)0,则表达式sizeof NULL将是语法错误。

那么((void*)0)呢?好吧,我99.9%确定它的意图NULL的有效定义,但6.5.1(描述括号表达式)说:

  

带括号的表达式是主表达式。它的类型和价值   与未表达的表达相同。它是一个   左值,函数指示符,或者如果是,则表示空值表达式   未表示的表达式分别是左值,一个函数   指示符,或无效表达。

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

如果空指针不是表示为全比特零,而是表示为某个其他比特模式,例如,一个等效于0xFFFFFFFF,则该怎么办?然后(void*)0xFFFFFFFF,即使它恰好评估为空指针也不是空指针常量,只是因为它不满足该术语的定义。

那么标准允许的其他变化是什么?

由于实现可以接受其他形式的常量表达式,因此编译器可以将__null定义为类型int的常量表达式,其值为0,允许__null((void*)__null)作为NULL的定义。它也可以使__null本身成为指针类型的常量,但它不能使用__null作为NULL的定义,因为它不是&{ #39; t满足6.3.2.3p3中的定义。

编译器可以完成同样的事情,没有编译器魔法,如下所示:

enum { __null };
#define NULL __null

此处__null是类型int的整数常量表达式,其值为0,因此可以在任何可以使用常量0的地方使用。

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

char c = NULL; /* PLEASE DON'T DO THIS */
如果NULL恰好被定义为0,则

完全合法;将NULL扩展为某些可识别的令牌(如__null)会使编译器更容易检测到这个有问题的构造。

答案 2 :(得分:1)

好吧,我找到了证明这一点的方法

#define NULL ((void*)-1)

不是NULL的合法定义。

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

使用NULL初始化函数指针是合法且正确的,而......

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

...是一种需要诊断的约束违规。所以那就是了。

__builtin_magic_null_pointer的{​​{1}}定义不会遇到这个问题。我还是想知道是否有人能想出不可能的原因。

答案 3 :(得分:1)

年龄较晚,但没有人提出这一点:假设实施确实选择使用

#define NULL __builtin_null

我对C99的解读是,只要特殊关键字__builtin_null表现为 - 如果它是“具有值0的整数常量表达式”或“具有值0的整数常量表达式,则转换为void *”。特别是,如果实现选择那些选项的前者,那么

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

是一个有效的翻译单元,将xy分别设置为整数值0和1。当然,如果它选择后者,则两者都是约束违规(分别为6.5.16.1,6.5.6; void *不是每个6.2.5p19的“指向对象类型的指针”; 6.7.8p11应用约束分配给初始化)。而且,如果没有为“误用”NULL提供更好的诊断,我也不会忘记为什么实现会这样做,所以看起来很可能会使选项无效更多代码。

答案 4 :(得分:0)

  

与...的积分常量表达式   值0,这样的表达式   强制转换为void *,称为a   空指针常量

     

NULL扩展为   实现定义的空指针   恒定

因此

NULL == 0

NULL ==(void *)0

答案 5 :(得分:-1)

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

NULL 扩展为0值表达式; AFAIK,它总是有。