使用%p
转换说明符打印空指针是不确定的行为?
#include <stdio.h>
int main(void) {
void *p = NULL;
printf("%p", p);
return 0;
}
这个问题适用于C标准,而不适用于C实现。
这是一个奇怪的角落案例,我们受到英语语言的限制和标准中不一致的结构。所以充其量,我可以提出一个令人信服的反驳论点,因为它无法证明它:)1
问题中的代码展示了明确定义的行为。
由于[7.1.4]是问题的基础,让我们从那里开始:
除非在以下详细说明中另有明确说明,否则以下每个语句均适用:如果函数的参数具有无效值(例如函数域外的值,或程序地址空间外的指针,或者是空指针,[......其他例子......])[...]行为未定义。 [......其他声明......]
这是一种笨拙的语言。一种解释是列表中的项目对于所有库函数都是UB,除非被各个描述覆盖。但该列表以“如”开头,表明它是说明性的,并非详尽无遗。例如,它没有提到正确的字符串空终止(对于例如strcpy
的行为至关重要)。
因此,很清楚7.1.4的意图/范围只是“无效值”导致UB(除非另有说明)。我们必须查看每个函数的描述,以确定什么算作“无效值”。
strcpy
[7.21.2.3]只说这个:
strcpy
函数将s2
指向的字符串(包括终止空字符)复制到s1
指向的数组中。如果在重叠的对象之间进行复制,则行为未定义。
它没有明确提到空指针,但它也没有提到null终止符。相反,从“由s2
指向的字符串”推断出唯一有效的值是字符串(即指向以空字符结尾的字符数组的指针)。
实际上,在整个单独的描述中可以看到这种模式。其他一些例子:
envp
指向的对象中exp
指向的int对象中stream
指出的溪流printf
[7.19.6.1]关于%p
说:
p
- 参数应该是指向void
的指针。指针的值以实现定义的方式转换为打印字符序列。
Null是一个有效的指针值,本节没有明确提到null是一种特殊情况,也不是指针必须指向一个对象。因此,它是定义的行为。
1.除非标准作者提出,否则我们可以找到类似于rationale文件的内容来澄清事情。
是。使用%p
转换说明符打印空指针具有未定义的行为。话虽如此,我并不知道任何现有的符合规定的实施方案都会出现问题。
答案适用于任何C标准(C89 / C99 / C11)。
%p
转换说明符期望类型指针的参数为void,指针到可打印字符的转换是实现定义的。它没有说明期望空指针。
对标准库函数的介绍表明,作为(标准库)函数的参数的空指针被认为是无效值,除非另有明确说明。
C99
/ C11
§7.1.4 p1
[...]如果函数的参数具有无效值(例如[...]空指针,则[...]行为未定义。
将空指针作为有效参数的(标准库)函数示例:
fflush()
使用空指针来刷新“所有流”(适用)。freopen()
使用空指针来指示与流“当前关联”的文件。snprintf()
允许传递空指针。realloc()
使用空指针来分配新对象。free()
允许传递空指针。strtok()
使用空指针进行后续调用。如果我们采用snprintf()
的情况,那么当'n'为零时允许传递空指针是有意义的,但对于允许类似的零'n'的其他(标准库)函数则不是这种情况。例如:memcpy()
,memmove()
,strncpy()
,memset()
,memcmp()
。
它不仅在标准库的介绍中指定,而且在这些函数的介绍中再次指出:
C99 §7.21.1 p2
/ C11 §7.24.1 p2
如果声明为
size_t
的参数指定了函数数组的长度,则在调用该函数时,n的值可以为零。除非在本子条款中对特定函数的描述中另有明确说明,否则此类调用上的指针参数仍应具有7.1.4中所述的有效值。
我不知道带有空指针的%p
的UB是否实际上是故意的,但是由于标准明确指出空指针被认为是无效值作为标准库函数的参数,然后它明确指出了空指针是一个有效的参数(snprintf,free等),然后它再次重复要求参数有效,即使在零'n'的情况下(memcpy
,memmove
,memset
),那么我认为这是合理的假设C标准委员会并不太关心这些事情是不确定的。
C标准的作者没有尽力列出实施必须满足的所有行为要求,以适合任何特定目的。相反,他们期望编写编译器的人会运用一定程度的常识,无论标准是否需要。
是否某些东西调用UB的问题很少有用。真正重要的问题是:
如果对标准的合理解释意味着定义了行为,但是一些编译器编写者会扩展解释以证明其他方面的合理性,那么标准所说的真的重要吗?