Menu

C 语言 printf 用法:格式字符串、格式说明符与输出浮点数

printf 在 C 语言中到底是怎么工作的:格式字符串的模型、日常用到的格式说明符、用宽度和精度对齐输出、合理地打印浮点数、返回值,以及为什么 printf(user_input) 是一个安全漏洞。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

printf 是每个 C 程序员学的第一个函数,也是大多数人从未完整读过手册的函数。花十分钟了解它很值得,因为它的格式字符串本身就是一门小语言,而且写错格式说明符不是笔误,而是未定义行为。

它定义在 <stdio.h> 中,所以使用它的每个程序都以 #include <stdio.h> 开头。

格式字符串的模型

printf 一次一个字符地遍历格式字符串。不是 % 的内容都按原样打印。% 开始一个转换说明,它会取走下一个参数,并按要求的形式打印。

参数严格按位置和格式说明符对应:第一个 % 取 count,第二个取 name,依此类推。printf 无法检查你传入的是不是你承诺的类型,它看不到类型,所以格式字符串是你必须遵守的约定。这正是值得打开 -Wall 的原因:GCC 会在编译时读取字面量格式字符串,并在参数不匹配时发出警告。

没有什么会自动换行。printf("Hello") 会让光标停在 o 后面,所以几乎每个格式字符串都以 \n 结尾。

日常用到的格式说明符

这五个能覆盖大多数代码:

格式说明符打印内容调用示例输出
%d有符号 intprintf("%d", -42)-42
%fdoubleprintf("%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)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始