第4章:字符串和格式化输入输出
字符串把多个字符组织成一个整体;格式化输入输出则负责在文本与程序中的数据之间建立对应关系。
本章的关键是分清三组概念:数组容量与字符串长度、数值本身与显示格式、输入匹配与成功赋值。
1. 字符串怎样存储
1.1 字符数组与字符串
数组是一组按顺序、连续存放的同类型元素。下面定义了一个包含 16 个 char 元素的数组:
char name[16];
如果用它保存 C 字符串,字符序列的末尾必须有一个空字符 \0,作为结束标记。
char name[16] = "Lin";
可以把它的开头理解为:
下标 0 1 2 3 ... 15
内容 'L' 'i' 'n' '\0' ...
└── 字符内容 ──┘ 结束标记
在这个带字符串初始化的定义中,后面没有用到的元素也会被初始化为零。
name[16] 这个声明中的 16 是元素数量,实际下标是 0 到 15。需要保存 15 个内容字节时,还要留出第 16 个位置存放 \0。
1.2 空字符、数字字符和空字符串
| 写法 | 含义 |
|---|---|
'0' |
数字字符零,在 ASCII 中编码为 48 |
'\0' |
数值为零的空字符,常用作字符串结束标记 |
0 |
整数零;赋给 char 时可以形成空字符值 |
"" |
空字符串,仍然包含结尾的 \0 |
'A' |
普通字符常量,在 C 中类型为 int |
"A" |
字符串字面量,包含 'A' 和 \0 两个元素 |
字符串允许为空。strlen("") 是 0,sizeof("") 是 1。
1.3 数组有多大,要把结束标记算进去
char word[] = "code";
这里由初始化内容推导容量,数组有 5 个元素:4 个内容字符,加 1 个空字符。
而 char raw[4] = "code"; 在 C 中可以建立恰好装满四个字符的数组,却没有位置保存结束标记。它不能直接当作以空字符结尾的字符串交给 strlen 或普通 %s 输出。
字符数组是一种存储形式,数组中的数据是否构成字符串,还要看有没有有效的结束标记。
字符串字面量可以用来初始化可修改的字符数组,但不应尝试修改字符串字面量本身。
2. strlen 与 sizeof 回答不同的问题
2.1 长度与容量
#include <stdio.h>
#include <string.h>
int main(void)
{
char name[16] = "Lin";
printf("length: %zu\n", strlen(name));
printf("capacity: %zu\n", sizeof name);
printf("literal size: %zu\n", sizeof "Lin");
return 0;
}
输出:
length: 3
capacity: 16
literal size: 4
| 操作 | 回答的问题 | 是否计算结束标记 |
|---|---|---|
strlen(name) |
第一个 \0 前面有多少字节 |
不计算 |
sizeof name |
这里这个数组对象占多少 C 字节 | 包括整个数组空间 |
sizeof "Lin" |
字符串字面量数组占多少 C 字节 | 包括结尾的 \0 |
strlen 声明在 <string.h> 中;sizeof 是语言运算符。两者的结果类型都是 size_t,输出时使用 %zu。
2.2 strlen 以第一个空字符为界
char text[] = "ab\0cd";
数组包含 'a'、'b'、\0、'c'、'd' 和最后自动添加的 \0,所以 sizeof text 是 6;strlen(text) 则是 2。
这也说明,字符串长度并不由数组容量直接决定。
调用 strlen 的前提是参数指向一个有效的、以空字符结尾的字符串。它不知道数组的容量,不能用来替缺少结束标记的数组检查边界。
2.3 字节数不一定等于文字字符数
对于基本英文字符,长度通常与直观的字母数量一致。但 UTF-8 等多字节编码中,一个文字字符可能占多个字节。
因此,strlen 统计的是结束标记之前的字节数,不能直接用来统计一段中文里有多少汉字。
另外,sizeof 能在这里得到数组容量,是因为操作数确实是数组。如果操作数是指针,得到的是指针对象的大小,不能据此推断它所指向字符串的容量。
3. 用名称表达固定值
3.1 为什么给常量起名字
area = 3.141592653589793 * radius * radius;
把圆周率命名后,公式更容易理解,也便于统一修改使用的精度。
同样,换算系数、数组容量和固定提示文字都可以通过清楚的名称表达用途,避免在各处重复难以理解的数字或文本。
3.2 #define 定义对象式宏
#define PI 3.141592653589793
#define NAME_CAPACITY 32
#define TITLE "Circle report"
这种常量用途的宏通常采用大写名称,便于与普通变量区分。
基本格式是:
#define 名称 替换内容
- 名称和替换内容之间不写
=。 - 指令末尾不加作为 C 语句终止符的分号。
- 预处理阶段完成替换,宏名称本身不是一个有独立存储空间的变量。
例如,double area = PI * radius * radius; 会在预处理时展开相应的宏。
替换按预处理记号进行,不会把较长标识符里的一部分随意替换,也不会把字符串 "PI" 内的文字替换成数字。
若替换内容是一个表达式,常用括号保护整体含义:
#define GROUP_SIZE (2 + 3)
这样 4 * GROUP_SIZE 才能保持“4 乘以整个和”的意思。
3.3 const 创建只读对象
const double pi = 3.141592653589793;
const int months = 12;
const 是类型限定符,这里的对象有明确类型和相应作用域,可以读取其值,但不能再通过普通赋值修改。
| 比较项 | #define 常量用途宏 |
const 对象 |
|---|---|---|
| 处理方式 | 预处理阶段替换记号 | 按语言的对象和类型规则处理 |
| 是否定义对象 | 宏本身不定义对象 | 定义带 const 限定的对象 |
| 类型 | 由展开后的内容决定 | 在声明中明确指定 |
| 常见用途 | 宏常量、预处理条件等 | 有类型、受作用域约束的只读数据 |
在 C 中,const int n = 10; 不会自动让 n 成为所有语法位置都接受的整型常量表达式。不要把它与直接写 10,或者将宏展开为 10 完全等同。
4. 用标准头文件了解类型限制
<limits.h> 和 <float.h> 提供当前实现的类型能力信息。程序应读取这些约定,而不是把某台机器的数值写死。
| 头文件 | 名称示例 | 含义 |
|---|---|---|
<limits.h> |
CHAR_BIT |
一个 C 字节包含的位数 |
<limits.h> |
INT_MIN、INT_MAX |
int 的最小值与最大值 |
<limits.h> |
UINT_MAX |
unsigned int 的最大值 |
<limits.h> |
LLONG_MIN、LLONG_MAX |
long long 的范围 |
<float.h> |
FLT_MAX、DBL_MAX |
相应类型的最大有限正值 |
<float.h> |
FLT_MIN、DBL_MIN |
相应类型的最小正规正值 |
<float.h> |
FLT_DIG、DBL_DIG |
十进制有效数字的精度保证 |
<float.h> |
FLT_EPSILON |
float 中 1 与大于 1 的下一个可表示值之差 |
FLT_EPSILON 描述 1 附近的间距,不是所有数量级上都通用的误差阈值。FLT_MIN 也不是最负的浮点值。
5. printf 的格式字符串
5.1 普通文字与转换说明
printf("Item: %s, count: %d\n", "pen", 12);
格式字符串中包含两类内容:
- 普通字符:按原样输出,例如
Item:、逗号和空格。 - 转换说明:规定后面的值用什么形式显示,例如
%s、%d。
转换说明影响的是文本表示,不会修改原变量的值。
5.2 常用转换字符
| 转换 | 用途与对应类型 |
|---|---|
%d、%i |
有符号十进制整数,普通形式对应 int |
%u |
无符号十进制整数,普通形式对应 unsigned int |
%o |
无符号八进制整数 |
%x、%X |
无符号十六进制整数,字母分别用小写、大写 |
%c |
单字符;普通形式接收经整数提升的字符值 |
%s |
从字符序列输出文本,通常到 \0 为止 |
%f、%F |
普通小数形式,接收 double |
%e、%E |
十进制指数形式,接收 double |
%g、%G |
根据数值与精度选择普通或指数形式 |
%a、%A |
十六进制浮点形式 |
%p |
对象指针的表示,要求对应参数为 void * |
%% |
输出一个百分号,不消耗实参 |
传给 printf 可变参数部分的 float 会提升为 double。long double 则要使用 %Lf、%Le 等带 L 的形式。
printf("Progress: %d%%\n", 75);
输出 Progress: 75%,因为 %% 表示一个字面百分号。
5.3 格式说明的组成顺序
%[标记][最小字段宽度][.精度][长度修饰符]转换字符
方括号表示可选部分,不需要把方括号输入代码。
例如,%+10.2f 可以拆成:
| 部分 | 作用 |
|---|---|
% |
转换开始 |
+ |
正值也显示加号 |
10 |
整个字段至少宽 10 |
.2 |
对 %f 而言,小数点后显示两位 |
f |
普通小数输出 |
不同转换字符支持的组合不同,不能把所有标记都随意加到所有转换上。
6. 字段宽度、对齐与填充
6.1 宽度是下限
对整数 27,以下格式有不同效果。用 | 标出字段边界:
| 调用 | 输出 |
|---|---|
printf("|%d|", 27); |
|27| |
printf("|%6d|", 27); |
| 27| |
printf("|%-6d|", 27); |
|27 | |
printf("|%06d|", 27); |
|000027| |
printf("|%1d|", 27); |
|27| |
默认右对齐,以空格补足宽度。- 表示左对齐;适用于数值转换的 0 标记使用前导零填充。
指定的宽度不够时,字段会扩展到能容纳完整结果,不会只保留最后几位数字。
6.2 常用标记
| 标记 | 作用 |
|---|---|
- |
在字段内左对齐 |
+ |
有符号数的非负值也带加号 |
| 空格 | 非负值前留一个空格,负值仍显示负号 |
0 |
对支持的数值转换用前导零填充 |
# |
采用另一种规定形式,例如十六进制非零值带前缀 |
组合时还要考虑优先关系:
+与空格标记同时存在时,以+为准。-与0同时存在时,零填充被忽略。- 对整数转换,明确给出精度时,
0标记被忽略。
# 的具体含义取决于转换类型。例如,%#x 为非零十六进制结果加上 0x;某些浮点格式中,它会保留小数点,或保留原本可能省略的末尾零。
6.3 排列表格时的用法
printf("%-10s %6d %9.2f\n", "pen", 12, 3.5);
字符串左对齐、数字右对齐,容易形成整齐的列。字段之间再明确放一个空格,即使某个值超过预设宽度,也不至于与下一个值直接黏在一起。
这些简单的宽度示例使用基本英文字符。对 UTF-8 中文文本,字节数与终端显示列数不同,不能只靠 %s 的字段宽度保证视觉对齐。
7. 精度的含义随类型变化
7.1 浮点数:小数位或有效数字
| 格式 | 精度的含义 |
|---|---|
%.2f |
小数点后两位 |
%.2e |
指数形式中小数点后两位,即通常共三位有效数字 |
%.4g |
最多四位有效数字,按规则选择表示形式 |
%f、%e 未指定精度时,通常默认小数点后六位;%g 默认精度为六位有效数字,并通常移除不必要的末尾零。
以 12.5 为例:
| 格式 | 输出 |
|---|---|
%f |
12.500000 |
%.2f |
12.50 |
%8.2f |
12.50 |
%08.2f |
00012.50 |
%.2e |
1.25e+01 |
字段宽度包括符号、小数点和指数部分等整个结果。精度影响输出舍入,但不会让变量本身获得更多存储精度。
7.2 整数:最少输出多少位数字
| 调用 | 输出 |
|---|---|
printf("|%6d|", 27); |
| 27| |
printf("|%6.4d|", 27); |
| 0027| |
printf("|%06d|", 27); |
|000027| |
%6.4d 的意思是:数字至少四位,不足补前导零;整个字段至少六位,剩余部分补空格。
整数精度不是截断上限。数值有五位时,不会因为精度写成 .4 就只打印四位。
7.3 字符串:最多输出多少字节
printf("|%12.5s|\n", "Learning C");
printf("|%-12.5s|\n", "Learning C");
输出:
| Learn|
|Learn |
对于普通 %s:
- 宽度决定字段至少多宽。
- 精度限制最多输出多少字节;提前遇到
\0时仍会停止。 - 不会改变原字符串。
这与整数精度的方向不同:整数精度是位数下限,字符串精度是输出长度上限。
8. 长度修饰符与类型匹配
8.1 长度修饰符选择对应类型
| 修饰符 | 示例 | 对应含义 |
|---|---|---|
hh |
%hhd、%hhu |
signed char、unsigned char 的整数显示 |
h |
%hd、%hu |
short、unsigned short |
l |
%ld、%lu |
long、unsigned long |
ll |
%lld、%llu |
long long、unsigned long long |
L |
%Lf |
long double |
j |
%jd、%ju |
intmax_t、uintmax_t |
z |
%zu |
size_t 的无符号输出 |
t |
%td |
ptrdiff_t 的有符号输出 |
char、short 等较窄整数在传给可变参数时会先发生整数提升,修饰符再按相应的格式规则处理。size_t 等类型不应根据某台机器的大小猜测格式,使用它们对应的修饰符即可。
8.2 格式字符串不会替代数值类型转换
如果变量是 long,需要浮点表示,应先取得合适的浮点值,再配上 %f;不能把 %ld 随意改成 %f 就期待函数完成类型转换。
值按实际表达式类型及参数提升规则传入,函数再依据格式字符串取用。类型不匹配可能导致未定义行为,不能把某次错误输出当成可依赖的转换方法。
具体参数可能通过寄存器、栈等方式传递,由实现和平台约定决定;不能假定所有环境都按照一套固定的字节排列取参数。
8.3 文本内容与格式分开
如果 text 是需要原样输出的字符串,应写:
printf("%s", text);
这样 text 中的百分号只作为内容显示。把它直接写成 printf(text);,则会把内容中的百分号当作潜在转换说明解析。
9. printf 的返回值与长字符串
9.1 返回的是输出字符数
int count = printf("Hi!\n");
printf("count: %d\n", count);
输出:
Hi!
count: 4
空格和换行符也计入结果。出现输出错误时,printf 返回负值。
返回值描述的是输出数量,不是格式字符串有多长,也不是传入了几个实参。对于普通多字节文本,它也不能直接当作屏幕上可见文字或显示列的数量。
9.2 长字符串优先用相邻字面量连接
printf("This is a long message "
"written on two source lines.\n");
C 会把相邻的字符串字面量连接起来,源码断行不会自动加入输出换行。
如果需要单词之间有空格,必须明确把空格写在某个字面量内部。"hello" "world" 得到的是 "helloworld"。
其他方法包括分成多次调用,或者使用反斜杠接续源码行。后者容易让缩进等细节影响结果,一般优先采用相邻字面量连接的写法。
10. scanf 把文本解释成数据
10.1 输入与输出的方向相反
printf:程序中的值 → 按格式生成文本
scanf :输入文本 → 按格式转换并保存到对象
int count = 0;
double price = 0.0;
char name[16] = "";
scanf("%d %lf %15s", &count, &price, name);
这里的数值输入提供变量地址。字符数组 name 在这个调用中会转换为指向首元素的指针,已经提供了写入位置,所以不写 &name。
name 与 &name 的类型不同,不能把“都是地址”当成可互换的依据。
10.2 几组关键格式
| 目标 | 格式 | 参数示例 |
|---|---|---|
十进制 int |
%d |
&count |
自动识别进制的 int |
%i |
&count |
unsigned int |
%u |
&unsigned_count,变量须为对应无符号类型 |
long |
%ld |
&value |
long long |
%lld |
&value |
float |
%f |
&value |
double |
%lf |
&value |
long double |
%Lf |
&value |
| 单字符 | %c |
&ch |
| 单词字符串 | %15s |
指向至少 16 个 char 空间的指针 |
printf 中 %d 与 %i 都输出十进制;scanf 中则不同:%i 会识别前缀,例如把 010 按八进制解释,%d 则按十进制读取。
scanf 的浮点转换也能识别相应的指数形式,不能把 %f 简单理解为“只接受带普通小数点的输入”。
10.3 %s 读取单词
对于 char name[16]:
scanf("%15s", name);
它会跳过前导空白,从第一个非空白字符开始读取,直到遇到空白或达到字段宽度,再在结果后添加 \0。
输入 Lin Chen 时,只会得到 Lin,后面的内容留在输入流中。增加字段宽度也不会使单个 %s 自动跨过中间空格。
读取包含空格的一整行,可以使用 fgets 等适合按行读取的函数,后续再展开。
11. 输入字段宽度与空白规则
11.1 输入宽度是读取上限
| 场景 | 宽度含义 |
|---|---|
printf("%10s", name) |
输出字段至少宽 10,不截断长字符串 |
printf("%.10s", name) |
最多输出 10 字节 |
scanf("%10s", name) |
最多读取 10 个内容字节,还要另留空间放 \0 |
因此,容量为 N 的字符数组,用 %s 输入时字段宽度最多设为 N - 1。
如果一个输入单词长于限制,超出的部分会留在输入流中,不会自动消失。这可能影响下一次读取。
%c 不自动添加字符串结束标记。例如 %3c 用于读取三个字符时,不会顺便写入第四个 \0,不能把结果无条件交给普通 %s。
输入宽度也不是浮点精度,scanf 不支持用 %.2f 来要求“读取两位小数”。
11.2 大多数普通转换跳过前导空白
对本章使用的数值转换和 %s,前面的空格、制表符和换行等会被跳过。因此,读取两个整数时,%d%d 与 %d %d 可以处理相同的空白分隔输入。
%c 则读取下一个字符本身,包括空白:
| 写法 | 行为 |
|---|---|
scanf("%c", &ch) |
读取下一个字符,可能是上次输入留下的换行 |
scanf(" %c", &ch) |
先跳过任意数量的前导空白,再读取字符 |
格式中的空白表示跳过零个或多个输入空白,不是要求恰好输入一个空格,也不是只匹配一种空白字符。
11.3 普通字符必须匹配
scanf("%d,%d", &first, &second);
这里要求第一个整数之后紧接逗号。输入 12, 30 可以匹配;输入 12 ,30 会在期待逗号时遇到空格,从而停止。
改成下面的格式,就允许逗号前后有空白:
scanf("%d , %d", &first, &second);
格式字符串中用于说明的普通文字,也会变成输入要求。提示用户的文字应通过 printf 输出,不要随意写进 scanf 的格式里。
11.4 格式末尾的空白可能让程序继续等待
交互输入中,scanf("%d\n", &count) 的末尾 \n 并不是“读掉一个回车就返回”。它会继续处理空白,可能等到看到后续非空白内容才结束这段匹配。
因此,普通数字输入通常不需要在格式末尾追加空格或换行。处理残留输入,应根据下一次要读取什么来设计。
12. 匹配失败、返回值与残留输入
12.1 一次读取可能只完成一部分
int first = 0;
int second = 0;
int assigned = scanf("%d %d", &first, &second);
| 输入情形 | 常见返回结果 | 含义 |
|---|---|---|
12 30 |
2 |
两项都完成转换并赋值 |
12 word |
1 |
只给第一个变量赋值 |
word |
0 |
第一个数值就匹配失败,没有赋值 |
| 开始读取就遇到文件结束或读取错误 | EOF |
尚未完成首次转换就发生输入失败 |
EOF 是 <stdio.h> 定义的负整数常量,不应把代码写死为某个特定数值。
返回值统计成功赋值的项目数,既不是输入字符数,也不是格式字符串中的所有转换说明数。后面介绍的抑制赋值转换不会计入这个数量。
12.2 未匹配部分可能留在输入流中
例如,用 %d 读取 42kg:
输入:42kg
└┘└┘
整数 剩余文本
它可以成功读取整数 42,而 k 及后面的内容仍等待后续处理。如果再用 %d 读取,就会在 k 处失败。
对 word 这样的首次字符就不符合整数要求的输入,简单重复同样的读取不会自动解决问题。需要另外消费或处理不符合预期的内容。
多个转换按顺序处理,遇到失败时后面的转换不会继续。已经赋值的对象不会因此自动撤销,未赋值的对象也不会被自动设为零。
12.3 检查返回值只是输入处理的一部分
读取成功后,还要判断值是否符合业务范围。数值过大而无法由目标类型表示,也不能指望只靠返回值检查就安全处理。
需要严格控制整行格式、检查超长内容或处理数值范围时,可以在后续采用“先读取一行,再解析”的方式。
13. * 在两个函数中的不同作用
13.1 printf:从实参取得宽度与精度
int width = 8;
int precision = 2;
double value = 12.5;
printf("|%*.*f|\n", width, precision, value);
输出:
| 12.50|
实参按格式中出现的顺序提供:
第一个 * → width
第二个 * → precision
f → value
宽度和精度实参都要求是 int。不能因为 strlen 的结果是一个整数,就直接把它作为 * 的实参;它的类型是 size_t,需要在确认可表示后转换为 int。
负宽度表示左对齐,绝对值作为宽度;负精度按没有给出精度处理。
13.2 scanf:读取但不赋值
int last = 0;
int assigned = scanf("%*d %*d %d", &last);
输入 10 20 30 后,last 是 30,assigned 是 1。
前两项仍须能按整数匹配,但只被消费,不保存到变量,也不要求对应地址参数。最后一项才完成赋值。
| 写法 | * 的含义 |
是否提供额外实参 |
|---|---|---|
printf("%*d", width, value) |
动态字段宽度 | 提供一个 int 宽度 |
printf("%.*f", precision, value) |
动态精度 | 提供一个 int 精度 |
scanf("%*d%d", &value) |
抑制第一个整数的赋值 | 不为被跳过项提供地址 |
因此,scanf 的 * 不是动态宽度。要跳过一个单词可以使用 %*s,它仍然只读取一个非空白单词,不是任意跳过一整行。
14. 一个串起本章知识的程序
下面的程序输入一个不含空白的标签和圆半径,输出名称长度、数组容量、周长和面积:
#include <stdio.h>
#include <string.h>
#define LABEL_CAPACITY 32
#define REPORT_TITLE "Circle report"
int main(void)
{
const double pi = 3.141592653589793;
char label[LABEL_CAPACITY] = "";
double radius = 0.0;
printf("Enter a one-word label and radius: ");
fflush(stdout);
if (scanf("%31s %lf", label, &radius) != 2) {
printf("Invalid input.\n");
return 1;
}
if (radius < 0.0) {
printf("Radius cannot be negative.\n");
return 1;
}
double circumference = 2.0 * pi * radius;
double area = pi * radius * radius;
printf("\n%s\n", REPORT_TITLE);
printf("Label: %s\n", label);
printf("Length: %zu, capacity: %zu\n",
strlen(label), sizeof label);
printf("%-14s %10.2f\n", "Radius", radius);
printf("%-14s %10.2f\n", "Circumference", circumference);
printf("%-14s %10.2f\n", "Area", area);
return 0;
}
输入 disk 2 后,主要输出为:
Circle report
Label: disk
Length: 4, capacity: 32
Radius 2.00
Circumference 12.57
Area 12.57
这个例子中,%31s 与 32 个元素的数组容量相配合。若调整 LABEL_CAPACITY,也要同步检查输入宽度;宏不会自动改写格式字符串中的 31。
示例按标签和普通、有限且可表示的半径输入设计,展示基本返回值和负数检查,不承担完整数值解析的任务。if 的详细语法将在后续章节展开。
保存为 circle_report.c 后,可以在已有 cc 的 macOS 或类 UNIX 环境中编译运行:
cc -std=c11 -Wall -Wextra circle_report.c -o circle_report
./circle_report
15. 本地化设置与数字文本
不同地区可能使用不同的小数分隔符。运行时的本地化设置可以影响 printf 和 scanf 对数字文本的处理。
程序初始采用 "C" locale;使用 <locale.h> 中的 setlocale 可以选择环境支持的其他设置。空字符串 "" 用于请求按运行环境选择相应设置,具体可用名称由实现决定。
但本地化不会改变 C 源代码中数值常量的语法。源码仍应写 3.14159,不能因为运行环境用逗号作小数点,就把声明改成 double pi = 3,14159;。

