遇到了一个有趣的面试问题:
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
发布于 2012-07-20 20:31:38
首先要做的事情是:printf
为其%s参数期待一个有效(即非空)指针,因此传递空值是官方未定义的。它可以打印"(null)“或者删除硬盘上的所有文件--对于ANSI来说,这都是正确的行为(至少,哈比森和斯蒂尔是这么告诉我的)。
尽管如此,是的,这是非常愚蠢的行为。事实证明,当您执行这样一个简单的printf
时,所发生的事情是:
printf("%s\n", NULL);
gcc很聪明,可以把它解构成对puts
的召唤。第一个printf
,这个:
printf("test %s\n", NULL);
已经够复杂的了,gcc反而会向真正的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-20 20:23:46
就C语言而言,原因是您正在调用未定义的行为,任何事情都可能发生。
至于为什么会发生这种情况的机制,现代gcc将printf("%s\n", x)
优化为puts(x)
,而puts
在看到空指针时没有打印(null)
的愚蠢代码,而printf
的一般实现则有这种特例。由于gcc不能像这样优化(通常)非平凡的格式字符串,所以当格式字符串中有其他文本时,printf
实际上会被调用。
发布于 2012-07-20 20:36:21
第7.1.4节( C99或C11)说:
§7.1.4图书馆职能的使用 除非在下面的详细描述中另有明确说明,否则以下每条语句都适用:如果函数的参数具有无效值(例如函数域外的值,或程序地址空间外的指针,或当对应的参数不符合const-限定时指向不可修改存储的指针)或具有可变参数的函数不期望的类型(升级后),则行为是未定义的。
由于printf()
规范没有说明当您为%s
说明符传递一个指向它的空指针时会发生什么,所以行为是显式的未定义的。(请注意,传递要由%p
说明符打印的空指针不是未定义的行为。)
以下是fprintf()
家庭行为的“章节和诗句”(C2011 --在C1999中是一个不同的节号):
§7.21.6.1 fprintf函数 如果没有
s
长度修饰符,则参数应该是字符类型数组的初始元素的指针。..。 如果存在l
长度修饰符,则该参数应是指向wchar_t类型数组的初始元素的指针。p
相对应的论点应是无效的指针。指针的值以实现定义的方式转换为打印字符序列.
s
转换说明符的规范排除了空指针有效的可能性,因为空指针没有指向适当类型数组的初始元素。p
转换说明符的规范不要求空指针指向任何特定的位置,因此NULL是有效的。
许多实现在传递空指针时打印一个字符串(如(null)
),这是一个很好的选择,依赖它是危险的。不明确行为的好处在于,这种反应是允许的,但它并不是必需的。类似地,崩溃是允许的,但不是必需的(更遗憾的是,如果人们在宽恕系统上工作,然后移植到其他不太宽容的系统,人们就会被咬伤)。
https://stackoverflow.com/questions/11589342
复制