字符串和格式化输入/输出

掌握C语言中字符串的本质——字符数组+空字符,以及scanf/printf的格式化输入输出,包含常用格式说明的完整用法

学习目标

  • 能区分字符数组容量、字符串首个空字符前的长度与指针大小,并说明何时 sizeof array == strlen(array) + 1
  • 能用 printf 的各种格式标志(宽度、精度、对齐、前缀)精确控制输出格式
  • 能回答:用 scanf("%d", &n) 读完一个整数后紧接着用 getchar(),为什么读到的不是下一个字符而是换行符?

机制总览

字符串和格式化输入/输出:机制路径

  1. 1

    为什么 C 的输入输出这么"啰嗦"?

    想象你要给一个自动化仓库发指令:"把一个红色盒子放到 3 号架第 2 层"。这条指令必须精确到每个细节——什么颜色、哪个架子、哪一层,错一个仓库机器人就放不对。

  2. 2

    C 字符串的本质——char 数组加一个看不见的尾巴

    在 C 语言里没有独立的字符串类型。 字符串(string) 依赖末尾的 空字符(null character) \0 判断结束。

  3. 3

    动手:scanf 从键盘到变量的三步

    猜一猜: scanf("%d %f", &n, &f); 输入 42 3.14 回车后, n 是 42、 f 是 3.14——但回车符去哪了?它留在输入缓冲区里了!通过下面 Stepper 一步步揭晓。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

字符串和格式化输入/输出:机制与证据

切换《字符串和格式化输入/输出》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 为什么 C 的输入输出这么"啰嗦"?

想象你要给一个自动化仓库发指令:"把一个红色盒子放到 3 号架第 2 层"。这条指令必须精确到每个细节——什么颜色、哪个架子、哪一层,错一个仓库机器人就放不对。

可核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么 C 的输入输出这么"啰嗦"?」的实际行为。

学完《字符串和格式化输入/输出》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

字符串和格式化输入/输出:失效与核验

为什么 C 的输入输出这么"啰嗦"?

典型失效

若只记语法而忽略「为什么 C 的输入输出这么"啰嗦"?」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「为什么 C 的输入输出这么"啰嗦"?」的实际行为。

C 字符串的本质——char 数组加一个看不见的尾巴

典型失效

若只记语法而忽略「C 字符串的本质——char 数组加一个看不见的尾巴」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「C 字符串的本质——char 数组加一个看不见的尾巴」的实际行为。

动手:scanf 从键盘到变量的三步

典型失效

若只记语法而忽略「动手:scanf 从键盘到变量的三步」的类型范围、求值顺序或资源边界,输入变化后可能出现未定义行为或错误结果。

核验证据

用 -Wall -Wextra -pedantic 构建本节示例,再以正常值、边界值和失败输入核对「动手:scanf 从键盘到变量的三步」的实际行为。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么 C 的输入输出这么"啰嗦"?

想象你要给一个自动化仓库发指令:"把一个红色盒子放到 3 号架第 2 层"。这条指令必须精确到每个细节——什么颜色、哪个架子、哪一层,错一个仓库机器人就放不对。

在 C 语言里,输入和输出就是这样的:电脑不会"猜"你想把数据存到哪、想用什么格式显示。你必须告诉它:"这个数字是整数(%d)、存在变量 n 的内存地址(&n)"。每个细节都得自己说清楚,少说一个就出问题。

如果没有这种精确控制呢? 那 printf 输出 3.14159 时你不知道该显示 2 位还是 10 位小数,scanf 不知道把读到的 42 存到哪个变量——数据就在缓冲区里漂着,进不了你的程序,你也看不见正确的输出。这一章就教你掌握 C 输入输出的"格式标签",把数据在程序和外部世界之间精确搬运。

C 字符串的本质——char 数组加一个看不见的尾巴

在 C 语言里没有独立的字符串类型。 依赖末尾的 \0 判断结束。

字符串字面值(双引号包围的文本,如 "Hello")在编译时就被处理成带有 \0 的字符数组。你写 char str[] = "Hello";,编译器自动分配 6 个字节而不是 5 个——多出来的那一个就是末尾的 \0

"Hello" 返回 5,而 sizeof "Hello" 返回包含终止字符的数组大小 6。这个“差 1”只适用于数组恰好由该字符串初始化且首个空字符位于最后一格的情形;数组有额外容量、内部更早出现空字符或表达式已退化为指针时都不成立。

#include <stdio.h>
#include <string.h>
int main(void) {
    char str[] = "Hello";
 
    printf("strlen(str) = %zu\n", strlen(str));  // 输出 5
    printf("sizeof(str) = %zu\n", sizeof(str));  // 输出 6
 
    // sizeof 算上了 C 自动给你加的那个 \0!
    return 0;
}

