【问题标题】:When NULL is not all-zero-bits, is an all-zero-bit pointer value also 'false'?当 NULL 不是全零位时,全零位指针值是否也是“假”?
【发布时间】:2020-12-15 23:42:19
【问题描述】:

我知道 C 编译器不需要对 NULL 的位表示使用全零,但它们*是*标准要求使 NULL 在布尔值中计算为 false上下文/比较。因此下面程序中的2nd printf 将始终输出false

但我想知道的是:在 NULL*not* 全零的系统上,是否会有一个指针值 *is* 全零也计算在布尔上下文/比较中为假?换句话说,下面程序中的1stprintf会输出true吗?

或者以稍微不同的方式问:我可以依靠calloc 生成一个在布尔上下文/比较中总是评估为假的指针值吗? this 问题的第一个答案使用memset 清除名为ylong* 的位,然后继续说y==0 是UB,因为y 可能是“陷阱表示”(无论那是)。 calloc 也只是清除位,所以可能 1st 中的 o->p printf 也是 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("%s\n", o->p ? "true" : "false");  // 1st: could print "true"?  Is o->p UB?
    o->p = NULL;
    printf("%s\n", o->p ? "true" : "false");  // 2nd: always prints "false"
    return 0;
}

【问题讨论】:

  • 您是否知道空指针并非全为零的架构?我想我从未见过,其中包括像分段 16 位 x86 这样的怪人。
  • 请注意,C 中的指针不一定是真正的数字,它只是在某些情况下表现得像一个(你可以做算术等)。这就是为什么 C 也没有为 printf 定义 %p 的原因,因为不需要“指针”的一致表示。 FWIW,指针实际上可以是指向某个“对象”的箭头,C 在这方面非常抽象。因此,询问“位表示”对于严格阅读标准通常没有真正意义。
  • @textral 你如何从这里总结出程序员的意图?我只能说程序员的意图是在那里写一个全为零的值。如果意图是有一个空指针,我不知道。有疑问,我不得不假设程序员知道他的目标平台,知道什么是全零模式,什么不是。
  • @MarkRansom 我为 CDC Cyber​​ 180 系列开发了一个 C 编译器。这些机器旨在运行类似于 Multics 的操作系统,因此它的 48 位指针包括一个 4 位环号。只有在环 0 中运行的代码才能创建将环号设置为 0 的指针。因此,我们有不全为 0 的空指针。如果在分支中使用了指针,我们会将其移至整数寄存器并屏蔽掉环号,然后再对其进行全零测试;第二个 println 语句将打印“false”。第一个 println 访问一个未初始化的位置,因此是未定义的行为。
  • @TheodoreNorvell 感谢您提供的具体示例。我在 CDC Cyber​​ 6400 上学习了汇编程序,当然它的工作方式完全不同——地址寄存器只有 18 位,并且没有环的概念。每个进程都有自己的地址空间,监控功能由一组单独的外围处理器处理。

标签: c pointers null language-lawyer undefined-behavior


【解决方案1】:
typedef struct { void * p; } obj;
obj * o = calloc(sizeof(obj), 1);
assert(o);  // Let us set aside the case of a failed allocation
printf("%s\n", o->p ? "true" : "false");  // 1st: could print "true" ?

我可以依靠calloc 生成一个在布尔上下文/比较中总是评估为假的指针值吗?

否 - 输出可能是 "true".*1.

全零位模式,作为指针,可能不是空指针

7.22.3.2 calloc 函数
2 calloc 函数为nmemb 对象的数组分配空间,每个对象的大小都是size。空间初始化为所有位为零。301)
脚注 301)请注意,这不必与浮点零或空指针常量的表示相同。


示例:一个实现可能只有一个空指针编码,其位模式为全1。 (void *)0 将全零位模式 int 0 转换为全一 void *if (null_pointer) 始终为假,与 空指针 的位模式无关。


*1 实际上是的,输出总是"false"。如今,不使用全零位模式作为空指针的实现并不常见。高度可移植的代码不会假设这种实用性。考虑一个旧的或新的新系统可能使用零位模式作为非空指针 - 遗憾地破坏了许多假设全零位模式是空指针的代码库em>。

