11.C 语言 printf 打印函数详解:格式说明符、宽度精度与安全输出
文章摘要: 本文从最小 C 程序出发,系统讲解 printf 的声明、格式字符串、参数对应关系,以及整数、浮点数、字符、字符串和不同进制的输出。进一步拆解标志、宽度、精度与长度修饰符,解释默认参数提升、%f 与 %lf、size_t、固定宽度整数和指针输出。通过完整程序演示对齐表格、格式化缓冲区与截断检测,并补充返回值、输出缓冲、中文编码、格式字符串安全及常见故障排查。
上一篇介绍了 C 语言常量、字符字符串与宏定义。本篇继续说明这些数据怎样输出:先保证格式与参数类型匹配,再学习宽度、精度和对齐,最后掌握输出缓冲区的容量与错误检查。正文以 C11/C17 为基础,涉及较新标准或特定工具链的内容会单独注明。
一、先认识一个完整 C 程序
c
#include <stdio.h>
int main(void)
{
printf("Hello, C!\n");
return 0;
}
1. #include <stdio.h>
stdio.h 声明标准输入输出接口,包括 printf、puts、putchar、fprintf 等。编译器需要这些声明来理解函数的参数与返回类型。
头文件提供接口声明,函数实现通常来自 C 标准库。不能因为包含了头文件,就把"函数实现已经复制进源文件"作为通用解释。
2. int main(void)
在常见的有宿主 C 环境中,main 是程序入口函数。int 表示返回整数状态;void 明确表示不接收参数。
对 C11/C17 初学代码,建议写 int main(void),避免把函数声明中空括号的旧语义与明确无参数的写法混淆。
3. 分号、字符串与换行
printf(...) 是函数调用,作为表达式语句时以分号结束。双引号内是字符串字面量,\n 表示换行字符,不是要打印的反斜杠和字母 n。
return 0; 向运行环境报告成功。无参数程序也可以使用 <stdlib.h> 中的 EXIT_SUCCESS 表达成功状态。
4. 编译运行
将代码保存为 hello.c:
bash
gcc hello.c -std=c11 -Wall -Wextra -Wpedantic -o hello
./hello
输出:
text
Hello, C!
后续示例同样使用 int main(void) 与显式返回值。
二、printf 的三个组成部分
常见调用结构:
c
printf("普通文本 + 格式说明符", 参数1, 参数2, ...);
例如:
c
printf("age = %d\n", 20);
解释过程:
text
age = 普通文本
%d 按有符号十进制格式转换对应的 int 参数
\n 换行
20 与 %d 对应的参数
输出 age = 20,然后换行。
函数声明可简化理解为:
c
int printf(const char *format, ...);
省略号表示可以接收数量可变的参数。函数本身不会根据每个参数自动选择输出类型,而是依照格式字符串解释参数,所以类型匹配必须由程序保证。
三、格式说明符按顺序消耗参数
c
printf("%d + %d = %d\n", 10, 20, 30);
对应关系:
text
第一个 %d -> 10
第二个 %d -> 20
第三个 %d -> 30
输出:
text
10 + 20 = 30
参数也可以是表达式:
c
printf("%d + %d = %d\n", 10, 20, 10 + 20);
printf 不会读懂字符串中的加号并替你计算,最后一个结果来自传入的表达式。
1. 参数不足
c
/* 错误演示,不应运行: */
printf("%d %d\n", 10);
第二个 %d 没有对应实参,产生未定义行为,不能依赖它显示某个"随机数"。
2. 多余参数
如果提供的参数多于格式需要,标准规定多余参数仍会求值,但随后被忽略。因此不应把"多一个参数"与"少一个参数"都简单归为同一种错误。
实际代码应清理多余参数,编译器也通常会发出警告。后面用 * 指定宽度或精度时,它们同样会消耗实参,不能只数末尾的转换字母。
3. 参数求值顺序
不要在同一次调用中,用多个参数修改同一个变量:
c
/* 错误演示,不应运行: */
printf("%d %d\n", i++, i++);
在本文采用的 C11/C17 规则下,这样的未排序修改会产生未定义行为。需要观察变化时,先用独立语句完成修改,再调用输出函数。
四、常用格式说明符与参数类型
| 格式 | 需要的参数类型或数据 | 显示形式 |
|---|---|---|
%d、%i |
int |
有符号十进制 |
%u |
unsigned int |
无符号十进制 |
%o |
unsigned int |
无符号八进制 |
%x、%X |
unsigned int |
无符号十六进制 |
%c |
int,输出时转换为 unsigned char |
一个字符 |
%s |
指向有效字符序列的指针,通常要求以空字符终止 | 字符串 |
%f |
double |
十进制小数形式 |
%e、%E |
double |
科学计数法 |
%g、%G |
double |
根据数值与精度选择形式 |
%ld |
long int |
有符号长整数 |
%lld |
long long int |
有符号长长整数 |
%zu |
size_t |
无符号大小值 |
%p |
void * |
实现规定的指针表示 |
%% |
不消耗参数 | 一个百分号 |
%d 和 %i 在 printf 中都输出有符号十进制。不要把 scanf 中两者的输入差异直接套到输出函数。C11 草案 N1570,7.21.6.1
五、整数输出:值、类型与进制
1. 有符号十进制
c
int temperature = -12;
printf("temperature = %d\n", temperature);
输出:
text
temperature = -12
2. 无符号十进制
c
unsigned int count = 42U;
printf("count = %u\n", count);
输出:
text
count = 42
不要因为当前值很小,就忽略它的有符号或无符号类型。
3. 八进制和十六进制
c
unsigned int value = 42U;
printf("decimal = %u\n", value);
printf("octal = %o\n", value);
printf("hex = %x\n", value);
printf("HEX = %X\n", value);
输出:
text
decimal = 42
octal = 52
hex = 2a
HEX = 2A
%x 与 %X 的区别主要是十六进制字母的大小写。它们都按无符号整数格式转换。
4. 想查看负数的无符号转换结果
c
int value = -10;
printf("signed = %d\n", value);
printf("converted = %u\n", (unsigned int)value);
printf("hex = %X\n", (unsigned int)value);
强制转换使后两个参数的类型明确符合格式要求。在常见 32 位 unsigned int 平台,结果分别包含 4294967286 与 FFFFFFF6。这里观察的是数值转换后的结果,不是让 printf 任意猜测参数类型。
六、浮点输出:%f、%e 与 %g
1. %f 默认保留六位小数
c
printf("%f\n", 12.5);
输出:
text
12.500000
控制小数位数:
c
printf("%.2f\n", 12.5);
输出 12.50。%.2f 控制显示时的小数位数,并不修改原变量。
2. %e 表示十进制科学计数法
c
printf("%e\n", 314.5);
典型输出:
text
3.145000e+02
其含义是 3.145000 × 10^2。指数前的 e 是格式标志,不是指数底数。
3. %g 适合紧凑显示
c
printf("%g\n", 12.5);
printf("%.3g\n", 12345.0);
典型输出:
text
12.5
1.23e+04
%g 的精度表示有效数字数量,不是小数点后位数;它按规则选择普通小数或科学计数法,并在通常情况下去掉无意义的尾随零。
七、为什么 float 和 double 都可以用 %f
printf 是可变参数函数。进入省略号参数时,会发生默认参数提升:
float提升为double;char、short等较窄整数类型按照整数提升规则转换,常见平台上提升为int。
所以:
c
float first = 1.25f;
double second = 2.5;
printf("%f %f\n", first, second);
两个参数实际都按 double 传递,使用 %f 是正确的。
1. printf 中的 %lf
在 C99 及后续标准中,浮点输出的 l 修饰符没有效果。因此对 printf 而言,%f 和 %lf 都接收 double。
c
printf("%f\n", second);
printf("%lf\n", second);
它们的含义相同。初学输出代码统一使用 %f 更简单。
2. long double 使用 %Lf
c
long double third = 3.75L;
printf("%Lf\n", third);
大写 L 才表示 long double。不要把 %lf 当成 long double 输出格式。
3. scanf 的规则不同
输入函数接收指向变量的指针:scanf 的 %f 对应 float *,%lf 对应 double *。指针不会像 float 数值那样自动提升,因此输入、输出不能共用同一套简化记忆。C11 草案 N1570,7.21.6.1、7.21.6.2
八、字符既可以显示字形,也可以显示编码
c
printf("%c : %d\n", 'A', 'A');
在 ASCII 环境下输出:
text
A : 65
前一个 %c 把编码值作为字符输出,后一个 %d 把相同整数显示为十进制。printf 不会改变参数本身,只改变它的显示形式。
下面这个完整程序对应几个常见转换:
c
#include <ctype.h>
#include <stdio.h>
int main(void)
{
printf("%c,%d\n", 'A', 'A');
printf("%c,%d\n", tolower('A'), tolower('A'));
printf("%c,%d\n", '1', '1' - '0');
printf("%c,%d\n", '\141', '\141');
printf("%c,%d\n", '\x50', '\x50');
return 0;
}
ASCII 环境下输出:
text
A,65
a,97
1,1
a,97
P,80
第三行的两个参数不同:'1' 是字符编码,'1' - '0' 才是数字 1。
字母编码及转义得到的字形与执行字符集有关;数字字符连续的规则则由 C 标准保证。
九、字符串输出:终止符决定结束位置
c
printf("%s\n", "hello");
输出 hello。%s 从给定指针指向的位置读取字符,遇到第一个 \0 时停止,终止符自身不会作为可见字符输出。
c
char text[] = "hello";
printf("%s\n", text);
字符数组在这个调用中转换成指向首元素的指针。
以下情况需要避免:
- 把字符值交给
%s,例如printf("%s", 'A'); - 把空指针交给
%s,不能依赖某个平台显示(null); - 对未以空字符终止、且未使用合适精度限制的字符数组使用
%s。
最后一种情况可能使函数继续访问数组边界之外的内存。后文会介绍如何通过精度限制输出一段已知长度的字符数据,并说明这种限制需要满足的条件。
十、百分号与反斜杠分别由谁处理
1. 打印百分号
c
printf("progress = 80%%\n");
输出:
text
progress = 80%
%% 不消耗参数,不能写成 \% 来代替标准格式字符串语法。
2. 打印反斜杠
c
printf("path = C:\\temp\\demo.txt\n");
输出:
text
path = C:\temp\demo.txt
两层规则可以这样理解:
text
C 编译器先处理字符串中的 \n、\\、\" 等转义
printf 运行时再处理 %d、%s、%% 等格式
想打印字面文本 \n,应在源代码里写 "\\n"。
十一、类型不匹配不会自动转换
c
/* 错误演示,不应运行: */
printf("%d\n", 12.5);
12.5 是 double,%d 需要 int。格式字符串不会把参数先转成整数,调用产生未定义行为。
如果确实需要转换,应显式表达:
c
printf("%d\n", (int)12.5);
输出 12。这里是 C 表达式先完成转换,再把 int 传入函数。
另一个常见问题:
c
/* 错误演示,不应运行: */
printf("%f\n", 10);
整数 10 不会因为用了 %f 就变成 double。可写成 10.0 或显式转换为 double。
编译时建议启用:
bash
gcc main.c -std=c11 -Wall -Wextra -Wpedantic -Wformat=2 -o main
当格式字符串可在编译期分析时,编译器通常能够发现类型和数量不匹配,但仍需要阅读并处理警告。
十二、格式说明符的完整结构
普通格式说明符可写成:
text
%[标志][宽度][.精度][长度修饰符]转换说明符
方括号表示对应部分可省略,不是要把方括号直接写进字符串。
例如:
c
printf("%+08.2f\n", 12.5);
拆解:
text
% 开始格式说明
+ 非负数也显示正号
0 使用前导零填充
8 最小字段宽度
.2 对 f 来说,小数点后显示 2 位
f double 按十进制小数形式输出
输出为:
text
+0012.50
符号、小数点和数字都计算在字段宽度之内。C11 草案 N1570,7.21.6.1
十三、宽度是最小字段宽度
c
printf("|%6d|\n", 42);
输出:
text
| 42|
42 占两个位置,字段至少宽 6,所以左侧补 4 个空格。竖线帮助观察边界,不属于这个转换字段。
1. 宽度不会截断数值
c
printf("|%3d|\n", 12345);
输出仍然是:
text
|12345|
宽度不足时,输出会扩大以容纳内容。它不是最大长度限制,不能依靠宽度防止字符串或数值超过缓冲区。
2. 字符串也可以设置宽度
c
printf("|%8s|\n", "cat");
输出为 | cat|。字符串不足 8 个字节时,左侧补空格;超过 8 时不会自动截断。
3. 宽度不等于终端显示列数
普通 %s 的宽度与转换中的字符单位有关,不能保证中文、组合字符和宽字符在终端中严格按列对齐。UTF-8 的字节数、Unicode 字符数与显示列数是不同的量。
使用纯 ASCII 表格最容易预测布局;涉及复杂文本时,需要额外的显示宽度计算。
十四、标志:左对齐、补零、符号与替代形式
1. -:左对齐
c
printf("|%-6d|\n", 42);
printf("|%-8s|\n", "cat");
输出:
text
|42 |
|cat |
- 把内容放到字段左侧,在右侧补空格。它适合名称列、描述列,右对齐则常用于整数和金额列。
c
printf("%-10s %6s %8s\n", "name", "count", "price");
同一行不同字段可以使用不同对齐方式。
2. 0:前导零填充
c
printf("%06d\n", 42);
printf("%06d\n", -42);
输出:
text
000042
-00042
负号仍在最前面,零补在符号后面,而不是写成 000-42。
十六进制带前缀时:
c
printf("%#08x\n", 42U);
输出:
text
0x00002a
宽度 8 包含两个前缀字符。0 适合固定格式的编号、计数和十六进制字段,但不要对 %s 等不支持该标志的转换随意使用。
几个组合规则:
- 同时出现
-与0时,-起作用,0被忽略; - 对整数转换指定精度时,
0标志被忽略; - 对浮点转换,精度与前导零填充可以同时发挥作用。
因此"写了 0 就一定补零"并不成立。
3. 正数也显示符号
c
printf("%+d, %+d\n", 42, -42);
输出:
text
+42, -42
+ 适合显示温度变化、坐标偏移和差值。
4. 为非负数预留符号位置
c
printf("|% d| |% d|\n", 42, -42);
输出:
text
| 42| |-42|
空格标志使非负数前面出现空格,负数仍显示负号。若同时指定 +,则使用 +,空格标志被忽略。
5. 替代形式
# 的作用随转换变化:
| 转换 | # 的效果 |
|---|---|
%o |
必要时增加精度,使首位为 0 |
%x、%X |
非零结果添加 0x、0X |
%f、%e 等浮点形式 |
即使精度为 0,也保留小数点 |
%g、%G |
保留小数点,不移除通常会省略的尾随零 |
c
printf("%#o %#x %#X\n", 42U, 42U, 42U);
输出:
text
052 0x2a 0X2A
# 让八进制形式具有前导 0,让非零十六进制结果具有 0x 或 0X 前缀。十六进制值为零时,%#x 默认输出 0,不会强行输出 0x0。
如果界面始终需要 0x,可以明确写:
c
printf("0x%X\n", 0U); /* 输出 0x0 */
浮点格式也可以使用替代形式:
c
printf("%.0f | %#.0f\n", 12.0, 12.0);
printf("%.4g | %#.4g\n", 12.0, 12.0);
常见输出:
text
12 | 12.
12 | 12.00
十五、精度的含义取决于转换类型
不能把所有 %.n 都解释为"小数点后 n 位"。
| 转换 | 精度含义 |
|---|---|
%d、%i、%u、%o、%x |
最少显示多少个数字 |
%f、%e |
小数点后显示多少位 |
%g |
有效数字数量 |
%s |
最多输出多少字节 |
1. 整数精度
c
printf("|%.4d|\n", 42);
printf("|%8.4d|\n", 42);
输出:
text
|0042|
| 0042|
.4 要求至少 4 位数字,补零得到 0042;宽度 8 再在左边补 4 个空格。负号不计入"数字位数",但计入字段总宽度。
2. 零值与零精度
c
printf("zero=|%.0d|\n", 0);
输出:
text
zero=||
指定整数精度为 0 且值为 0 时,数字序列为空。八进制的替代形式存在例外:%#.0o 对零仍输出一个 0。
3. 浮点精度与舍入
c
printf("%.2f\n", 12.375);
常见默认舍入环境下输出 12.38,不是简单把后面的数字截掉。实际转换对存储的浮点值进行格式化,因此既受浮点近似影响,也可能受舍入模式影响。
例如十进制 2.675 在常见二进制浮点类型中未必精确保存为数学上的 2.675,不能只看源码数字就断定保留两位必定输出 2.68。
4. %g 的有效数字
c
printf("%.3g\n", 12345.0);
printf("%.3g\n", 12.345);
典型输出:
text
1.23e+04
12.3
对 %g,精度 0 按 1 处理。采用普通小数还是科学计数法,有明确的指数判断规则:设有效精度为 P,当所对应科学计数形式的指数小于 -4 或大于等于 P 时使用科学计数法,否则使用普通小数形式。
十六、字符串精度可以限制输出长度
c
printf("|%.3s|\n", "hello");
printf("|%8.3s|\n", "hello");
输出:
text
|hel|
| hel|
先由精度限制字符串内容,再由宽度决定填充。%.3s 最多输出 3 字节,但遇到空字符仍提前停止。
1. 输出没有终止符的固定长度数组
c
char raw[5] = {'H', 'E', 'L', 'L', 'O'};
printf("%.5s\n", raw);
这里数组确实有 5 个可访问元素,精度也是 5,因而不需要继续寻找第 6 个位置的终止符。
精度限制不能让无效指针变得有效。若只提供 3 字节数组,却允许读 5 字节,仍可能越界。
2. UTF-8 的截断问题
普通 %s 精度按字节限制,可能截在一个多字节字符的中间,导致输出乱码。它不是"最多几个汉字"的规则。中文内容需要按编码边界决定安全的截断位置。
十七、用 * 提供动态宽度与精度
c
printf("|%*.*f|\n", 8, 2, 12.5);
输出:
text
| 12.50|
参数按以下顺序消耗:
text
第一个 int:宽度 8
第二个 int:精度 2
第三个 double:数值 12.5
每个 * 都要求一个 int 参数,不能直接用 size_t 替代。
1. 负宽度
c
printf("|%*d|\n", -6, 42);
等价于左对齐、宽度 6,输出 |42 |。
2. 负精度
精度参数为负时,相当于省略精度。对 %s 而言,这意味着恢复寻找终止符的行为,不能把负精度当作"输出 0 个字符"。
3. 动态输出一段字符数据
c
printf("%.*s\n", length, text);
其中 length 必须是有效的非负 int,text 指向至少该长度可访问的字符数据,或在该范围内存在终止符。
如果长度来自 size_t,先判断它是否不大于 INT_MAX,再转换为 int。仅写 (int)length 并不能证明转换后的值仍正确。
十八、长度修饰符决定如何解释类型
| 格式示例 | 对应类型或规则 |
|---|---|
%hhd |
经整数提升传入的值,转换为 signed char 后输出 |
%hd |
经整数提升传入的值,转换为 short 后输出 |
%d |
int |
%ld |
long int |
%lld |
long long int |
%u |
unsigned int |
%lu |
unsigned long int |
%llu |
unsigned long long int |
%zu |
size_t |
%td |
ptrdiff_t |
%jd |
intmax_t |
%Lf |
long double |
对 %x、%o 等整数输出,同样可以组合相应的长度修饰符,例如 %lx 对应 unsigned long。
不能因为某平台上 int 和 long 都占 4 字节,就认为 %d 与 %ld 随意互换。格式要求的是类型,不只是存储大小。
十九、sizeof 为什么用 %zu
sizeof 的结果类型是 size_t,它是无符号整数类型,实际对应的基础类型取决于实现。
c
printf("sizeof(int) = %zu\n", sizeof(int));
其中 z 表示与大小类型相关的长度修饰,u 表示无符号十进制。
%zd 需要的是与 size_t 对应的有符号整数类型,不是直接输出 size_t 的推荐格式。课件中若用 %zd 输出 sizeof,应改为 %zu。
把结果强制转换为 int 再用 %d,可能在大对象上丢失范围;已经有对应的标准格式时,直接使用它更合适。
二十、固定宽度整数使用 <inttypes.h>
当实现提供 uint32_t 等类型时,它们的实际基础类型可能是 unsigned int,也可能是其他满足宽度的类型。
使用格式宏可以避免猜测:
c
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int main(void)
{
uint32_t value = UINT32_C(0xA1B2C3D4);
printf("decimal = %" PRIu32 "\n", value);
printf("hex = 0x%08" PRIX32 "\n", value);
return 0;
}
输出:
text
decimal = 2712847316
hex = 0xA1B2C3D4
这里的相邻字符串会在翻译期间连接,例如 PRIu32 展开为目标平台需要的转换后缀。
恰好固定宽度的类型并非所有 C 实现都必须提供;对协议和硬件字段使用它们时,要确认目标实现支持。
二十一、地址输出使用 %p
c
#include <stdio.h>
int main(void)
{
int value = 42;
printf("address = %p\n", (void *)&value);
return 0;
}
%p 需要 void *,所以显式转换对象指针。因为 printf 使用可变参数,不能依赖函数原型替你完成这一转换。
地址的显示形式由实现决定,可能有前缀,也可能采用其他写法。不能把某次运行的地址写成固定输出,程序重新启动后地址也可能变化。
%p 面向对象指针;函数指针并不保证能以同样方式转换为 void * 并可移植输出。
二十二、二进制与十六进制浮点输出
1. 二进制 %b 的兼容性
C23 增加了无符号二进制输出相关的 %b 转换,但编译器支持 C23 与实际链接的 C 库支持该格式,是两件事。
按 C11/C17 编写可移植程序时,不使用 %b;需要二进制文本,可以用移位和掩码自行输出。即便启用较新的语言模式,也应确认目标标准库已经实现相应功能。C23 草案 N3096,格式化输出章节
2. %a 输出十六进制浮点形式
c
printf("%a\n", 3.0);
常见输出为 0x1.8p+1,表示 1.5 乘以 2^1。它便于观察二进制浮点值,不是把整数按十六进制输出的 %x。
实现对某些等价形式的选择可能不同,不应把具体文本布局当成所有实现唯一允许的结果。
二十三、printf 返回什么
成功时,printf 返回输出的字符数量;发生输出或编码错误时返回负值。普通窄字符输出可把计数理解为输出的字节数量,不是终端显示列数。
c
#include <stdio.h>
int main(void)
{
int written = printf("ABC\n");
if (written < 0) {
return 1;
}
printf("written = %d\n", written);
return 0;
}
正常输出:
text
ABC
written = 4
换行也计入输出数量。字符串源码中的 \n 占两个可见源字符,但执行时是一个换行字符。
成功返回不等于数据已经物理写到显示设备或磁盘,部分数据可能仍在标准库输出缓冲区中。
二十四、为什么输出有时没有立即出现
printf 输出到标准输出流 stdout,不保证每次调用都马上出现在屏幕上。
常见运行环境中,连接终端的标准输出通常按行缓冲;输出到文件或管道时通常使用完整缓冲。因此换行经常能让终端输出及时出现,但它不是所有情况下都强制刷新缓冲区的保证。
如果提示语没有换行,可以主动刷新:
c
printf("Please enter a value: ");
fflush(stdout);
正式程序可以检查 fflush 的返回值以处理输出错误。它刷新标准库缓冲区,也不等于要求操作系统把文件数据同步到物理介质。
正常结束程序时,标准库会处理相应输出流;异常终止则不能保证尚未刷新的内容完整保留。
二十五、同一套格式可以写到不同目标
| 函数 | 输出目标 | 是否限制数组容量 |
|---|---|---|
printf |
stdout |
不涉及调用方字符数组 |
fprintf |
指定输出流 | 不涉及调用方字符数组 |
sprintf |
字符数组 | 没有容量参数 |
snprintf |
字符数组 | 通过容量参数限制写入 |
在终端执行:
bash
./hello > output.txt
cat output.txt
程序的 printf 输出被重定向到文件。可见,"打印函数"更准确的描述是"格式化写入标准输出流"。
需要把错误信息送到标准错误流时:
c
fprintf(stderr, "Invalid data\n");
需要只输出一行字符串时,也可以使用:
c
puts("hello");
puts 会自动追加换行,并且不会把文本里的 % 当作格式说明符。putchar('A') 则输出单个字符,不会自动换行。
如果需要生成文件名、日志文本或协议字段,而不是马上打印,通常应选择 snprintf。
二十六、snprintf 的容量与返回值
c
char output[32];
int result = snprintf(output, sizeof(output), "id=%04u", 42U);
第二个参数是缓冲区总容量,包含为结尾空字符预留的空间。容量大于 0、格式化未发生错误时,函数至多写入 容量-1 个内容字符,并用空字符结束。
返回值不是"实际写进数组的字符数",而是缓冲区足够大时本来需要写入的字符数,不包含终止符。
检查顺序:
c
if (result < 0) {
/* 格式化或编码失败。 */
} else if ((size_t)result >= sizeof(output)) {
/* 发生截断,完整内容需要 result + 1 字节容量。 */
} else {
/* 完整输出已保存。 */
}
要先判断负值,再转换为 size_t,否则负值会变成很大的无符号数,错误类别容易混淆。
1. 截断示例
c
char small[8];
int needed = snprintf(small, sizeof(small), "value=%d", 12345);
完整文本 value=12345 长 11,需要 12 字节容量。实际数组只能保存 7 个内容字符和终止符,所以保存 value=1,返回值仍是 11。
2. 不要把源和目标重叠
c
/* 错误演示,不应运行: */
snprintf(output, sizeof(output), "%s suffix", output);
这里写入目标与读取源发生重叠,不能用它给字符串原地追加文本。应另备缓冲区,或在严格检查剩余容量的前提下从已有内容后面写入。
二十七、格式字符串应保持明确
当要输出外部文本时:
c
printf("%s", text);
或者:
c
fputs(text, stdout);
不要直接把任意文本当作格式字符串:
c
/* 错误用法:text 中的百分号可能被解释为转换指令。 */
printf(text);
例如文本包含 %d,调用却没有对应参数,就可能产生未定义行为。格式字符串与被输出的数据应在调用中表达清楚。
%n 是一个特殊转换:它不输出内容,而是通过指针写入截至当前位置的输出数量。初学者了解它存在即可,普通日志通常不需要使用它。
二十八、完整程序:基础输出、对齐与截断检测
1. 基础输出示例
保存为 printf_basics.c:
c
#include <stdio.h>
int main(void)
{
int left = 10;
int right = 20;
unsigned int value = 42U;
float first = 1.25f;
double second = 2.5;
long double third = 3.75L;
char letter = 'A';
const char *message = "hello";
printf("Hello, C!\n");
printf("%d + %d = %d\n", left, right, left + right);
printf("character = %c\n", letter);
printf("string = %s\n", message);
printf("float = %.2f, double = %.2f\n", first, second);
printf("long double = %.2Lf\n", third);
printf("scientific = %.2e\n", 314.0);
printf("compact = %.3g\n", 12345.0);
printf("bases = %u, %o, %x, %X\n", value, value, value, value);
printf("prefixes = %#o, %#x, %#X\n", value, value, value);
printf("progress = 80%%\n");
printf("array bytes = %zu\n", sizeof("hello"));
return 0;
}
输出:
text
Hello, C!
10 + 20 = 30
character = A
string = hello
float = 1.25, double = 2.50
long double = 3.75
scientific = 3.14e+02
compact = 1.23e+04
bases = 42, 52, 2a, 2A
prefixes = 052, 0x2a, 0X2A
progress = 80%
array bytes = 6
2. 对齐、精度与截断检测
保存为 printf_advanced.c:
c
#include <stdio.h>
int main(void)
{
char raw[5] = {'H', 'E', 'L', 'L', 'O'};
char output[32];
char small[8];
printf("|%6d|\n", 42);
printf("|%-6d|\n", 42);
printf("|%06d|\n", -42);
printf("|%8.4d|\n", 42);
printf("|%+08.2f|\n", 12.5);
printf("|%*.*f|\n", 8, 2, 12.5);
printf("hex = %#08x\n", 42U);
printf("text = %.5s\n", raw);
printf("zero=|%.0d|\n", 0);
printf("%-10s %6s %8s\n", "name", "count", "price");
printf("%-10s %6u %8.2f\n", "sensor", 3U, 12.5);
int result = snprintf(output, sizeof(output), "id=%04u", 42U);
if (result < 0 || (size_t)result >= sizeof(output)) {
fputs("Cannot create complete text\n", stderr);
return 1;
}
printf("buffer = %s, length = %d\n", output, result);
int needed = snprintf(small, sizeof(small), "value=%d", 12345);
if (needed < 0) {
return 1;
}
printf("small = %s, needed = %d\n", small, needed);
printf("truncated = %d\n", (size_t)needed >= sizeof(small));
return 0;
}
输出:
text
| 42|
|42 |
|-00042|
| 0042|
|+0012.50|
| 12.50|
hex = 0x00002a
text = HELLO
zero=||
name count price
sensor 3 12.50
buffer = id=0042, length = 7
small = value=1, needed = 11
truncated = 1
最后的截断是为演示容量不足而有意保留,代码已检测它。某些 GCC 版本会对这个编译期可预测的截断发出 -Wformat-truncation 警告;这条警告对应本例要观察的情况,正式业务中应扩大缓冲区或按实际所需容量处理。
二十九、常见输出问题的排查
1. 先检查语法和类型
先确认完整程序能够编译,检查头文件、分号、引号和括号。再逐项核对格式说明符、参数数量和实际类型,尤其是整数与浮点数、字符与字符串、size_t 与 int 的区别。
类型正确但输出迟迟不出现时,再检查标准输出是否被重定向,以及是否需要调用 fflush(stdout)。
2. 中文乱码与编码
输出乱码时,应检查源文件编码、编译器采用的输入与执行字符集、字符串实际字节,以及终端的解码方式。把源文件保存为 UTF-8,并让相关环节使用一致编码,通常比直接修改全局编辑器配置更容易定位问题。
在 Vim 中可以先查看:
vim
:set encoding?
:set fileencoding?
:set fileencodings?
encoding 是内部编码;fileencoding 是当前文件写出编码;fileencodings 用于读取文件时的候选检测。打开文件后简单改变 fileencoding,不等于已正确修复读取时发生的错误;应在确认原始文件编码后重新读取或转换,并保留必要的原文件副本。
Unix 终端环境还可以通过 locale 检查本地化设置。printf 对普通 %s 的处理不会自动把不匹配的编码转成终端需要的编码。
3. MinGW 打印库兼容设置
如果在 Windows 的某些 MinGW 配置中遇到 %zu 警告或 %Lf 输出异常,还要检查所用的打印库。部分配置默认调用 Microsoft 的打印函数,而 MinGW 的 long double 表示与它的参数规则并不匹配。可以按该工具链的配置,在编译时启用 MinGW 的 C99 兼容实现,例如:
bash
gcc printf_basics.c -std=c11 -D__USE_MINGW_ANSI_STDIO=1 -Wall -Wextra -Wpedantic -o printf_basics.exe
这是特定 MinGW 环境的兼容选项,不能推广为所有编译器都需要的设置。使用标准要求的 %Lf 和 %zu 时,应让编译器、头文件与实际链接的打印库相互匹配。
三十、使用格式时先检查什么
先确认参数类型,再选择转换;先确定需要保留的内容,再设置精度;最后添加宽度与对齐。涉及字符数组时,额外检查总容量、终止符和返回值。
字段宽度负责布局,字符串精度限制读取长度,snprintf 的容量参数限制写入长度。理解这三者的分工,可以避免把看似美观的格式误当成可靠的边界检查。
下一篇将介绍 C 语言变量:声明与定义、基本数据类型,以及怎样用 sizeof 和标准宏查看当前平台的大小与范围。