记住这条铁律:C 语言的字符串必须靠 \0 判断结束——没有 \0 的不是字符串,只是普通字符数组。

printf 的格式说明——百分号后面那短短几个字符的全套能力

上一章我们已经用 printf() 输出过数据,但 printf 的能力远不止 %d%f 的通式是 %[标志][宽度][.精度]类型,由三个可选的控制字段和一个必选的类型字段组成。

printf 格式标志速查表格式说明含义用法示例输出结果%d有符号十进制整数printf("%d", 42)42%u无符号十进制整数printf("%u", 42U)42%f浮点数(默认6位小数)printf("%f", 3.14)3.140000%c单个字符printf("%c", 'A')A%s字符串(char*)printf("%s", "Hi")Hi%.2f浮点数保留2位小数printf("%.2f", 3.14159)3.14%10d右对齐总宽10字符printf("%10d", 42) 42%-10d左对齐总宽10字符printf("%-10d", 42)42 %#x十六进制带0x前缀printf("%#x", 255)0xff%+d强制显示正负号printf("%+d", 42)+42%010d宽度10,左边补零printf("%010d", 42)0000000042格式通式:%[标志][宽度][.精度]类型标志:-(左对齐) +(显示正号) #(加前缀 0x/0) 0(补零) 类型:d/f/s/c/u/x 等
printf 的格式说明遵循 %[标志][宽度][.精度]类型 组合规则——所有控制能力都在百分号后面这短短几个字符里。

来逐一看每个字段的作用:

  • 类型(必选): 告诉 printf 参数是什么类型——d(有符号整数)、u(无符号整数)、f(浮点数)、c(字符)、s(字符串)、x(十六进制)。
  • 宽度(可选): %10d 表示总宽度至少 10 个字符——不够的用空格填满。数字默认右对齐,加 -%-10d)变成左对齐。
  • .精度(可选): %.2f 指浮点数保留 2 位小数;%.5s 指字符串最多打印前 5 个字符。
  • 标志(可选): - 左对齐、+ 强制显示正号、# 给八/十六进制加前缀(如 %#x0xff)、0 用 0 补足宽度而不是空格。

下面这段代码把上面所有玩法串在一起:

#include <stdio.h>
int main(void) {
    int  n = 42;
    double pi = 3.1415926535;
 
    printf("默认:    |%d|\n", n);      // |42|
    printf("宽度10:  |%10d|\n", n);    // |        42|  ⟵ 右对齐
    printf("左对齐:  |%-10d|\n", n);   // |42        |  ⟵ 左对齐
    printf("补零:    |%010d|\n", n);   // |0000000042|  ⟵ 左边填0
    printf("带符号:  |%+d|\n", n);     // |+42|        ⟵ 强制显示正号
    printf("十六进制:|%#x|\n", 255);   // |0xff|       ⟵ 0x前缀
    printf("浮点默认:|%f|\n", pi);     // |3.141593|   ⟵ 默认6位
    printf("精确2位: |%.2f|\n", pi);   // |3.14|       ⟵ 保留2位
    printf("宽10精2: |%10.2f|\n", pi); // |      3.14| ⟵ 宽度+精度组合
    printf("左宽10精2:|%-10.2f|\n", pi);//|3.14      | ⟵ 组合标志
 
    return 0;
}

scanf 的用法——每次输入都是一次"格式匹配"

如果说 printf 是把数据按你指定的格式"编码"成文字输出,那 scanf() 就是反过来——从输入里按格式"解码"数据存入变量。 的用法跟 printf 相似——也是用格式说明来告诉电脑"输入里的数据长什么样"。

下面用 Stepper 一步步看 scanf 到底怎么工作的——每一步都有对应的图示:

动手:scanf 从键盘到变量的三步

猜一猜:scanf("%d %f", &n, &f); 输入 42 3.14 回车后,n 是 42、f 是 3.14——但回车符去哪了?它留在输入缓冲区里了!通过下面 Stepper 一步步揭晓。

分步1 / 3

第一步:键盘输入进入缓冲区

scanf 输入流程⌨ 键盘42 3.14↵← 第一步:用户输入输入缓冲区(stdin buffer)42 3.14scanf("%d %f", &n, &f);格式串告诉 scanf:先读一个整数 → 再读一个浮点数n = 42int n; &n = 0x...f = 3.14float f; &f = 0x...
第一步:用户输入的内容先进入键盘缓冲区,scanf 从这里读而不是直接从键盘读。

当你敲 42 3.14↵(↵ = 回车键),这些字符首先进入一个叫输入缓冲区(stdin buffer)的临时存储区。scanf 不是直接从键盘读的——它是从这个缓冲区里读。缓冲区是一个先到先取的队列:你按顺序输入的字符在这里排好队,等 scanf 来取。

