遇到一个有趣的采访问题:
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
答案 0 :(得分:52)
首先要做的事情是:printf
期待有效(即非NULL)
它的%s参数的指针正式传递给NULL
未定义。它可能会打印“(null)”或者它可能会删除您的所有文件
硬盘 - 就ANSI而言,要么是正确的行为
(至少,这就是哈比森和斯蒂尔告诉我的。)
话虽如此,是的,这是非常奇怪的行为。事实证明
发生的事情就是当你像这样做一个简单的printf
:
printf("%s\n", NULL);
gcc( ahem )非常聪明,可以将其解构为调用
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
奇怪的是,它不会打印以下换行符。 就好像它已经发现这会导致一个段错误 所以它没有打扰。 (它有它 - 当我编译时它警告我 它)。
答案 1 :(得分:25)
就C语言而言,原因是你正在调用未定义的行为,任何事情都可能发生。
至于为什么会发生这种情况的机制,现代gcc会优化printf("%s\n", x)
到puts(x)
,并且puts
在看到(null)
时没有打印printf
的愚蠢代码一个空指针,而printf
的常见实现有这种特殊情况。由于gcc无法优化(通常)像这样的非平凡格式字符串,因此当格式字符串中包含其他文本时,{{1}}实际上会被调用。
答案 2 :(得分:16)
第7.1.4节(C99或C11)说:
§7.1.4库函数的使用
¶1以下各项陈述均适用,除非在详细说明中另有明确说明 以下描述:如果函数的参数具有无效值(例如值) 在函数域之外,或者程序地址空间之外的指针, 或者是一个空指针,或者是指向不可修改存储的指针 参数不是const限定的)或函数不期望的类型(提升后) 如果参数数量可变,则行为未定义。
由于printf()
的规范没有说明当为%s
说明符传递空指针时发生的事情,因此行为显然是未定义的。 (请注意,传递由%p
说明符打印的空指针不是未定义的行为。)
以下是fprintf()
家庭行为的“章节和经文”(C2011 - 它是C1999中的不同章节编号):
§7.21.6.1fprintf函数
s
如果不存在l
长度修饰符,则参数应为指向初始值的指针 字符数组的元素。 [...]如果存在
l
长度修饰符,则参数应为指向初始值的指针 wchar_t类型数组的元素。
p
参数应该是指向void的指针。指针的值是 转换为一系列打印字符,在实现定义中 方式。
s
转换说明符的规范排除了空指针有效的可能性,因为空指针不指向适当类型的数组的初始元素。 p
转换说明符的规范不要求void指针指向特定的任何内容,因此NULL是有效的。
许多实现在传递空指针时打印诸如(null)
之类的字符串这一事实是一种很难依赖的善意。未定义行为的美妙之处在于允许这样的响应,但这不是必需的。类似地,允许崩溃,但不是必需的(更可惜的是 - 如果他们在宽容系统上工作然后移植到其他不那么宽容的系统,人们就会被咬伤。)
答案 3 :(得分:6)
NULL
指针不指向任何地址,并且尝试打印它会导致未定义的行为。未定义意味着由您的编译器或C库决定在尝试打印NULL时要执行的操作。