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


93

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

#include <stdio.h>

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

    printf("%p", p);

    return 0;
}

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


实际上,不要以为任何人(包括C委员会)都在乎它。这是一个非常人为的问题,没有(或几乎没有)实际意义。
P__J__

就像printf仅显示该值,并且不触摸(在读取或写入指向的对象的意义上)-不能是UB i指针对其类型值具有有效值(NULL是有效值
P__J__

3
@PeterJ让我们说您所说的是正确的(尽管标准清楚地指出了其他事实),仅此一个事实,即我们对此进行辩论,就使该问题成为有效和正确的问题,因为该标准的以下引用部分使得对于普通开发人员来说,很难理解到底发生了什么。。含义:这个问题不应该被否决,因为这个问题需要澄清!
彼得·瓦罗


2
@PeterJ则是一个不同的故事,感谢您的澄清:)
Peter Varo

Answers:


93

这是我们受到英语限制和标准结构不一致的怪异案例之一。所以充其量,我可以提出一个令人反感的论点,因为不可能证明它:) 1


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

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

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

这是笨拙的语言。一种解释是,对于所有库功能,列表中的项目均为UB,除非由个别描述覆盖。但是该列表以“诸如”开头,表示它是说明性的,并非详尽无遗。例如,它没有提及正确的字符串空终止(对于eg的行为至关重要strcpy)。

因此,很明显,7.1.4的意图/范围只是“无效值”导致UB(除非另有说明)。我们必须查看每个函数的描述,以确定哪些算作“无效值”。

范例1- 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

示例2- printf

[7.19.6.1]这样%p

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

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


1.除非标准作者挺身而出,或者除非我们能找到类似于阐明事实的基本原理文件的内容。


评论不作进一步讨论;此对话已转移至聊天
巴尔加夫饶

1
在示例1中,“还没有提到空终止符”很弱-规范说明strcpy是“复制字符串 ”。 字符串被明确定义为具有空字符
chux-恢复莫妮卡

1
@chux-这在某种程度上是我的观点-必须从上下文推断出什么是有效/无效,而不是假设7.1.4中的列表是详尽的。(但是,在我的回答的这一部分的存在方面,从后来被删除的评论的角度来看,它更有意义,因为他们认为strcpy是一个反例。)
奥利弗·查尔斯沃思

1
问题的关键是读者对诸如此类的解释。这是否意味着一些可能的无效值示例?这是否意味着某些示例始终都是无效值?作为记录,我同意第一种解释。
ninjalj

1
@ninjalj-是的,同意。从本质上讲,这就是我想要传达的意思,即“这些是可能是无效值的事物类型的示例”。:)
奥利弗·查尔斯沃思

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 实际上是否是有意的,但是由于标准明确指出将空指针视为无效值,这是标准库函数的参数,因此它明确指定了空值的情况指针是一个有效的参数(snprintf的,自由的,等等),然后它会再次重复要求的参数,即使在零是有效的“N”的情况下(memcpymemmovememset),那么我认为这是合理的假设C标准委员会不太关心未定义的事情。


评论不作进一步讨论;此对话已转移至聊天
巴尔加夫饶

1
@JeroenMostert:这种说法的目的是什么?7.1.4给出的报价很清楚,不是吗?什么是有争论“除非明确说明,否则”时,它没有被说明,否则?关于(不相关的)字符串函数库具有相似的措辞,因此措辞似乎并非偶然,有什么可争论的呢?我认为这个答案(虽然在实践中并没有真正的用处)是尽可能正确的。
达蒙

3
@Damon:您的神话般的硬件不是神话般的,有许多体系结构可能无法将不代表有效地址的值加载到地址寄存器中。但是,仍然需要将空指针作为函数参数传递,以在这些平台上作为通用机制工作。仅将一个堆栈放在堆栈上就不会炸毁。
Jeroen Mostert

1
@anatolyg:在x86处理器上,地址分为两部分-段和偏移量。在8086上,加载段寄存器就像加载任何其他寄存器一样,但是在以后的所有机器上,它都会获取段描述符。加载无效的描述符会导致陷阱。很多为80386及更高版本处理器的代码,但是,只使用一个段,因此从来没有加载段寄存器在所有
超级猫

1
我想每个人都会同意,打印带有null指针的%p行为不应该是未定义的行为
MM

-1

C标准的作者没有努力详尽列出实施必须满足的特定行为的所有行为要求。取而代之的是,他们希望编写编译器的人们能够运用一定的常识,无论该标准是否需要它。

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

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

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

  3. 某些晦涩的编译器作者可能会扩展标准的解释,以证明做些奇怪的事情是合理的吗? 我希望不会,但不会排除它。

  4. 消毒编译器是否应该对行为the之以鼻?这将取决于用户的偏执程度;一个消毒的编译器可能不应该默认使用这种行为,但可能提供一个配置选项,以防程序被移植到行为怪异的“聪明” /笨拙的编译器中。

如果对标准的合理解释意味着已经定义了行为,但是某些编译器作者将解释加以延伸以证明这样做是合理的,那么标准所说的内容真的重要吗?


1.程序员发现现代/积极的优化器所做的假设与他们认为“合理”或“质量”不一致的情况并不少见。2.当涉及到规范中的歧义时,实现者对于他们可能承担的自由持不同意见并不少见。3.关于C标准委员会的成员,即使他们并不总是就“正确”的解释达成共识,更不用说它应该是什么了。鉴于上述情况,我们应该遵循谁的合理解释?
Dror K.

6
回答“您是否对UB的有用性或编译器应该如何行为”的问题,回答“是否要调用UB的特定代码”这一问题的答案很不好,尤其是因为您可以将其复制粘贴为几乎可以回答有关特定UB的任何问题。作为您夸夸其谈的一种说法:是的,无论某些编译器作者做什么或您对他们的看法如何,该标准的内容确实很重要,因为标准是程序员和编译器作者的起点。
Jeroen Mostert

1
@JeroenMostert:“ X是否会调用未定义的行为”的答案通常取决于问题的含义。如果某个程序被视为具有“未定义行为”,并且该标准不对符合性实现的行为施加任何要求,则几乎所有程序都将调用UB。该标准的作者明确地允许,如果程序嵌套函数调用的深度过深,则实现可以以任意方式表现,只要实现可以正确处理至少一个(可能是人为的)源文本,即可执行标准中的翻译限制。
超级猫

@supercat:非常有趣,但是printf("%p", (void*) 0)根据标准,行为是否未定义?深度嵌套的函数调用与此相关,就像中国的茶叶价格一样。是的,UB在现实世界的程序中非常常见-它是什么?
Jeroen Mostert

1
@JeroenMostert:由于该标准允许一个钝的实现将几乎所有程序都视为具有UB,因此重要的是非钝的实现的行为。如果您没有注意到,我不仅会写关于UB的副本/粘贴信息,还针对问题%p的每种可能含义回答了问题。
超级猫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.