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

Printing null pointers with %p is undefined behavior?

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

#include <stdio.h>

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

    printf("%p", p);

    return 0;
}

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

简答

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

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


长答案

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

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

C99 / C11 §7.1.4 p1

[...] If an argument to a function has an invalid value (such as [...] a null pointer, [...] the behavior is undefined.

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

  • fflush() 使用空指针刷新 "all streams"(适用)。
  • freopen() 使用空指针来指示流中的文件 "currently associated"。
  • snprintf() 允许在 'n' 为零时传递空指针。
  • realloc() 使用空指针分配新对象。
  • free() 允许传递空指针。
  • strtok() 对后续调用使用空指针。

如果我们考虑 snprintf() 的情况,当 'n' 为零时允许传递空指针是有意义的,但对于允许的其他(标准库)函数而言情况并非如此类似的零 'n'。例如:memcpy()memmove()strncpy()memset()memcmp().

不仅在标准库的介绍中明确指出,在这些函数的介绍中也再次指出:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Where an argument declared as size_t n specifies the length of the array for a function, n can have the value zero on a call to that function. Unless explicitly stated otherwise in the description of a particular function in this subclause, pointer arguments on such a call shall still have valid values as described in 7.1.4.


是故意的吗?

我不知道 %p 的 UB 是否真的有空指针是故意的,但由于标准明确指出空指针被视为无效值作为标准库函数的参数,然后它开始并明确指定空指针是有效参数的情况(snprintf、free 等),然后它再次重复即使在零 'n' 情况下参数也有效的要求(memcpy, memmove, memset), 那么我认为 C 标准委员会不太关心这些未定义的事情是合理的。

这是我们受制于英语语言限制和标准结构不一致的那些奇怪的极端情况之一。所以充其量,我可以做出令人信服的反驳,因为不可能证明它:)1


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

因为[7.1.4]是题目的基础,所以我们从这里开始:

Each of the following statements applies unless explicitly stated otherwise in the detailed descriptions that follow: If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer, [... other examples ...]) [...] the behavior is undefined. [... other statements ...]

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

因此很明显 7.1.4 的 intent/scope 只是 "invalid value" 导致 UB(除非另有说明)。我们必须查看每个函数的描述以确定什么算作 "invalid value".

示例 1 - strcpy

[7.21.2.3] 仅表示:

The strcpy function copies the string pointed to by s2 (including the terminating null character) into the array pointed to by s1. If copying takes place between objects that overlap, the behavior is undefined.

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

的确,这种模式可以在各个描述中看到。其他一些示例:

  • [7.6.4.1 (fenv)] store the current floating-point environment in the object pointed to by envp

  • [7.12.6.4 (frexp)] store the integer in the int object pointed to by exp

  • [7.19.5.1 (fclose)] the stream pointed to by stream

示例 2 - printf

[7.19.6.1] 说的是 %p:

p - The argument shall be a pointer to void. The value of the pointer is converted to a sequence of printing characters, in an implementation-defined manner.

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


1.除非标准作者挺身而出,或者除非我们能找到类似于 rationale 文档的内容来澄清事情。

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

关于是否调用 UB 的问题本身很少有用。真正重要的问题是:

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

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

  3. 一些迟钝的编译器编写者可能会延伸对标准的解释,以便证明做一些奇怪的事情是合理的吗? 我希望不会,但不排除这种可能性。

  4. 清理编译器是否应该对这种行为发出警告?这将取决于他们用户的偏执程度; 净化编译器可能不应该默认对此类行为发出警告,但也许提供一个配置选项以防程序可能被移植到行为怪异的"clever"/哑编译器。

如果对标准的合理解释意味着定义了一种行为,但一些编译器作者扩展了解释以证明不这样做是合理的,那么标准所说的真的重要吗?