输入输出深入 scanf/printf
掌握格式化输入输出的细节与常见坑(scanf 返回值/限宽/残留、printf 宽度精度),生产代码用 snprintf
- 来源
- 补充
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 120 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.2.7 输入输出深入 scanf/printf
本章来源:printf 格式串/宽度/精度、scanf 用法与返回值、sprintf/snprintf/fprintf 原型来自《第一阶段讲义》18.4 节(部分)与 Linux IO 相关页【来源】,经重新组织表述;snprintf 返回值语义与截断判断、scanf 限宽与输入残留处理、浮点显示"舍入非精确"说明为新编【补充】(约 30%)。本章无勘误。平台适用性 universal。
① 学习目标
- 用宽度/精度/对齐/补零写出正确的 printf 格式串,并知道
%.2f是舍入显示而非精确值; - 检查 scanf 的返回值(成功匹配项数),并说出返回 0 时"输入未被消费"的含义;
- 解释输入残留(缓冲区遗留
'\n')对后续%c的影响,并给出两种解决方案; - 给
%s加限宽(%31s),说出它与 gets 的同类风险; - 说清 sprintf 的无边界风险与 snprintf 的三特性(限长、必写
'\0'、返回值截断判断),并用 snprintf 重构工程代码。
② 前置知识
- 必选:1.2.6 字符串与安全函数(
'\0'结尾、fgets/snprintf 的安全理念是本章的直接基础); - 建议:1.2.2 数据类型(浮点数的二进制表示背景,理解"显示舍入")。
③ 核心概念【来源】
| 概念 | 说明 |
|---|---|
| printf 宽度与精度 | %5d 最小宽度 5(右对齐);%-5d 左对齐;%05d 补零;%8.2f 总宽 8、两位小数——小数位是四舍五入显示(二进制浮点本身不精确表示十进制小数) |
| scanf 返回值 | 返回成功匹配的项数;返回 0 = 格式不匹配(输入未被消费、残留缓冲区);返回 EOF = 输入结束。忽略返回值是输入 bug 的头号来源 |
| scanf 必须传地址 | scanf("%d", &n) 的 & 不能丢——scanf 要把读到的值写进变量,需要它的地址 |
%s 限宽 | scanf("%s", buf) 与 gets 同类风险(无边界写入);写 %31s(最多 31 字符 + '\0')是基本防御 |
| 输入残留 | %d 只读走数字,行尾 '\n' 留在缓冲区;紧接着的 %c 会把它读走("跳过输入"现象)。解法:" %c" 前的空格跳过空白,或 fgets 整行读取 + sscanf 解析 |
| sprintf 的风险 | 无边界检查:目标小于内容即缓冲区溢出(与 gets 同类)——本章只做静态讲解,不执行越界写 |
| snprintf 三特性 | ① 写入受 n 限制;② 必写结尾 '\0';③ 返回"本应写入"的字符数——返回值 >= n 即发生截断,可据此提示或扩容 |
| fprintf / stderr | fprintf(stderr, ...) 把错误信息写到标准错误流(与 stdout 分离,便于日志与管道分离) |
④ 通俗解释【补充】
- printf 像"排版工":宽度是占位格子,
-是左对齐、0是空位补零;精度是小数位数——但排版工只负责"按规则四舍五入打印近似值",不负责让二进制浮点变得精确; - scanf 像"点菜员":点几道菜(格式项)成功几道,返回值就是"上齐了几道"。点错了(格式不匹配),菜原封不动留在桌上(缓冲区),下一桌(下一个 scanf)端走的就是剩菜——这就是输入残留;
- sprintf 是"往固定小箱子无限装货"(会溢出压坏邻居),snprintf 是"限重装箱并报告实际需要多少"——装不下时告诉你"本应装 37 件、箱子只有 16 格",你就能决定扩容还是截断。
⑤ 示例代码
代码示例与验证记录
- examples/ex1-printf-format.cprintf 宽度/精度/左对齐/补零(%5d / %.2f / %#x)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex1-printf-format.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
[ 42] [42 ] [00042] [ 3.14] [3.142 ] [3.1] 0xff 0377 A literal %
差异说明:编译 0 警告;%.2f 是四舍五入后的十进制显示(3.14159 的二进制近似值显示为 3.14),并非浮点可精确表示十进制小数
完整源码见 /code 代码示例页
- examples/ex2-scanf-checks.cscanf 返回值检查、输入残留与 %s 限宽(输入固定来自 ex2-input.txt)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex2-scanf-checks.c -o ex2 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
scanf("%d") 返回 1,age = 25 残留字符被 %c 读走,ASCII = 10(10 = 换行符 \n) 名字(限宽 %31s)= Alice 喂错输入后 scanf 返回 0(0 = 一个都没匹配上;本例不读 x 的值) 残留在缓冲区的错误输入被读走 = "hello"差异说明:输入文件内容为 25 / Alice / hello 三行;程序用 freopen("ex2-input.txt", "r", stdin) 固定输入以保证可复现。错误输入 scanf 返回 0 且不消费输入(第 4/5 行输出实证)
完整源码见 /code 代码示例页
- examples/ex3-snprintf-safe.csprintf 溢出风险(conceptual 说明)vs snprintf 截断与返回值(实跑 snprintf 版)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-16)编译:
gcc ex3-snprintf-safe.c -o ex3 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
small 容量 = 16,本应写入 = 37 字符 截断后 small = "标题:嵌入" 截断判断:37 >= 16 → 发生了截断 big 中完整内容 = "标题:嵌入式 Linux 学习路线"
差异说明:编译 0 警告(标题为运行时输入,避免 -Wformat-truncation 对'必然截断'的静态告警——这正是工程中的真实形态);snprintf 返回值 37 即'本应写入'长度,用于截断判断
完整源码见 /code 代码示例页
⑥ 编译与运行方法
三个示例编译命令一致:gcc <文件>.c -o <输出> -std=c11 -Wall -Wextra -Wpedantic,本机实测 0 警告。ex2/ex3 的输入分别固定来自同目录的 ex2-input.txt / ex3-input.txt(程序内 freopen 重定向,保证每次运行结果可复现)。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
| 输入整数后,下一个字符输入"被跳过" | 行尾 '\n' 残留在缓冲区被 %c 读走 | " %c"(空格跳过空白)或 fgets + sscanf |
| scanf 返回 0 后程序死循环 | 错误输入未被消费,每次都匹配失败 | 检查返回值;失败时清空残留(while (getchar() != '\n') 或 fgets) |
| 读入长字符串程序崩溃 | %s 未限宽,写入越过数组边界 | %31s 等限宽;更推荐 fgets |
| sprintf 拼接后程序崩溃 | 目标缓冲区不够大,越界写 | 一律 snprintf + 返回值截断判断 |
| 金额用 %.2f 输出"少一分钱" | 二进制浮点表示误差 + 舍入规则 | 金额用定点(分)或十进制库;比较用容差 |
| scanf 编译告警"format %d expects int*" | 忘了 &(传地址) | scanf("%d", &n) 补上取址符 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-2-7-1.printf("[%05d]", 42) 的输出是?(单选)
◌ 未作答ex-1-2-7-2.scanf("%d", &n) 的返回值 0 表示什么?(单选)
◌ 未作答ex-1-2-7-3.先 scanf("%d", &n) 读一个整数,紧接着 scanf("%c", &c) 想读一个字符,结果 c 得到了换行符——原因和修法?(单选)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
用 snprintf 重构格式化拼接代码
找一段(或写一段)使用 sprintf/手动拼接的格式化输出代码,用 snprintf 重构: 统一缓冲区边界、利用返回值做截断判断,并解释为什么生产代码用 snprintf 而不是 sprintf。
输入与输出
交付物:① 重构前与重构后的两份代码;② 运行输出(至少覆盖"正常"与"截断"两种情况); ③ 3 句话以内的原因说明。
功能要求
- 重构前代码使用 sprintf 或 strcat 拼接(可参考 1.2.6 的拼接例子)
- 重构后一律 snprintf(dst, sizeof(dst), ...)
- 用 snprintf 返回值实现截断判断并输出提示
- 编译使用 -std=c11 -Wall -Wextra -Wpedantic,0 警告
限制条件
- 不执行会越界写的 sprintf 版(其风险只作静态说明)
- 输入来自固定文件或命令行参数,保证可复现
验收步骤(自检清单 0/3)
验收标准
- 重构后程序在正常输入下输出与重构前一致(验收步骤 2)
- 截断场景下输出截断提示且程序不越界(验收步骤 2)
- 说明包含"边界限制 + 返回值截断判断 + 必写 \\0"三点中的至少两点(验收步骤 3)
常见失败原因
- snprintf 返回值理解错(当成实际写入数,截断判断写反)
- 缓冲区大小手写常量而不是 sizeof
- sprintf 版本真的执行了越界写(禁止)
可选扩展
- 用 -Wformat-truncation 观察编译期告警,并说明它的局限
- 把 scanf 输入改造成 fgets + sscanf 双函数方案
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
scanf 的返回值有什么用?输入残留(缓冲区里遗留的换行符)会造成什么现象?高频C/C++ · medium
要点:返回值 = 成功匹配的项数,用于判断读取成败;输入残留指上次读取后留在缓冲区的内容(如 \n)被后续 %c/gets 读走,产生"跳过输入"现象。
scanf 返回成功匹配并赋值的项数:scanf("%d %d", &a, &b) 返回 2 才是完全成功; 返回 0 说明格式不匹配(且输入未被消费,仍留在缓冲区);返回 EOF 说明输入结束。 输入残留的典型场景:scanf("%d") 读走数字后,行尾 \n 还在缓冲区, 紧接着的 scanf("%c") 会直接把这个 \n 读走——用户感觉"跳过了输入"。 解决:%c 前加空格(" %c" 跳过空白);或改用 fgets 整行读取再解析(工程推荐)。
追问:- 追问:scanf("%d") 遇到 'abc' 会发生什么?(返回 0,'abc' 残留,死循环风险)
- 追问:为什么更推荐 fgets + sscanf?(行语义清晰、不依赖缓冲状态)
评分要点:- 返回值语义
- 残留机理与现象
- fgets+sscanf 的工程替代
sprintf 和 snprintf 有什么区别?生产代码为什么用 snprintf?高频C/C++ · medium
要点:sprintf 无边界检查,目标小于内容即缓冲区溢出;snprintf 受 n 限制、必写 \0,且返回值是"本应写入"长度可判断截断。
sprintf(dst, fmt, ...) 不知道 dst 的容量,内容超出即越界写——与 gets 同类风险。 snprintf(dst, n, fmt, ...) 最多写 n-1 个字符并保证结尾 \0;返回值是"若空间充足本应写入" 的字符数(不含 \0):返回值 >= n 即说明发生截断,可据此做错误处理或扩容。 生产代码一律用 snprintf(或 asprintf 等带边界的变体);sprintf 仅在被证明安全的 固定缓冲区场景谨慎使用。加分项:提到 -Wformat-truncation 编译告警能在编译期拦截部分截断。
追问:- 追问:snprintf 返回负值是什么情况?(编码错误;POSIX 另有具体 errno)
评分要点:- sprintf 无边界 → 溢出
- snprintf 三特性(限长/必 \0/返回值语义)
- 工程结论
printf("%.2f", f) 输出的是浮点数的精确值吗?为什么?高频C/C++ · medium
要点:不是。二进制浮点不能精确表示多数十进制小数,%.2f 只是按四舍五入规则显示的近似值。
double 是 IEEE 754 二进制浮点:0.1、0.2 等十进制小数在二进制中是无限循环小数, 存储时被舍入为最接近的可表示值。printf 的 %f 系列只是把这个近似值再按 十进制格式化显示(%.2f = 四舍五入保留两位)。所以 3.14159 显示为 3.14 是 显示规则的结果,不是"精确到两位"。工程上金额等场景要用定点整数(分)或 十进制库,比较浮点用容差(fabs(a-b) < epsilon)而不是 ==。
追问:- 追问:为什么 0.1 + 0.2 不等于 0.3?(二进制舍入误差的经典演示)
评分要点:- 二进制浮点的表示局限
- 显示舍入 vs 存储精度
- 工程对策(定点/容差)
⑫ 延伸阅读
- 《C 程序设计语言(K&R)》第 7 章(输入输出,格式化的完整参考);
- cppreference.com:printf / scanf 的格式说明符页面(只引名称);
man 3 printf/man 3 scanf(Linux 本地手册);- 下一章预告:1.3.1 指针基础与 const 指针——scanf 的
&与数组名的"地址"问题将在那里得到彻底解答。
迁移训练(migration training)
把本章技能迁移到 Linux 与 嵌入式环境:
| 环节 | Windows(MinGW) | Linux | 说明 |
|---|---|---|---|
| printf/scanf 格式 | 一致 | 一致 | 按 C 标准实现 |
| 输入重定向 | < 文件 | < 文件 | 本章 ex2/ex3 用 freopen 固定输入,双平台可复现 |
| 浮点显示 | 一致 | 一致 | 均为二进制浮点 + 十进制舍入显示 |
| 嵌入式 printf | 重定向到 UART(1.3.10/支线 A3) | 终端 | 格式化语义不变,输出后端不同 |
不变的:格式串语法、返回值语义、残留机理、snprintf 三特性;要改的:输出后端(终端/串口/日志)。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| printf 格式串/宽度/精度、scanf 用法与返回值、sprintf/snprintf/fprintf 原型 | 第一阶段讲义 | 18.4 节(部分)与 Linux IO 相关页 | 【来源】 | 正文在原资料基础上重新组织表述,未大段复制原文 |
| snprintf 返回值语义与截断判断、scanf 限宽与输入残留处理、浮点显示"舍入非精确"说明、实验与练习 | 无 | 【补充】 | 原资料未覆盖安全与工程细节;本章新编约 30%(实验为本机 gcc 15.2.0 实测) | |
| 小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读 | 无 | 【补充】 | 原资料该章无成体系练习,全部新编 |