67

遇到一个有趣的面试问题:

test 1:
printf("test %s\n", NULL);
printf("test %s\n", NULL);

prints:
test (null)
test (null)

test 2:
printf("%s\n", NULL);
printf("%s\n", NULL);
prints
Segmentation fault (core dumped)

尽管这在某些系统上可能运行良好,但至少我的系统抛出了分段错误。这种行为的最佳解释是什么?上面的代码在 C 中。

以下是我的 gcc 信息:

deep@deep:~$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
4

4 回答 4

77

首先要做的事情:printf期望其 %s 参数的有效(即非 NULL)指针,因此将其传递 NULL 是正式未定义的。它可能会打印“(null)”,也可能会删除硬盘驱动器上的所有文件——就 ANSI 而言,这两种行为都是正确的(至少,Harbison 和 Steele 是这样告诉我的。)

话虽如此,是的,这确实是一种奇怪的行为。事实证明,当你做这样一个简单的事情时,正在发生的事情是printf

printf("%s\n", NULL);

gcc足够聪明,可以将其解构为对 puts. 第一个printf,这个:

printf("test %s\n", NULL);

足够复杂,以至于 gcc 将改为发出对 real 的调用 printf

(请注意,gcc 在编译时会发出有关您的无效printf参数的警告。那是因为它很久以前就开发了解析*printf格式字符串的能力。)

您可以通过使用该-save-temps选项编译然后查看生成的.s文件来自己查看。

当我编译第一个示例时,我得到:

movl    $.LC0, %eax
movl    $0, %esi
movq    %rax, %rdi
movl    $0, %eax
call    printf      ; <-- Actually calls printf!

(评论是我添加的。)

但是第二个产生了这个代码:

movl    $0, %edi    ; Stores NULL in the puts argument list
call    puts        ; Calls puts

奇怪的是它不打印以下换行符。就好像它已经发现这会导致段错误,所以它不会打扰。(它有——当我编译它时它警告我。)

于 2012-07-21T04:31:38.847 回答
34

就 C 语言而言,原因是您正在调用未定义的行为,并且任何事情都可能发生。

至于发生这种情况的机制,现代 gcc 优化printf("%s\n", x)puts(x),并且在看到空指针时puts没有可打印的愚蠢代码,而常见的实现有这种特殊情况。由于 gcc 不能像这样优化(通常)非平凡的格式字符串,因此当格式字符串中存在其他文本时实际上会被调用。(null)printfprintf

于 2012-07-21T04:23:46.467 回答
18

第 7.1.4 节(C99 或 C11)说:

§7.1.4 库函数的使用

¶1 除非在随后的详细描述中另有明确说明,否则以下每个语句均适用:如果函数的参数具有无效值(例如函数域之外的值,或地址空间之外的指针)程序,或空指针,或指向不可修改存储的指针(当相应的参数不是 const 限定时)或具有可变数量参数的函数不期望的类型(提升后),行为未定义。

由于 的规范printf()没有说明当您将空指针传递给说明符时会发生什么%s,因此该行为是明确未定义的。(请注意,传递要由说明符打印的空指针%p不是未定义的行为。)

这是fprintf()家庭行为的“章节和诗句”(C2011 - 它是 C1999 中的不同节号):

§7.21.6.1 fprintf 函数

s     如果不存在l长度修饰符,则参数应为指向字符类型数组的初始元素的指针。[...]

     如果存在l长度修饰符,则参数应是指向 wchar_t 类型数组的初始元素的指针。

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

转换说明符的规范s排除了空指针有效的可能性,因为空指针不指向适当类型数组的初始元素。转换说明符的规范p不要求 void 指针特别指向任何东西,因此 NULL 是有效的。

许多实现打印字符串(例如(null)在传递空指针时)的事实是一种危险的善意依赖。未定义行为的美妙之处在于允许这样的响应,但不是必需的。同样,崩溃是允许的,但不是必需的(更遗憾的是——如果他们在一个宽容的系统上工作,然后移植到其他宽容度较低的系统,就会被咬伤)。

于 2012-07-21T04:36:21.847 回答
7

NULL指针不指向任何地址,尝试打印它会导致未定义的行为。未定义意味着它由您的编译器或 C 库决定当它尝试打印 NULL 时要做什么。

于 2012-07-21T04:13:10.423 回答