11.C 语言 printf 打印函数详解:格式说明符、宽度精度与安全输出

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 和标准宏查看当前平台的大小与范围。

相关推荐
不会就选b1 小时前
Linux之ip地址<3>
linux·运维·tcp/ip
Victor Zhong1 小时前
llama.cpp 部署大模型到 Ubuntu 台式机 SOP
linux·ubuntu·llama
AAA代码批发商2 小时前
DAYS 57【Linux 驱动深度】I2C 子系统驱动开发全指南:从架构、核心数据结构到设备驱动实战
linux·驱动开发·架构·内核开发·i2c 子系统
小此方2 小时前
Linux网络(二十三):图文详解子网划分、分类划分的本质
linux·运维·网络
NendoZen2 小时前
mangopi(芒果派)MQ-R T113 Buildroot开启adbd调试
linux·嵌入式硬件
Huangjin007_2 小时前
【Linux 系统篇(二十九)】文件(六):动静态库制作
linux·运维·服务器
额额额对了2 小时前
Linux 驱动学习笔记(部分)
linux·笔记·学习
zhikouai3 小时前
AI辅助逆向工程链路:从二进制接入到等价性校验
java·linux·前端
程序员-Benothing3 小时前
Linux磁盘IO性能分析:iostat、iotop、fio实战
linux·运维·服务器