偶然发现了一个有趣的面试问题:
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
首先第一件事情: printf
期待一个有效的(即非NULL)指针的%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
该奇怪的是,它不打印以下换行符。 就好像它是想通了,这会导致一个segfault所以它不打扰。 (它有 - 它提醒我,当我编译它。)
至于C语言来讲,其原因是,你调用未定义行为,任何事情都有可能发生。
至于为什么发生这种情况,现代GCC优化机制printf("%s\n", x)
来puts(x)
和puts
没有傻代码打印(null)
时,看到一个空指针,而普遍的实现printf
有这种特殊情况。 由于GCC不能优化(一般)不平凡的格式字符串这样, printf
实际被调用时,格式字符串具有其他文字存在于它。
7.1.4节(C99的C11或)说:
§7.1.4的库函数使用
¶1每个下列陈述适用,除非在下面的详细描述明确地指出,否则:如果一个参数传递给函数的值无效(如函数的域之外的值,或者的地址空间外的指针程序,或一个空指针,或指向当相应的参数不是常量限定不可修改的存储器)或类型(升级后)不通过与可变的参数数目的函数预期,该行为是未定义的。
由于规范printf()
什么都不说,当你传递一个空指针,它的发生的事情%s
说明符,这个行为是明确的定义。 (注意,传递一个空指针由被打印%p
说明符不未定义行为)。
这里是“引经据典”的fprintf()
家庭行为(C2011 -这是在C1999不同的区段号):
§7.21.6.1fprintf函数
s
如果没有l
长度改性剂的存在,该参数将是指向字符类型的数组的初始元素。 [...]
如果l
长度改性剂的存在,该参数将是指向wchar_t的类型的数组的初始元素。
p
的参数应是无效的指针。 指针的值被转换成打印字符的序列,在一个实现定义方式。
的详细规格s
转换说明排除一个空指针是有效的,因为空指针不指向初始适当类型的数组的元素的可能性。 为规范p
转换说明不需要空指针在什么特别指向和NULL因此有效。
许多实现打印一个字符串,如事实(null)
传递空指针是一个善良是危险的依赖。 未定义行为的好处是,这种反应是允许的,但它不是必需的。 同样,崩溃是允许的,但不是必需的(更为可惜 - 如果他们宽容的系统端口工作,然后到其他不宽容系统的人得到咬伤)。
该NULL
指针不指向任何地址,并试图打印它会导致不确定的行为。 未定义这意味着它是给你的编译器或C库,以决定何时试图打印NULL做什么。