使用%p
转换说明符打印空指针是否是未定义的行为?
#include <stdio.h>
int main(void) {
void *p = NULL;
printf("%p", p);
return 0;
}
该问题适用于C标准,不适用于C实现。
使用%p
转换说明符打印空指针是否是未定义的行为?
#include <stdio.h>
int main(void) {
void *p = NULL;
printf("%p", p);
return 0;
}
该问题适用于C标准,不适用于C实现。
Answers:
这是我们受到英语限制和标准结构不一致的怪异案例之一。所以充其量,我可以提出一个令人反感的论点,因为不可能证明它:) 1
问题中的代码表现出明确的行为。
因为[7.1.4]是问题的基础,所以让我们从这里开始:
除非在下面的详细描述中另有明确说明,否则以下每个语句均适用:如果函数的参数具有无效值(例如,函数域之外的值或程序地址空间之外的指针,或空指针,[...其他示例...])[...]行为未定义。[...其他陈述...]
这是笨拙的语言。一种解释是,对于所有库功能,列表中的项目均为UB,除非由个别描述覆盖。但是该列表以“诸如”开头,表示它是说明性的,并非详尽无遗。例如,它没有提及正确的字符串空终止(对于eg的行为至关重要strcpy
)。
因此,很明显,7.1.4的意图/范围只是“无效值”导致UB(除非另有说明)。我们必须查看每个函数的描述,以确定哪些算作“无效值”。
strcpy
[7.21.2.3]仅说:
该
strcpy
函数将指向的字符串s2
(包括终止的空字符)复制到指向的数组中s1
。如果在重叠的对象之间进行复制,则行为是不确定的。
它没有明确提及空指针,但是也没有提及空终止符。取而代之的是,从“所指向的字符串”推断出s2
唯一有效的值是字符串(即,指向以空字符结尾的字符数组的指针)。
确实,这种模式可以在各个描述中看到。其他一些例子:
[7.6.4.1(fenv)]的当前浮点环境存储在指向物体通过
envp
[7.12.6.4(frexp)]将整数存储在由指向的int 对象中
exp
[7.19.5.1(FCLOSE)]的流指向由
stream
printf
[7.19.6.1]这样说%p
:
p
-参数应为的指针void
。指针的值以实现定义的方式转换为一系列打印字符。
Null是有效的指针值,本节没有明确提到null是一种特殊情况,也没有指针必须指向一个对象。因此,它被定义为行为。
1.除非标准作者挺身而出,或者除非我们能找到类似于阐明事实的基本原理文件的内容。
是的。使用%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_t
n 的参数指定了函数数组的长度,则在调用该函数时n的值可以为零。除非在本节中对特定功能的描述中另有明确说明,否则此类调用上的指针参数仍应具有7.1.4中所述的有效值。
我不知道%p
带有空指针的UB 实际上是否是有意的,但是由于标准明确指出将空指针视为无效值,这是标准库函数的参数,因此它明确指定了空值的情况指针是一个有效的参数(snprintf的,自由的,等等),然后它会再次重复要求的参数,即使在零是有效的“N”的情况下(memcpy
,memmove
,memset
),那么我认为这是合理的假设C标准委员会不太关心未定义的事情。
%p
行为不应该是未定义的行为
C标准的作者没有努力详尽列出实施必须满足的特定行为的所有行为要求。取而代之的是,他们希望编写编译器的人们能够运用一定的常识,无论该标准是否需要它。
某些东西是否调用UB的问题本身很少有用。真正重要的问题是:
尝试编写高质量编译器的人是否应该使其行为可预测? 对于所描述的场景,答案显然是肯定的。
程序员是否应该有权期望与普通平台类似的高质量编译器将以可预测的方式运行? 在描述的情况下,我会说答案是肯定的。
某些晦涩的编译器作者可能会扩展标准的解释,以证明做些奇怪的事情是合理的吗? 我希望不会,但不会排除它。
消毒编译器是否应该对行为the之以鼻?这将取决于用户的偏执程度;一个消毒的编译器可能不应该默认使用这种行为,但可能提供一个配置选项,以防程序被移植到行为怪异的“聪明” /笨拙的编译器中。
如果对标准的合理解释意味着已经定义了行为,但是某些编译器作者将解释加以延伸以证明这样做是合理的,那么标准所说的内容真的重要吗?
printf("%p", (void*) 0)
根据标准,行为是否未定义?深度嵌套的函数调用与此相关,就像中国的茶叶价格一样。是的,UB在现实世界的程序中非常常见-它是什么?
%p
的每种可能含义回答了问题。