我知道C编译器不需要使用全零来表示NULL
的位,但是标准要求它们在布尔上下文/比较中NULL
计算为false。 因此,下面程序中的第二个printf
将始终输出false
。
但我想知道的是:在NULL
*不是*全为零的系统上,*是*所有零的指针值在布尔上下文/比较中也会计算为假吗? 换句话说,下面程序中的第一个printf
会输出true
吗?
或者以稍微不同的方式询问:我可以依靠calloc
来生成一个指针值,该值在布尔上下文/比较中始终计算为 false? 这个问题的第一个答案使用memset
来清除名为y
的long*
的位,然后继续说y==0
是UB,y
因为它可能是"陷阱表示"(不管是什么)。calloc
也只是清理位,所以也许第一printf
o->p
也是 UB?
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef struct { void * p; } obj;
int main() {
obj * o = calloc(sizeof(obj), 1);
assert(o); // assume successful allocation
printf("%sn", o->p ? "true" : "false"); // 1st: could print "true"? Is o->p UB?
o->p = NULL;
printf("%sn", o->p ? "true" : "false"); // 2nd: always prints "false"
return 0;
}
typedef struct { void * p; } obj;
obj * o = calloc(sizeof(obj), 1);
assert(o); // Let us set aside the case of a failed allocation
printf("%sn", o->p ? "true" : "false"); // 1st: could print "true" ?
我可以依靠
calloc
来生成一个指针值,该值在布尔上下文/比较中始终计算为 false?
否 - 输出可以"true"
.*1.
作为指针,所有零的位模式可能不是空指针。
7.22.3.2calloc 函数
2 calloc 函数为nmemb
对象数组分配空间,每个对象的大小都是大小。空格初始化为所有位零.301)
脚注 301) 请注意,这不必与浮点零或空指针常量的表示形式相同。
示例:一个实现可能只有一个空指针编码,其位模式为全部 1。(void *)0
将全零位模式int 0
转换为全 1void *
。if (null_pointer)
始终为 false,无论空指针的位模式如何。
*1但实际上是的,输出总是"false"
。 如今,不使用所有零位模式作为空指针的实现并不常见。 高度可移植的代码不会假设这种实用性。考虑一个旧的或新的新系统可能使用零位模式作为非空指针- 并且可悲地破坏了许多假设全零位模式是空指针的代码库。
背景信息
考虑以下使用表达式逻辑值的地方,全部取自 C18,我强调的是粗斜体:
-
6.3.1.2(布尔类型)p1:当任何标量值转换为
_Bool
时,如果值比较等于0,则结果为0;否则,结果为1。 -
6.5.3.3 (一元算术运算符) p5:逻辑否定运算符
!
的结果是0,如果其操作数的值比较不等于0,则为1,如果其操作数的值等于0。结果的类型为int
。表达式!E
等效于(0==E)
。 -
6.5.13 (逻辑 AND 运算符) p3:如果
&&
运算符的两个操作数都比较不等于0,则其结果为 1;否则,则生成 0。结果的类型为int
。 -
6.5.14 (逻辑 OR 运算符) p3:如果
||
运算符中的任何一个操作数比较不等于0,则其应生成 1;否则,它产生 0。结果的类型为int
。 -
6.5.15 (条件运算符) p4:计算第一个操作数;其计算值与第二个或第三个操作数的计算(以计算者为准)之间有一个序列点。仅当第一个操作数比较不等于 0时,才计算第二个操作数;仅当第一个操作数等于 0时,才计算第三个操作数;结果是第二个或第三个操作数(以计算者为准)的值,转换为下面描述的类型。
-
6.8.4.1 (
if
语句) p2:在这两种形式中,如果表达式比较不等于 0,则执行第一个子语句。在else
形式中,如果表达式比较等于 0,则执行第二个子语句。如果通过标签到达第一个子语句,则不执行第二个子语句。 -
6.8.5(迭代语句) p4:迭代语句会导致称为循环体的语句重复执行,直到控制表达式的比较等于 0。无论循环是否循环,都会发生重复 主体从迭代语句或通过跳转输入。
"E 比较等于 0"等价于 C 表达式(E == 0)
,"E 比较不等于 0"等价于 C 表达式(E != 0)
。 相等运算符的约束由下式给出:
- 6.5.9 (相等运算符) p2:下列条件之一应成立:
- 两个操作数都具有算术类型;
- 这两个操作数都是指向兼容类型的限定或非限定版本的指针;
- 一个操作数是指向对象类型的指针,另一个是指向限定或非限定版本的指针
void
;或 - 一个操作数是指针,另一个是空指针常量。
关于相等运算符的语义,其中至少有一个操作数是指针:
6.5.9(相等运算符) p5:否则,至少有一个操作数是指针。如果一个操作数是指针,另一个是空指针常量,则空指针常量将转换为指针的类型。如果一个操作数是 指向对象类型的指针,另一个是指向
void
的限定或非限定版本的指针,前者转换为后者的类型。p6:两个指针比较相等,当且仅当两者都是空指针,都是指向同一对象的指针(包括指向对象的指针和开头的子对象)或函数,两者都是指向一个经过同一数组对象的最后一个元素的指针,或者一个是指向一个数组对象末尾的指针,另一个是指向不同数组开头的指针碰巧紧跟在地址空间中第一个数组对象之后的对象。
关于空指针常量:
- 6.3.2.3(指针)p3:值为 0 的整数常量表达式或转换为类型
void *
的此类表达式称为空指针常量67)。如果将 null 指针常量转换为指针类型,则生成的指针(称为null指针)保证与指向任何对象或函数的指针不相等进行比较。
OP的问题
但我想知道的是:在
NULL
不全为零的系统上,全为零的指针值在布尔上下文/比较中也会计算为 false 吗?
旁白:NULL
是一个空指针常量,不一定是空指针(请参阅上面的 6.3.2.3p3,其中它可以是一个整数常量表达式)。您真正指的是一个空指针的位表示不全为零的系统。
注意:正如 Eric Postpischil 在下面的评论中指出的那样,一个系统可以有多个空指针值的位表示,因此我们假设它们都不是这个问题的全零位表示。
为了使指针值在布尔上下文/比较中的计算结果为 false,它必须比较不等于 0。 在此上下文中,它必须将不等于与空指针常量进行比较。 通过上面的 6.5.9p5,空指针常量将转换为要与之进行比较的指针的类型。通过上述 6.5.9p6,空指针值不会与非空指针值进行比较。因此,在空指针值并非全部为零的系统上,所有位为零的非空指针值在布尔上下文中的计算结果为 true。
或者以稍微不同的方式询问:我可以依靠
calloc
来生成一个在布尔上下文/比较中始终计算为 false 的指针值吗?
不可以,您不能依赖calloc
(或字节值为 0 的memset
)来生成在布尔上下文中计算结果为 false 的指针值。如果具有全零位表示形式的指针值不是空指针值,则在布尔上下文中,其计算结果将为 true。
在这个问题的第一个答案中,对 NULL 和 0 进行了很好的讨论:NULL、'\0' 和 0 有什么区别?
这个答案的要点是:
请注意,在 C 语言中什么是空指针。没关系 在底层架构上。如果底层架构具有 定义为地址0xDEADBEEF的空指针值,则由 编译器来解决这个问题。
。即使在这个有趣的架构上,以下方法仍然有效 检查空指针的方法:
if (!pointer)
if (pointer == NULL)
if (pointer == 0)
在同一个问题的第二个答案中...
值为 0 的 int 类型的常量表达式或表达式 对于此类型,强制转换为类型 void * 是一个空指针常量,如果 转换为指针变为空指针。它由 标准,用于比较不等于指向任何对象或函数的任何指针。
(简短回答,是的,您可以使用if (!ptr)
检查 NULL 指针)。
核心答案
但我想知道的是:在
NULL
*不是*全为零的系统上,*是*所有零的指针值在布尔上下文/比较中也会计算为false吗?
在 C 实现中,C 标准允许以下任何一项:
- 所有位零是空指针,没有其他位模式。 所有位零是空指针
- ,一个或多个其他位模式是空指针。
- 所有位零不是空指针,一个或多个其他位模式是空指针。
换句话说,C 实现可以将任何一个或多个位模式指定为 null 指针,这可能包括也可能不包括所有位零。(如果 C 实现确实允许多个位模式为空指针,则必须确保它们比较相等。
。下面程序中的第一个
printf
会输出true
吗?
允许它打印"true";calloc
的结果是所有位为零的内存,将该内存解释为void *
可能会导致指针值不是空指针值。
补充
。其中
NULL
*不是*全是零...
NULL
只是源代码中的东西。它要么是0
,要么是((void *) 0)
或等效的。无论它在源代码中用作指针(也就是说,您正在执行像if (pointer != NULL)
这样的正常操作,而不是像int x = 3 + NULL;
那样的笨拙),编译器都会有效地将其转换为空指针。也就是说,如果 all bits-zero 在 C 实现中不是空指针,编译器将编译pointer != NULL
,以便将pointer
与表示空指针的某个位模式进行比较。
所以你的问题都是关于空指针的;它们不是关于NULL
的。
。在以下系统上...
什么是空指针的最终确定取决于 C 实现,而不是它执行的系统。C 实现可以以它想要的任何方式表示指针,并在指令中使用机器地址时根据需要转换它们。
您可以通过显式和防御性的编码风格来避免此类问题。
如果你有一个指针_p,写下这样的构造
(_p==NULL)?(A):(B)
现在任何读者都会立即知道,您的意图是检查 _p 是否等于 NULL,即使在 NULL 可能与整数值 0 不同的机器上,编译器也会自动正确执行此操作。此外,静态代码检查器现在不会因为依赖隐式行为而警告您。
(_p)?(A):(B)
只是做得不对
但除此之外,这是一个有趣的技术问题。
2019 年或 2020 年C++委员会的一个有趣的谈话显示,即使是这些人也考虑放弃对一些奇怪的未定义行为的兼容性,这在 1970 年之前对于一些 3-4 架构来说是需要的。在过去的几十年里,没有任何已知的使用过这种东西——至少据我所知。正如对您的问题的第一条评论所述:您几乎找不到任何有此类问题的机器——至少在博物馆之外。