printf
系列函数(另有 fprintf
、sprintf
、snprintf
等)在输出时使用了强大的格式化字符串。格式化字符串以一个 %
开始,以类型字段(d
、x
、f
等)结束,完整格式如下(除了 %
和类型之外均为可选字段,注意精度之前有一个句点):
%[参数][标志][宽度][.精度][长度]类型
参数
参数字段为 POSIX 扩展功能,非 C99 标准定义。使用 m$
指定格式化字符串之后的第 m 个参数,从 1 开始编号。假如使用了参数字段,则所有的参数必须都至少使用一次,即从 1$
到 n$
都必须至少出现一次(其中 n
为格式化字符串之后的参数个数),否则编译时将产生如下警告:
warning: missing $ operand number in format [-Wformat=]
借助参数字段,可以实现参数的多次使用、乱序使用等,而不需要重复书写参数。例如:printf("%2$d %2$#x; %1$d %1$#x",16,17)
将输出 17 0x11; 16 0x10
,两个参数都使用了两次,且先使用第二个参数(同时也使用了下一节的 #
标志)。
标志
-
: 左对齐(默认为右对齐)+
: 给正数附加符号前缀(默认为正数没有任何前缀)0
: 指定了宽度字段(见下文)且为右对齐时,前缀补 0(默认补空格;当使用-
标志指定了左对齐时,0
标志失效)#
: 使用「备选格式」。对于g
和G
类型,不省略小数点部分最后的0
;对于f
、F
、e
、E
、g
、G
类型,总是输出小数点;对于o
、x
、X
类型,分别在非零数值前附加0
、0x
、0X
前缀。上一节的例子中就使用了#
标志。
宽度
宽度字段指定输出字符的最小长度。长度不足的输出将使用填充字符补齐,填充字符及对齐方式使用上述的 0
标志和 -
标志确定;超长的输出不受影响(当然可使用下文的精度字段限定)。
当指定宽度字段时,可使用确定的整数值静态指定,也可使用 *
号由某个参数动态指定。例如,printf("%0*d", 5, 10)
将输出 00010
。注意:此例不能写成 %*0d
即颠倒了标志字段和宽度字段,否则将产生编译警告:
warning: unknown conversion type character ‘0’ in format [-Wformat=]
warning: too many arguments for format [-Wformat-extra-args]
精度
精度字段指定输出字符的最大长度。对浮点型数据来说,精度字段指定了小数点后的最长有效位数;对字符串来说,精度字段指定了输出的最大字符数。
与宽度字段相同,指定精度字段时,也可使用确定的整数值静态确定或 *
号动态确定。为了与宽度字段做区分,精度字段前必须加句点。例如,printf("%.*s", 3, "abcdef")
将输出 abc
。假如没有句点,将被解析成宽度字段(最小长度)为 3 而精度字段(最大长度)未指定,因此将输出 abcdef
。
长度
hh
: 用于将char
型参数转换成int
型输出。h
: 用于将short
型参数转换成int
型输出。l
: 用于输出long
型参数。对于浮点型无效果。ll
: 用于输出long long
型参数。L
: 用于输出long double
型参数。z
: 用于输出size_t
型参数。j
: 用于输出intmax_t
型参数。t
: 用于输出ptrdiff_t
型参数。
另有一些平台特定的非标准长度字符,如 I
、I32
、I64
、q
等,可参考维基百科 printf format string 词条。
类型
%
: 原样输出一个%
符号。此类型不接收任何其它字段,即只能使用%%
。d
,i
: 输出十进制signed int
型数据。二者仅在使用scanf
输入时有区别(使用%i
将0x
开头的数解析为十六进制,将0
开头的数解析为八进制)。u
: 输出十进制unsigned int
型数据。f
,F
: 以定点数表示法输出double
型数据。二者区别在于无限小数和 NaN 输出时是全小写的inf
、infinity
、nan
还是全大写的INF
、INFINITY
、NAN
。e
,E
: 以指数表示法输出double
型数据。二者区别在于字符e
的大小写。g
,G
: 根据指数自动选择定点数表示法或指数表示法。二者区别同样在于输出字符的大小写。以定点数表示法输出时与f
和F
的区别在于,可能省略小数部分最后的0
或小数点(数据为整数时)。x
,X
: 输出十六进制unsigned int
型数据。二者区别在于十六进制数的字符大小写。o
: 输出八进制unsigned int
型数据。s
: 输出以\0
结束的字符串。c
: 输出一个char
字符。p
: 输出void *
,输出格式信赖于具体实现。a
,A
: 输出十六进制double
型数据,前缀0x
或0X
。n
: 将当前的格式化字符串中已成功输出的字符数写入一个整型参数,字符数不包括\n
、\t
等转义字符。从输入输出的方向来说,此类型使用时更像是在scanf
中接受输入,只不过输入不是来自用户、而是来自系统计数。注意:此计数仅对当前格式化字符串有效,重新开始一个格式化字符串时,计数将重置为 0。例如:printf("%n", &num)
将把num
赋值为 0。
举例
前文在说明不同字段时举了对应的简单的例子,本节举几个在内核源码中实际使用的例子。
可变宽度
可变宽度对于根据不同层次输出不同的缩进有奇效,例如:
<kernel/resource.c>
seq_printf(m, "%*s%0*llx-%0*llx : %s\n",
depth * 2, "",
width, start,
width, end,
r->name ? r->name : "<BAD>");
其中的 %*s
表示输出 depth * 2
个空字符,字符个数是深度的两倍;两个 %0*llx
分别将 start
和 end
以十六进制 long long
型输出,最小宽度为 width
,宽度不足则以前导 0
补足。
十六进制输出
内核中大量使用了 #
标志配合 x
类型输出十六进制数,随便举一个例子,在 mm/slab.c
中:
seq_printf(m, "%s+%#lx/%#lx", name, offset, size);
其中的两个 %#lx
即分别将 offset
和 size
以十六进制 long
型输出,并自动附加 0x
前缀。注意:此例中的 +
号不是 +
标志而是原样输出的字符,因为并不出现在 %
与类型字段之间。
另外,内核的 printk
所使用的格式化字符串有一些自定义的格式,可参考内核文档 How to get printk format specifiers right。
以上。