【讨论】:

  • 感谢@chux,这证明了我在使用此代码时的不安(尽管正如您所说,实际上没有任何实现方式以这种不直观的方式运行)。我在我的问题中链接的另一个答案是否正确:甚至引用这样的值是否被视为 UB?
  • 另外,@Howlium 链接到的社区答案是否错误?还是我只是误读了它? (我需要更仔细地阅读它......也许他们从不提到位级别的初始化。)
  • 在第二个代码片段中,6.3.2.3 3 不相关。表达式o-&gt;i 不是整数常量表达式
  • @chux-ReinstateMonica:实现可以指定取消引用空指针的行为——有时很有用。在某些摩托罗拉/飞思卡尔微控制器的典型编译器上,端口 A 的控制寄存器位于地址 0,尝试将 0x42 存储到 (unsigned char)0` 会将端口 A 控制寄存器设置为 0x42,这将是设置该寄存器值的正常方式(如果硬件忽略写入 0xFFFF,*((unsigned short*)0xFFFF) = 0x42; 可能会起作用,但这似乎比写入文字地址 zeor 更麻烦。
  • 对不起@chux 的长时间停顿。回到我们的对话(在 cmets 中):所以如果 memset 写入的全零位模式恰好是根据 C 实现的 空指针 值,那么引用 y 并比较它到 0 是明确定义的。但是代码可能会在 C impl 只使用非零位 null 指针 的系统上找到它的方式,在这种情况下 y==0 正在比较 non-null ptr为零,因此 0 不会转换为指针类型,因此比较是 UB。我理解正确吗?
【解决方案2】:

背景资料

考虑以下使用表达式的逻辑值的地方,全部取自 C18,我的重点是粗斜体

  • 6.3.1.2(布尔型)p1:当任何标量值转换为_Bool时,如果值比较等于0,结果为0;否则,结果为 1。

  • 6.5.3.3(一元算术运算符)p5:如果其操作数的值比较不等于0,则逻辑否定运算符!的结果为0,如果其操作数的值比较等于 0,则为 1。结果类型为int。表达式!E 等价于(0==E)

  • 6.5.13(逻辑与运算符)p3:如果&amp;&amp; 运算符的两个操作数比较不等于0,则该运算符将产生1;否则,它产生 0。结果的类型为 int

  • 6.5.14(逻辑或运算符)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)。如果将空指针常量转换为指针类型,则生成的指针(称为 空指针)保证与指向任何对象或函数的指针不相等。

OP 的问题

但我想知道的是:在NULL 不是全为零的系统上,is全零的指针值是否也会在布尔值中计算为 false上下文/比较?

旁白:NULL 是一个 空指针常量,不一定是 空指针(参见上面的 6.3.2.3p3,它可以是一个整数常量表达式)。你真正的意思是一个空指针的位表示不全为零的系统。

注意:正如 Eric Postpischil 在下面的 cmets 中指出的,一个系统可能有多个空指针值的位表示,因此我们假设它们都不是全零位表示问题。

为了使指针值在布尔上下文/比较中评估为 false,它必须比较不等于 0。在这种情况下,它必须比较不等于空指针常量。通过上面的 6.5.9p5,空指针常量将被转换为与之比较的指针的类型。通过上面的 6.5.9p6,空指针值将不等于非空指针值。因此,在空指针值并非所有位为零的系统上,所有位为零的非空指针值将在布尔上下文中评估为真。

或者以稍微不同的方式问:我可以依靠calloc 生成一个在布尔上下文/比较中总是评估为假的指针值吗?

不,您不能依赖 calloc(或字节值为 0 的 memset)来生成在布尔上下文中评估为 false 的指针值。如果具有全零位表示的指针值不是空指针值,它将在布尔上下文中评估为真。

【讨论】:

  • “如果空指针具有非全零位表示,则具有全零位表示的指针值将在布尔上下文中评估为真”不是有效的推论。一个实现可能有多个构成空指针的位模式,包括一个全为零和一些不是。这个答案(以及这里的其他答案)在这方面不清楚。
  • @EricPostpischil 好的,我应该将其更改为具有全零位表示的非空指针值将在布尔上下文中评估为 true。
【解决方案3】:

在这个问题的第一个答案中有一个关于 NULL 和 0 的精彩讨论:What is the difference between NULL, '\0' and 0?

该答案的妙语是:

注意什么是 C 语言中的空指针。没关系 关于底层架构。如果底层架构有一个 空指针值定义为地址 0xDEADBEEF,那么它取决于 编译器来解决这个问题。

…即使在这个有趣的架构上,以下方法仍然有效 检查空指针的方法:

if (!pointer)
if (pointer == NULL)
if (pointer == 0)

在同一个问题的第二个答案中……

值为 0 的 int 类型常量表达式,或表达式 对于这种类型,强制转换为 void * 类型是一个空指针常量,如果 转换为指针变成空指针。它是由 比较不等于指向任何对象或函数的任何指针的标准。

(简短回答,是的,您可以使用if (!ptr) 检查空指针)。

【讨论】:

  • 但这并不能回答关键问题:下面程序中的第一个 printf 会输出 true 吗?
  • 等一下@Howlium,我找到了另一个让我感到困惑的答案 (stackoverflow.com/questions/21386995/is-int-0-a-null-pointer)。第一个答案使用 memset 将 long* (y) 的位设置为 0,然后继续说 y == 0 是 UB,因为 y 可能是陷阱表示。 calloc 就像 memset 在那个答案中一样清除位,那为什么有什么不同呢?
  • @M.NejatAydin 我想不是。编译器知道结构的数据类型并会采取相应的行动。
  • @stderr 我不相信。赋值obj *o = calloc(sizeof(obj), 1) 将指针o-&gt;p 的所有位设置为零。但该对象表示(所有位为零)不必表示空指针。
  • @M.NejatAydin 不,你是对的。请参阅提供的答案chux。 C89 也是这么说的。脚注 127:请注意,这不必与浮点零或空指针的表示相同。
【解决方案4】:

核心答案

但我想知道的是:在NULL*not* 全为零的系统上,*is* 全为零的指针值也会计算在布尔上下文/比较中为 false?

在 C 实现中,C 标准允许:

  • 所有位为零是空指针,没有其他位模式。
  • 所有位为零是空指针,而一个或多个其他位模式是。
  • All-bits-zero 不是空指针,一个或多个其他位模式是。

换句话说,C 实现可以将任何一个或多个位模式指定为空指针,这可能包括也可能不包括所有位为零。 (如果 C 实现确实允许多个位模式为空指针,则必须确保它们比较相等。)

…下面程序中的1stprintf会输出true吗?

允许打印“true”; calloc 的结果是所有位为零的内存,并且将该内存解释为 void * 可能会导致指针值不是空指针值。

补充

...其中NULL*不是*全为零...

NULL 只是源代码中的东西。它是0((void *) 0) 或等效项。无论它在源代码中用作指针的什么地方(也就是说,您正在做像if (pointer != NULL) 这样的普通事情,而不是像int x = 3 + NULL; 这样的杂乱无章的事情),编译器都会有效地将它转换为空指针。也就是说,如果在 C 实现中全位为零不是空指针,则编译器会将 pointer != NULL 编译为将 pointer 与确实表示空指针的某些位模式进行比较。

所以你的问题都是关于空指针的;他们不是关于NULL

…在…的系统上…

什么是空指针的最终决定取决于 C 实现,而不是执行它的系统。 C 实现可以以任何它想要的方式表示指针,并在指令中使用机器地址时根据需要对其进行转换。

【讨论】:

    【解决方案5】:

    您可以通过明确和防御性的编码风格避免此类问题。

    如果你有一个指针 _p,编写类似的结构

        (_p==NULL)?(A):(B)
    

    现在任何读者都知道,您的意图是检查 _p 是否等于 NULL,即使在 NULL 可能不同于整数值 0 的机器上,编译器也会自动正确执行。由于依赖于隐式行为,现在静态代码检查器也不会警告您。

        (_p)?(A):(B)
    

    只是做得不对

    但除此之外,这是一个有趣的技术问题。

    C++ 委员会在 2019 年或 2020 年的一次有趣谈话表明,即使是这些人也考虑放弃对一些奇怪的未定义行为的兼容性,这在 1970 年之前对于一些 3-4 架构是必需的。在过去的几十年里,这些东西没有任何已知的用途——至少据我所知。正如对您的问题的第一条评论所说:您几乎找不到任何有这种问题的机器 - 至少在博物馆之外。

    【讨论】:

    • (_p)?(A):(B) 选项是否有问题完全是风格问题。如果_p 的值是空指针,那么该表达式的计算结果将与(B) 相同,否则与(A) 相同,无论_p 的位模式如何。这完全等同于您的其他版本。充其量,说一个人“只是做得不对”会被误解。
    • 好吧,我明确地说:风格。并非编译器接受的所有内容以及标准中的所有内容(至少对于向后兼容性)都是好的代码。和 (_p)?()() 只是不是好的代码。正如这个问题所表明的那样,它不会被误解。
    • @schnedan "(_p)?()() 只是不好的代码" 嗯,这是主观的。 “正如这个问题表明它不能避免误解”取决于谁解释它。
    • 如果您碰巧调试了一个由其他人开发的大型项目,您将训练自己编写清晰、防御性、可维护性的代码......
    • 对“2019 年或 2020 年 C++ 委员会的谈话”有任何参考吗?想看看正在讨论什么想法。
    猜你喜欢
    • 2020-07-01
    • 1970-01-01
    • 2011-05-26
    • 1970-01-01
    • 2016-09-11
    • 2011-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多