printf / scanf 与缓冲区——不能只看表面

printf / scanf 与缓冲区的关系printf("Hello, %d", 42)格式化 → 写入输出缓冲区输出缓冲区stdout buffer遇到 \n 或满时刷新🖥 控制台显示 Hello, 42⌨ 键盘用户输入 42↵输入缓冲区stdin buffer暂存键盘输入的全部字符scanf("%d", &n)从缓冲区读取 → 存入变量 n"缓冲"是什么意思?为什么不直接从键盘读?缓冲 = 程序和硬件之间的中转站。键盘逐字符送来,缓冲区攒够再一起处理,避免 scanf("%d") 每次都跟键盘交互一次。输出缓冲区攒着 printf 内容,遇到换行符或缓冲区满才刷到控制台。⚠ 重要:输入缓冲区残留问题scanf 只吃掉匹配内容——换行符等留在缓冲区,下次读取会意外读到"剩菜"。
printf 和 scanf 都不直接跟硬件打交道——它们通过缓冲区这个中转站来往于程序和设备之间。缓冲区的存在让输入输出高效有序,但也是 C 初学者「输入被跳过」类 bug 的根源。

缓冲区也影响输出可见时机。正常程序终止会关闭并刷新输出流;但没有换行的交互提示可能在程序等待输入时仍留在缓冲区。终端上的 stdout 常为行缓冲但不是无条件保证,需立即显示提示时调用 fflush(stdout)

不要把“换行必刷新”或“所有 scanf 都不吃换行”当成规则。应根据流缓冲模式、具体转换说明是否跳过空白以及函数返回值判断状态。

代码详解:四个核心场景逐段拆解

场景一:声明和输出字符串——字符串的三种"写法"

#include <stdio.h>
#include <string.h>
int main(void) {
    // 方式①:{} 逐字符初始化(C 会自动加 \0)
    char s1[] = {'H', 'i', '\0'};   // "Hi"——手动写 \0
 
    // 方式②:"" 字符串字面值(编译器自动加 \0)
    char s2[] = "World!";           // "World!"——自动加 \0
 
    // 方式③:char 指针指向字面值
    const char *s3 = "Hello";       // 指针指向只读的"Hello\0"
 
    printf("%s %s %s\n", s1, s2, s3);   // 输出 Hi World! Hello
    printf("strlen(s2) = %zu\n", strlen(s2));  // 6
    return 0;
}

三种方式有细微差别:方式②创建可修改数组,其存储期取决于声明位置;方式③指向字符串字面量。通过该指针修改字面量的行为未定义,无论目标平台是否把它放在只读段。%s 会从地址读取直到首个空字符,因此调用者必须保证对象边界内存在终止符。

场景二:printf 格式组合——宽度、精度、标志一起上

#include <stdio.h>
int main(void) {
    double price = 12.50;
    int    count = 3;
 
    // 记账风格:左对齐标签 + 右对齐金额
    printf("%-20s %8.2f\n", "单价:", price);       // 单价:            12.50
    printf("%-20s %8d\n",   "数量:", count);       // 数量:                3
    printf("%-20s %8.2f\n", "总价:", price * count);// 总价:            37.50
 
    return 0;
}

%-20s- 表示左对齐,20 是宽度——标签左边贴着显示,右边用空格补齐到 20。%8.2f8 是总宽度(含小数点和小数位),.2 是 2 位小数——金额 37.50 刚好占满 8 个字符宽度,右边对齐。这套格式化组合在打印表格时非常实用。

场景三:scanf 读取混合类型——每一个百分号都在"扫描"

#include <stdio.h>
int main(void) {
    int  age;
    float height;
    char name[20];
 
    printf("请输入:姓名 年龄 身高\n");
    if (scanf("%19s %d %f", name, &age, &height) != 3) {
        fputs("输入格式错误\n", stderr);
        return 1;
    }
 
    printf("%s%d 岁,身高 %.2f\n", name, age, height);
    return 0;
}

在此函数实参中 name 转换为首元素指针,所以不写 &nameageheight 则传对象地址。%19s 最多写入 19 个非空白字符并追加空字符,匹配 name[20] 的容量。返回值 3 表示三个转换都成功;不检查返回值就可能继续读取未赋值对象。

场景四:gets 的危险与 fgets 的安全替代

#include <stdio.h>
int main(void) {
    char line[20];
 
    // ❌ gets(line)——已从 C11 标准删除,永不使用!
    // gets 不知道数组有多大,用户输入 100 个字符会把栈冲垮
 
    // ✅ 安全替代:fgets
    printf("请输入一行文字:");
    fgets(line, sizeof(line), stdin);   // 最多读 sizeof(line)-1 个字符
    fputs(line, stdout);                // 输出读到的内容(fgets 会保留末尾 \n)
 
    return 0;
}

