跳到主要内容
🔍
1.2.7已发布beginner · 约 2 课时 · P0

输入输出深入 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。

① 学习目标

  1. 用宽度/精度/对齐/补零写出正确的 printf 格式串,并知道 %.2f 是舍入显示而非精确值;
  2. 检查 scanf 的返回值(成功匹配项数),并说出返回 0 时"输入未被消费"的含义;
  3. 解释输入残留(缓冲区遗留 '\n')对后续 %c 的影响,并给出两种解决方案;
  4. %s 加限宽(%31s),说出它与 gets 的同类风险;
  5. 说清 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 / stderrfprintf(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 得到了换行符——原因和修法?(单选)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

用 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 实测)
小练习 / 章节测验 / 实践任务 / 面试问题 / 延伸阅读补充原资料该章无成体系练习,全部新编