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

时间:2017-07-09 12:38:21

标签: c language-lawyer c99 undefined-behavior c11

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

#include <stdio.h>

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

    printf("%p", p);

    return 0;
}

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

3 个答案:

答案 0 :(得分:92)

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

问题中的代码表现出明确定义的行为。

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

  

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

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

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

示例1 - strcpy

[7.21.2.3] 仅表示:

  

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

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

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

  •   

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

  •   

    [7.12.6.4(frexp)] 将整数存储在exp

    指向的对象
  •   

    [7.19.5.1(fclose)] stream 指向的

示例2 - printf

[7.19.6.1] %p

说了这个
  

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

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

<子> 1。除非标准作者提出,否则我们可以找到类似于rationale文件的内容,以澄清事情。

答案 1 :(得分:20)

简答

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

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

长答案

%p转换说明符期望类型指针的参数为void,指针到可打印字符的转换是实现定义的。它没有说明期望空指针。

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

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_t n的参数指定了函数数组的长度,则在调用该函数时,n的值可以为零。除非在本子条款中对特定函数的描述中另有明确说明,否则此类调用上的指针参数仍应具有7.1.4中所述的有效值。

是故意的吗?

我不知道具有空指针的%p的UB是否实际上是故意的,但是因为标准明确指出空指针被认为是无效值作为标准库函数的参数,然后它会去并显式指定空指针是有效参数(snprintf,free等)的情况,然后它再次重复要求参数有效,即使在零'n'个例子中也是如此(memcpymemmovememset),那么我认为C标准委员会并不太关心这些事情是不确定的。

答案 2 :(得分:-1)

C标准的作者没有尽力列出实施必须满足的所有行为要求,以适合任何特定目的。相反,他们期望编写编译器的人会运用一定程度的常识,无论标准是否需要它。

某些东西是否调用UB的问题很少有用。真正重要的问题是:

  1. 试图编写高质量编译器的人是否应该以可预测的方式运行? 对于所描述的情况,答案显然是肯定的。

  2. 程序员是否有资格期望任何类似普通平台的质量编译器都能以可预测的方式运行? 在描述的场景中,我会说答案是肯定的。

  3. 可能一些迟钝的编译器编写者会扩展标准的解释,以便证明做一些奇怪的事情是正确的吗? 我希望不会,但不会排除它。

  4. 清理编制者应该对这种行为发出嘘声吗?这将取决于其用户的偏执程度; 一个清理编译器可能不应该默认为这种行为发出嘎嘎声,但是可能提供一个配置选项,以防程序被移植到行为奇怪的聪明的&#34; / dumb编译器。 /强>

  5. 如果对标准的合理解释意味着行为被定义,但是一些编译器编写者会扩展解释以证明其他方面的合理性,那么标准所说的真的很重要吗?