格式说明符告诉 printf 如何把值变成文本,告诉 scanf 如何把文本变成值。这是 C 语言中编译器默认无法帮你的一个地方,因为函数看不到参数的类型,所以这一页就是供你对照检查的表格。
一个格式说明的一般形式是:
%[flags][width][.precision][length]conversion
只有 % 和转换字母是必需的。%-8.2lf 用到了全部五个部分。
转换说明符表
| 说明符 | 含义 | printf 参数 | scanf 参数 |
|---|---|---|---|
%d | 有符号十进制整数 | int | int * |
%i | 有符号整数 | int(同 %d) | int *(还接受 0x 和以 0 开头的八进制) |
%u | 无符号十进制 | unsigned int | unsigned int * |
%f | 十进制浮点数 | double | float * |
%F | 同 %f,INF/NAN 为大写 | double | - |
%e / %E | 科学计数法 | double | float * |
%g / %G | %f / %e 中较短的一种 | double | float * |
%a / %A | 十六进制浮点数 | double | float * |
%c | 单个字符 | int(提升后的 char) | char * |
%s | 字符串 | char *(以 '\0' 结尾) | char *(缓冲区) |
%p | 指针地址 | void * | void ** |
%x / %X | 无符号十六进制 | unsigned int | unsigned int * |
%o | 无符号八进制 | unsigned int | unsigned int * |
%n | 存储目前已写入的字符数 | int * | int * |
%% | 字面的 % | 无 | 匹配字面的 % |
%[...] | 扫描集(仅 scanf) | - | char * |
有两项需要特别提醒。%n 是写内存而不是打印,它正是格式化字符串漏洞的根源:永远不要让用户输入的文本进入格式字符串。%p 要求传入 void *;直接传 int * 是未定义行为,尽管所有实际的编译器都照样会打印出地址。
长度修饰符
长度修饰符位于精度和转换字母之间,说明参数实际有多大。
有符号转换(d i)和无符号转换(u x o)对应不同的类型:%lu 要的是 unsigned long,而不是 long。弄错了是未定义行为,不是无关紧要的小问题。
| 修饰符 | 搭配 d i(printf / scanf) | 搭配 u x o(printf / scanf) |
|---|---|---|
hh | int(来自 signed char) / signed char * | unsigned int(来自 unsigned char) / unsigned char * |
h | int(来自 short) / short * | unsigned int(来自 unsigned short) / unsigned short * |
| (无) | int / int * | unsigned int / unsigned int * |
l | long / long * | unsigned long / unsigned long * |
ll | long long / long long * | unsigned long long / unsigned long long * |
z | 有符号的 size 类型 / 指向它的指针 | size_t / size_t * |
j | intmax_t / intmax_t * | uintmax_t / uintmax_t * |
t | ptrdiff_t / ptrdiff_t * | ptrdiff_t 对应的无符号类型 |
浮点数和字符类的情况如下:
| 修饰符 | 搭配 | printf 类型 | scanf 类型 |
|---|---|---|---|
| (无) | f e g a | double | float * |
l | f e g a | double(可接受,无作用) | double * |
L | f e g a | long double | long double * |
l | c s | wint_t / wchar_t * | wchar_t * |
你实际会用到的组合:
%zu 一定要记住。 sizeof、strlen 以及标准库中所有表示大小的返回值都是 size_t,它在 64 位系统上是 8 字节,而 int 是 4 字节。printf("%d", strlen(s)) 是一个真实的 bug,在某些平台上看起来能用,所以要用 %zu。
hh 和 h 修饰符主要是为 scanf 准备的,因为那里目标的大小很重要。在 printf 中,short 在调用前就被提升为 int,所以用 %d 也能正确打印;%hd 只是表明意图。
定宽类型
对于 <stdint.h> 中的 int32_t 这类类型,可移植的说明符是 <inttypes.h> 中的宏:
PRId32 会展开为适合你的平台的字母,而相邻的字符串字面量会自动拼接,所以格式字符串被拆成了三段。这种写法很难看;另一种做法是强制转换为 long long 再用 %lld,通常更易读,而且总是正确的。
标志
标志紧跟在 % 之后,用来改变输出的样式。它们只适用于 printf。
| 标志 | 作用 | 示例 | 输出 |
|---|---|---|---|
- | 在值的后面填充,而不是前面 | 对 42 使用 %-6d| | 42 | |
+ | 有符号值总是打印符号 | 对 42 使用 %+d | +42 |
| 空格 | 在本该打印 + 的位置打印空格 | 对 42 使用 % d | 42 |
0 | 用零而不是空格填充 | 对 42 使用 %06d | 000042 |
# | 替代形式:%x 加 0x,%o 加 0,%g 保留小数点 | 对 255 使用 %#x | 0xff |
标志可以组合使用:%-+8.2f 表示最小宽度 8、两位小数、总是带符号、在数字后面填充。如果同时写了 - 和 0,以 - 为准。
宽度和精度
宽度是字段的最小尺寸:输出不足时会填充到这个宽度,但超出时永远不会被截断。精度(写在点号之后)对不同类别的含义各不相同:
| 转换 | 精度的含义 |
|---|---|
%f %e | 小数点后的位数(默认 6) |
%g | 有效数字的总位数(默认 6) |
%s | 最多打印的字符数 |
%d %i %u %x %o | 最少位数,不足时补零 |
用 * 代替其中任一个数字,就会从一个 int 参数中取值,这个参数在值本身之前读取。这样就能构建列宽在运行时才计算出来的表格。
在 scanf 中,宽度承担着一个不同而且重要得多的职责:它限制一次转换最多读取多少输入,这是 %s 和缓冲区溢出之间唯一的屏障。
printf 和 scanf 的不同之处
它们看起来很像,但有四个地方不一致。每一处都是常见的 bug 来源:
| 情况 | printf | scanf |
|---|---|---|
double 值 | %f(float 会被提升为 double) | %lf,%f 表示 float * |
| 宽度 | 最小字段尺寸,会填充 | 最多读取的输入量,会截断 |
| 参数 | 值 | 指针:&x,或者数组名 |
| 格式中的空格 | 打印一个空格 | 跳过任意数量的连续空白 |
试着输入 2.5 hello。读取它的格式和打印它的格式对同一个 double 使用了不同的字母,而 %15s 中的 15 把输入限制在 15 个字符加上结束符,正好是 word 的大小。
scanf 扫描集
%[...] 读取的是一组字符,而不是以空白分隔的单词。这是用 scanf 读取一整行的方法,不过 fgets 仍然是更好的工具:
[^\n] 表示“除换行符之外的任意字符”。%[0-9] 只读取数字,%[abc] 只读取这三个字母。和 %s 不同,扫描集不会跳过开头的空白,所以缓冲区里残留的一个换行符会让它什么也匹配不到。
用错就是未定义行为
printf 看不到参数的真实类型。它按说明符暗示的字节数读取,并按说明符的说法去解释。所以类型不匹配不是格式上的小问题,而是把内存当成了错误的东西来读:
printf("%d\n", 3.5); /* prints garbage: reads a double's bits as int */
printf("%s\n", 42); /* treats 42 as an address - crash */
printf("%d\n", strlen(s)); /* size_t read as int - wrong on 64-bit */
printf("%f\n", 3); /* int read as double - garbage */
防御只需要一个编译选项。用 -Wall 编译,GCC 就会对照参数检查每一个字面量格式字符串:
gcc -Wall -Wextra program.c -o program
上面四行都会在编译时被报告出来。这是 C 语言中最有价值的警告,上面的编辑器默认就开启了它。
关于这些说明符的实际用法,比如对齐表格、让浮点数打印得更易读、读取输入时避开经典的换行符 bug,参见 printf 和 scanf。
常见问题
C 语言中 %d 是什么意思?
%d 是有符号十进制整数(int)的转换说明符。在 printf 中,它把参数打印成十进制数字;在 scanf 中,它把十进制数字读入一个 int *。在 printf 中 %i 与它含义相同,但在 scanf 中 %i 还能接受 0x 开头的十六进制和以 0 开头的八进制。
C 语言中 %f 和 %lf 有什么区别?
在 printf 中实际上没有区别:float 参数会被提升为 double,所以 %f 两者都能打印,C99 也接受 %lf 作为同义写法。在 scanf 中区别至关重要:%f 写入 float *(4 字节),%lf 写入 double *(8 字节)。对 double 使用 %f 会破坏内存。
C 语言中怎么打印 size_t 或 long long?
size_t(sizeof 和 strlen 的返回类型)用 %zu,long long / unsigned long long 用 %lld / %llu。对它们使用 %d 是未定义行为:在 32 位系统上碰巧能用,在 64 位系统上就会出错,这也是这个 bug 能长期潜伏的原因。
C 语言中用错格式说明符会怎样?
这是未定义行为,而不是舍入误差。printf 会按说明符声称的类型来读取参数,所以 printf("%d", 3.5) 会打印出垃圾值,printf("%s", 42) 会把数字 42 当作地址去访问,通常会崩溃。用 -Wall 编译,GCC 就会对照参数检查格式字符串。