使用%p打印空指针是未定义的行为?

Dro*_* K. 92 c c99 undefined-behavior language-lawyer c11

使用%p转换说明符打印空指针是不确定的行为?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这个问题适用于C标准,而不适用于C实现.

Oli*_*rth 92

这是一个奇怪的角落案例,我们受到英语语言的限制和标准中不一致的结构.所以充其量,我可以提出一个令人信服的反驳论点,因为它无法证明它:) 1


问题中的代码展示了明确定义的行为.

由于[7.1.4]是问题的基础,让我们从那里开始:

除非在以下详细说明中另有明确说明,否则以下每个语句均适用:如果函数的参数具有无效值(例如函数域外的值,或程序地址空间外的指针,或者是空指针,[......其他例子......])[...]行为未定义.[......其他声明......]

这是一种笨拙的语言.一种解释是列表中的项目对于所有库函数都是UB,除非被各个描述覆盖.但该列表以"如"开头,表明它是说明性的,并非详尽无遗.例如,它没有提到正确的字符串空终止(例如对于行为的关键strcpy).

因此,很清楚7.1.4的意图/范围只是"无效值"导致UB(除非另有说明).我们必须查看每个函数的描述,以确定什么算作"无效值".

例1 - strcpy

[7.21.2.3]只说这个:

strcpy函数将指向的字符串s2(包括终止空字符)复制到指向的数组中s1.如果在重叠的对象之间进行复制,则行为未定义.

它没有明确提到空指针,但它也没有提到null终止符.相反,从"指向的字符串"推断出s2唯一有效的值是字符串(即指向以空字符结尾的字符数组的指针).

实际上,在整个单独的描述中可以看到这种模式.其他一些例子:

  • [7.6.4.1(fenv)]存储当前浮点环境中的对象指向envp

  • [7.12.6.4(frexp)]存储整数的INT 对象指向exp

  • [7.19.5.1(FCLOSE)]流指向stream

例2 - printf

[7.19.6.1]对此说%p:

p- 参数应该是指针void.指针的值以实现定义的方式转换为打印字符序列.

Null是一个有效的指针值,本节没有明确提到null是一种特殊情况,也不是指针必须指向一个对象.因此,它是定义的行为.


1.除非标准作者提出,否则我们可以找到类似于澄清事物的理由文件.


Dro*_* K. 20

简答

是的.使用%p转换说明符打印空指针具有未定义的行为.话虽如此,我并不知道任何现有的符合规定的实施方案都会出现问题.

答案适用于任何C标准(C89/C99/C11).


答案很长

%p转换指定期望类型的指针的参数无效,则指针可打印字符的转化是实现定义.它没有说明期望空指针.

对标准库函数的介绍表明,作为(标准库)函数的参数的空指针被认为是无效值,除非另有明确说明.

C99/C11 §7.1.4 p1

[...]如果函数的参数具有无效值(例如[...]空指针,则[...]行为未定义.

将空指针作为有效参数的(标准库)函数示例:

  • fflush() 使用空指针来刷新"所有流"(适用).
  • freopen() 使用空指针指示与流"当前关联"的文件.
  • snprintf() 允许在'n'为零时传递空指针.
  • realloc() 使用空指针分配新对象.
  • free() 允许传递空指针.
  • strtok() 使用空指针进行后续调用.

如果我们采用这种情况snprintf(),那么当'n'为零时允许传递空指针是有意义的,但对于允许类似的零'n'的其他(标准库)函数则不是这种情况.例如:memcpy(),memmove(),strncpy(),memset(),memcmp().

它不仅在标准库的介绍中指定,而且在这些函数的介绍中再次指出:

C99 §7.21.1 p2/C11 §7.24.1 p2

如果声明为size_tn 的参数指定了函数数组的长度,则在调用该函数时,n的值可以为零.除非在本子条款中对特定函数的描述中另有明确说明,否则此类调用上的指针参数仍应具有7.1.4中所述的有效值.


这是故意的吗?

我不知道%p带有空指针的UB是否实际上是故意的,但是由于标准明确指出空指针被认为是无效值作为标准库函数的参数,然后它继续显式地指定null的情况指针是一个有效的参数(snprintf的,自由的,等等),然后它会再次重复要求的参数,即使在零是有效的"N"的情况下(memcpy,memmove,memset),那么我认为这是合理的假设C标准委员会并不太关心这些事情是不确定的.

  • @Damon:你的神秘硬件并不是神话般的,有很多架构可能无法在地址寄存器中加载不代表有效地址的值.但是,将空指针作为函数参数传递仍然需要作为一般机制在这些平台上工作.仅仅把一个放在堆栈上就不会搞砸了. (3认同)