清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第4章:字符串和格式化输入输出

字符串把多个字符组织成一个整体;格式化输入输出则负责在文本与程序中的数据之间建立对应关系。

文章目录 50 个章节

第4章:字符串和格式化输入输出

字符串把多个字符组织成一个整体;格式化输入输出则负责在文本与程序中的数据之间建立对应关系。

本章的关键是分清三组概念:数组容量与字符串长度、数值本身与显示格式、输入匹配与成功赋值。

1. 字符串怎样存储

1.1 字符数组与字符串

数组是一组按顺序、连续存放的同类型元素。下面定义了一个包含 16 个 char 元素的数组:

c
char name[16];

如果用它保存 C 字符串,字符序列的末尾必须有一个空字符 \0,作为结束标记。

c
char name[16] = "Lin";

可以把它的开头理解为:

text
下标       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 数组有多大,要把结束标记算进去

c
char word[] = "code";

这里由初始化内容推导容量,数组有 5 个元素:4 个内容字符,加 1 个空字符。

而 char raw[4] = "code"; 在 C 中可以建立恰好装满四个字符的数组,却没有位置保存结束标记。它不能直接当作以空字符结尾的字符串交给 strlen 或普通 %s 输出。

字符数组是一种存储形式,数组中的数据是否构成字符串,还要看有没有有效的结束标记。

字符串字面量可以用来初始化可修改的字符数组,但不应尝试修改字符串字面量本身。

2. strlen 与 sizeof 回答不同的问题

2.1 长度与容量

c
#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;
}

输出:

text
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 以第一个空字符为界

c
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 为什么给常量起名字

c
area = 3.141592653589793 * radius * radius;

把圆周率命名后,公式更容易理解,也便于统一修改使用的精度。

同样,换算系数、数组容量和固定提示文字都可以通过清楚的名称表达用途,避免在各处重复难以理解的数字或文本。

3.2 #define 定义对象式宏

c
#define PI 3.141592653589793
#define NAME_CAPACITY 32
#define TITLE "Circle report"

这种常量用途的宏通常采用大写名称,便于与普通变量区分。

基本格式是:

text
#define 名称 替换内容
  • 名称和替换内容之间不写 =。
  • 指令末尾不加作为 C 语句终止符的分号。
  • 预处理阶段完成替换,宏名称本身不是一个有独立存储空间的变量。

例如,double area = PI * radius * radius; 会在预处理时展开相应的宏。

替换按预处理记号进行,不会把较长标识符里的一部分随意替换,也不会把字符串 "PI" 内的文字替换成数字。

若替换内容是一个表达式,常用括号保护整体含义:

c
#define GROUP_SIZE (2 + 3)

这样 4 * GROUP_SIZE 才能保持“4 乘以整个和”的意思。

3.3 const 创建只读对象

c
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 普通文字与转换说明

c
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 的形式。

c
printf("Progress: %d%%\n", 75);

输出 Progress: 75%,因为 %% 表示一个字面百分号。

5.3 格式说明的组成顺序

text
%[标记][最小字段宽度][.精度][长度修饰符]转换字符

方括号表示可选部分,不需要把方括号输入代码。

例如,%+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 排列表格时的用法

c
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 字符串:最多输出多少字节

c
printf("|%12.5s|\n", "Learning C");
printf("|%-12.5s|\n", "Learning C");

输出:

text
|       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 是需要原样输出的字符串,应写:

c
printf("%s", text);

这样 text 中的百分号只作为内容显示。把它直接写成 printf(text);,则会把内容中的百分号当作潜在转换说明解析。

9. printf 的返回值与长字符串

9.1 返回的是输出字符数

c
int count = printf("Hi!\n");
printf("count: %d\n", count);

输出:

text
Hi!
count: 4

空格和换行符也计入结果。出现输出错误时,printf 返回负值。

返回值描述的是输出数量,不是格式字符串有多长,也不是传入了几个实参。对于普通多字节文本,它也不能直接当作屏幕上可见文字或显示列的数量。

9.2 长字符串优先用相邻字面量连接

c
printf("This is a long message "
       "written on two source lines.\n");

C 会把相邻的字符串字面量连接起来,源码断行不会自动加入输出换行。

如果需要单词之间有空格,必须明确把空格写在某个字面量内部。"hello" "world" 得到的是 "helloworld"。

其他方法包括分成多次调用,或者使用反斜杠接续源码行。后者容易让缩进等细节影响结果,一般优先采用相邻字面量连接的写法。

10. scanf 把文本解释成数据

10.1 输入与输出的方向相反

text
printf:程序中的值 → 按格式生成文本
scanf :输入文本   → 按格式转换并保存到对象
c
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]:

c
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 普通字符必须匹配

c
scanf("%d,%d", &first, &second);

这里要求第一个整数之后紧接逗号。输入 12, 30 可以匹配;输入 12 ,30 会在期待逗号时遇到空格,从而停止。

改成下面的格式,就允许逗号前后有空白:

c
scanf("%d , %d", &first, &second);

格式字符串中用于说明的普通文字,也会变成输入要求。提示用户的文字应通过 printf 输出,不要随意写进 scanf 的格式里。

11.4 格式末尾的空白可能让程序继续等待

交互输入中,scanf("%d\n", &count) 的末尾 \n 并不是“读掉一个回车就返回”。它会继续处理空白,可能等到看到后续非空白内容才结束这段匹配。

因此,普通数字输入通常不需要在格式末尾追加空格或换行。处理残留输入,应根据下一次要读取什么来设计。

12. 匹配失败、返回值与残留输入

12.1 一次读取可能只完成一部分

c
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:

text
输入:42kg
      └┘└┘
     整数 剩余文本

它可以成功读取整数 42,而 k 及后面的内容仍等待后续处理。如果再用 %d 读取,就会在 k 处失败。

对 word 这样的首次字符就不符合整数要求的输入,简单重复同样的读取不会自动解决问题。需要另外消费或处理不符合预期的内容。

多个转换按顺序处理,遇到失败时后面的转换不会继续。已经赋值的对象不会因此自动撤销,未赋值的对象也不会被自动设为零。

12.3 检查返回值只是输入处理的一部分

读取成功后,还要判断值是否符合业务范围。数值过大而无法由目标类型表示,也不能指望只靠返回值检查就安全处理。

需要严格控制整行格式、检查超长内容或处理数值范围时,可以在后续采用“先读取一行,再解析”的方式。

13. * 在两个函数中的不同作用

13.1 printf:从实参取得宽度与精度

c
int width = 8;
int precision = 2;
double value = 12.5;

printf("|%*.*f|\n", width, precision, value);

输出:

text
|   12.50|

实参按格式中出现的顺序提供:

text
第一个 * → width
第二个 * → precision
f        → value

宽度和精度实参都要求是 int。不能因为 strlen 的结果是一个整数,就直接把它作为 * 的实参;它的类型是 size_t,需要在确认可表示后转换为 int。

负宽度表示左对齐,绝对值作为宽度;负精度按没有给出精度处理。

13.2 scanf:读取但不赋值

c
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. 一个串起本章知识的程序

下面的程序输入一个不含空白的标签和圆半径,输出名称长度、数组容量、周长和面积:

c
#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 后,主要输出为:

text
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 环境中编译运行:

sh
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;。

Aron 的清风日常

作者 Aron · 发布于 2026.07.18

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