printf 是每个 C 程序员学的第一个函数,也是大多数人从未完整读过手册的函数。花十分钟了解它很值得,因为它的格式字符串本身就是一门小语言,而且写错格式说明符不是笔误,而是未定义行为。
它定义在 <stdio.h> 中,所以使用它的每个程序都以 #include <stdio.h> 开头。
格式字符串的模型
printf 一次一个字符地遍历格式字符串。不是 % 的内容都按原样打印。% 开始一个转换说明,它会取走下一个参数,并按要求的形式打印。
参数严格按位置和格式说明符对应:第一个 % 取 count,第二个取 name,依此类推。printf 无法检查你传入的是不是你承诺的类型,它看不到类型,所以格式字符串是你必须遵守的约定。这正是值得打开 -Wall 的原因:GCC 会在编译时读取字面量格式字符串,并在参数不匹配时发出警告。
没有什么会自动换行。printf("Hello") 会让光标停在 o 后面,所以几乎每个格式字符串都以 \n 结尾。
日常用到的格式说明符
这五个能覆盖大多数代码:
| 格式说明符 | 打印内容 | 调用示例 | 输出 |
|---|---|---|---|
%d | 有符号 int | printf("%d", -42) | -42 |
%f | double | printf("%f", 2.5) | 2.500000 |
%s | 字符串(char *) | printf("%s", "hi") | hi |
%c | 单个字符 | printf("%c", 'A') | A |
%p | 指针地址 | printf("%p", (void*)&x) | 0x7ffd... |
这段代码里藏着两条规则。字面量 % 要写成 %%,格式字符串末尾单独一个 % 是未定义行为。另外 %p 要求 void *,所以要对指针做强制转换;直接传 int * 严格来说是未定义行为,尽管实际中打印出来没问题。
完整的表格,包括每个格式说明符、每个长度修饰符以及 scanf 对应的一列,都在格式说明符页面。本页只讲你常用的那些。
宽度、精度与对齐
在 % 和字母之间,你可以放标志、宽度和精度。这就是把一个循环变成表格的方法。
宽度是最小值,而不是最大值:用 %3d 打印 123456,六位数字全都会输出。精度对不同类型含义不同:对 %f 是小数点后的位数,对 %s 是最多输出的字符数,对 %d 是最少的数字位数。
两者都可以在运行时用 * 提供,它会读取一个 int 参数:
修改 width 就能重新排布整张表。这是在 C 中打印对齐列的标准做法,不需要任何字符串填充辅助函数。
合理地打印浮点数
不带精度的 %f 总是打印小数点后六位,这几乎从来不是你想要的:
三种选择,以及各自的经验法则:
%.Nf用于金额、测量值等小数位数天然固定的数据。货币用%.2f就对了。%e用于科学计数法,格式总是d.dddddde±dd。%g用于事先不知道数量级的情况。它会在%f和%e中选较短的那个,并去掉末尾的零,记录一个你没见过的值时,这是最稳妥的默认选择。
有一个陷阱值得明确指出:printf 中没有 %lf。float 参数在 printf 看到之前就已被提升为 double,所以 %f 两者都能处理,%lf 只在 scanf 中有意义,因为那里目标变量的大小很重要。(C99 确实接受 printf 中的 %lf 作为同义写法,但习惯这样写的人往往会在 scanf 中写成 %f,那就真的出错了。)
其他宽度的整数
%d 用于 int。更大的类型或无符号类型需要长度修饰符,写错了是未定义行为,而不仅仅是舍入误差:
size_t 用 %zu,这是最常被写错的一个:sizeof 返回的是 size_t,不是 int,所以 printf("%d", sizeof x) 是个 bug,只是恰好在 32 位系统上能用,在 64 位系统上就会出问题。
返回值
printf 返回它写出的字符数,失败时返回负数。这个返回值通常被忽略,大多数情况下也没关系,但当你需要知道输出有多宽时,它确实很有用:
相关的 sprintf 和 snprintf 把内容写入缓冲区而不是屏幕,并返回生成的长度。请始终优先使用 snprintf,它接收缓冲区大小,不会溢出:
snprintf 返回的是完整字符串本来需要的长度,所以返回值大于或等于缓冲区大小,就说明输出被截断了。
格式字符串漏洞
这是 printf 唯一的安全问题,而且很严重。永远不要把不是你写的数据当作格式字符串传入:
/* DANGEROUS - never do this */
printf(user_input);
/* Correct */
printf("%s", user_input);
如果 user_input 中含有 %s,printf 就会读取一个从未传入的参数,并顺着它找到的那个看起来像指针的垃圾值去访问,通常会导致崩溃。重复的 %x 会把栈内容输出到屏幕上,其中可能包括密码或密钥。而 %n 会把字符数写入一个参数,历史上它让这个问题演变成了任意内存破坏和远程代码执行。
这条规则是绝对的,而且毫无代价:格式字符串永远是你自己写的字面量。任何可变的内容都作为参数传入。GCC 的 -Wformat-security 会标记这个错误,它包含在 -Wall -Wextra 中。
缓冲:为什么输出有时出现得晚
printf 不会立即写到终端。输出是有缓冲的:缓冲区满了、在终端上遇到换行符,或者程序正常退出时才会刷新。这带来两个后果:
- 崩溃的程序可能会丢失“已经打印过”的输出。如果你在用
printf调试,而最后一条消息始终没有出现,那么崩溃发生在那一行之后,而不是之前。加上fflush(stdout),或者把调试输出发送到无缓冲的stderr。 fprintf(stderr, ...)是打印错误的正确方式。它没有缓冲,而且输出到一个独立的流,用户可以把它和正常输出分开重定向。
常见问题
C 语言中的 printf 是怎么工作的?
printf 接收一个格式字符串,并逐个字符扫描它。普通字符原样输出;每个 % 开始一个转换说明,它会消耗后面的一个参数,并按要求的形式打印出来。printf("%d apples\n", 5) 会打印 5 apples 和一个换行。
C 语言中怎么打印保留 2 位小数的浮点数?
使用精度:printf("%.2f\n", 3.14159) 打印 3.14。点后面的数字表示小数点后保留几位。不带精度的 %f 总是打印 6 位,这就是为什么没有格式化的浮点数看起来那么杂乱。
为什么 printf 会打印乱码或崩溃?
几乎总是因为格式说明符和参数不匹配。printf("%d", 3.5) 或 printf("%s", 42) 是未定义行为:printf 看不到参数的真实类型,它只相信格式字符串。编译时加上 -Wall,GCC 就会检查格式和参数是否匹配。
为什么 printf(user_input) 很危险?
如果用户输入的文本中含有 % 序列,printf 就会执行它们,去读取根本没有传入的参数,从而泄露栈内存,用 %n 甚至能写入内存。这就是经典的格式字符串漏洞。请始终写成 printf("%s", user_input)。