gets() 在 C11 中被删除,因为接口无法获知目标容量。fgets() 接受数组、最大写入数量和输入流,成功时最多存 n-1 个字符并追加空字符。若换行在容量内会一并保存;长行只读到一段,EOF 前最后一行也可能没有换行。调用者必须检查返回值并决定是否继续读取剩余片段。

容易踩的坑

小结

  • C 字符串以空字符终止;strlen 找首个终止符,sizeof 查询数组或指针本身,两者只在特定数组初始化场景差 1
  • printf 的格式通式 %[标志][宽度][.精度]类型 —— 组合这些字段就能完成日常所有输出格式需求
  • scanf 必须传变量地址(&),从输入缓冲区按格式匹配读取——跟 printf 方向相反但格式体系相同
  • 输入残留要按目标协议处理:非空白字符可在 %c 前加空格,整行输入应统一用 fgets 并检查完整性
  • 永远不用 gets——用 fgets(name, sizeof(name), stdin) 安全替代;puts/fputs 做行输出

练习

问题 1(改代码型) 下面程序至少有七处错误或隐患。找出并修正,且检查每次输入的返回值。

#include <stdio.h>
int main(void) {
    char name[5] = "Xiaow";
    int age;
    float score;
    char grade;
 
    gets(name);
    scanf("%f", score);
    scanf("%c", &grade);
    printf("姓名: %s 年龄: %c 成绩: %d\n", name, age, score);
    return 0;
}

问题 2(问答型) 读下面程序,写出前五行结果,并说明为什么第六行没有可移植答案。

#include <stdio.h>
#include <string.h>
int main(void) {
    char str[] = "CPro";
    printf("① strlen = %zu\n", strlen(str));
    printf("② sizeof = %zu\n", sizeof(str));
    printf("③ |%10s|\n", str);
    printf("④ |%-10s|\n", str);
    printf("⑤ str[4] = %d\n", str[4]);
    printf("⑥ str[5] = %d\n", str[5]);
    return 0;
}

问题 3(综合实现型) 用 64 字节数组读取一行,检查是否完整,去掉已读换行,再按默认、右对齐和左对齐格式输出。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

字符串(string)

C 语言中存文本的方式——就是 char 类型的字符一个接一个在内存里排好队,然后队尾放一个空字符 \0 当"终点站"标记。C 没有专门的 string 类型,所谓的"字符串"本质上就是 char 数组——只是多了一个 \0 结尾。详见本章「C 字符串的本质」和 CStringMemoryDiagram 图示。

空字符(null character)

数值为零的字符,写作 \0,用于终止 C 字符串;它与字符 '0' 不同,也不依赖 ASCII。

strlen

从给定字符串起点查找到首个空字符并返回此前字符数。它不知道数组容量;sizeof 与它只在特定数组初始化情形下恰好差 1。

printf()

C 标准库里最常用的输出函数(<stdio.h>)。它像一个填空题的模板引擎:格式串里的普通文字原样输出,百分号 % 开头的格式说明被替换成后面参数的值。百分号之后控制三个维度——宽度(占几位)、精度(留几位小数)、类型(int/float/char)——合起来就是 %[标志][宽度][.精度]类型。详见本章「printf 的格式说明」一节和 PrintfFormatFlagsDiagram 图示。

格式说明符(format specifier)

printf 和 scanf 中以 % 开头、以类型字母结尾的指令。%d 表示"这里要填一个有符号整数",%f 表示"这里要填一个浮点数"。不同函数对格式说明符的规则不同——比如 scanf 里 double 用 %lf,但 printf 里 double 和 float 都用 %f。详见本章「printf 的格式说明」和 PrintfFormatFlagsDiagram 图示。

scanf

按格式从输入流执行转换并写入目标对象的函数。数值转换传对象地址;数组表达式在函数实参中通常转换为首元素指针。应限制字符串字段宽度并检查成功转换项数。

取地址运算符(&)

返回对象地址的一元运算符。scanf%d 等转换要求匹配的对象指针;传值而非地址会造成可变参数类型不匹配和未定义行为。

输入缓冲区(input buffer)

标准 I/O 实现为输入流维护的暂存状态。格式转换只消费匹配所需字符;未消费字符会供后续读取使用,而不同转换对前导空白的处理不同。

原版目录概念补充核对

以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。

常量与预处理器:机制、边界与证据

在《字符串和格式化输入/输出》的官方单元 cpr-04 中,常量与预处理器连接本章第 3 组知识约束。学习时要同时说明它接受什么输入、改变什么状态、在何种边界失效;再以本章示例的编译诊断、固定输入输出或失败用例复核结论,不能只记术语名称。

讨论

评论区加载中…