用printf的%s说明符打印NULL的行为是什么?

遇到一个有趣的采访问题:

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) 

虽然这可能在一些系统上运行良好,但至less我会抛出一个分段错误。 这个行为最好的解释是什么? 以上代码在C.

以下是我的gcc信息:

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

Solutions Collecting From Web of "用printf的%s说明符打印NULL的行为是什么?"

首先要做的事情是: printf需要一个有效的(即非NULL)指针来指定它的%s参数,所以传递一个NULL值是未定义的。 它可能会打印“(null)”,或者它可能会删除硬盘上的所有文件 – 就ANSI而言是正确的行为(至少,Harbison和Steele告诉我的是这样)。

这是说,是的,这是非常奇怪的行为。 事实证明,当你做一个简单的printf时,发生的事情就是:

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

海湾合作委员会( 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 

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

就C语言而言,原因是你调用了未定义的行为,任何事情都可能发生。

至于为什么会发生这种情况的机制,现代gcc优化printf("%s\n", x) puts(x) ,并puts没有愚蠢的代码打印(null)当它看到一个空指针,而printf普通实现有这种特殊情况。 由于gcc不能像这样优化(一般来说)非平凡的格式化字符串,所以当格式化字符串中有其他文本时, printf实际上会被调用。

第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这个论据应该是一个无效的指针。 指针的值将以实现定义的方式转换为打印字符序列。

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

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

NULL指针不指向任何地址,并尝试打印它导致未定义的行为。 未定义的含义取决于您的编译器或C库决定当它尝试打印NULL时该怎么做。