
刚开始学C语言的时候很多人是被 printf 和 scanf 绊住的。第一次写代码printf 可以顺利打出 “Hello World”于是你觉得自己已经摸到门道了可接着用 scanf 想读一个数字却发现程序要么卡住不动要么跳过一次输入要么输出一个莫名其妙的数。网上搜了一圈答案五花八门有人说加getchar()有人说清空缓冲区还有人说换编译器。折腾到最后你可能已经分不清问题到底出在哪。其实问题不出在环境也不出在编译器而在于没有理解 printf 和 scanf 到底在做什么。这两个函数看似是“输出”和“输入”但它们真正的本质是一套格式化文本处理机制一个是按模板把变量组装成文本另一个是按模板把文本解析成变量。如果你能接受这个视角后面很多坑都不再是坑而是可以预判的流程。1. 先搞清楚 printf 和 scanf 到底帮你做了什么1.1 格式化输出从“把变量显示出来”到“按模板组装文本”很多人把 printf 理解成“打印变量的值”这个说法没有错但如果只停在这一层后面的宽度、对齐、小数位、进制转换、返回值都会变成一堆孤立的知识点。printf 全称是 print formatted也就是“格式化打印”。它的核心不是输出而是组装文本。你给出一段格式模板再把变量交给它它按照模板把变量转换成人类能读的文本然后写到标准输出。比如这句代码int a 5; printf(a %d\n, a);a %d\n就是模板。%d是一个占位符表示“这里要放一个有符号十进制整数”。模板里除了占位符之外的其他字符例如a 和换行符都是原样输出的。所以最终输出的不是“a 的值”这几个字而是a 5这段文本。这个流程里最重要的一点是计算机内部存的其实是 5 的二进制形式printf 负责把它转换成十进制数字字符再交给终端显示。为什么要强调这一点因为一旦你明白“printf 按模板组装字符串”再看%5d、%.2f、%x、%-10s这些写法就全都有了解释它们都是对模板的控制让你决定数字占几列、保留几位小数、用什么进制输出、是左对齐还是右对齐。零基础阶段不需要背全各种格式控制符但一定要建立“模板”意识。以后你看到printf里的每一段内容都会自然地去想哪些是原样输出哪些是占位符哪些是修饰占位符的参数。1.2 格式化输入scanf 不是“读键盘”而是“按模板解析字符串”scanf 比 printf 更容易让人误解。我见过很多教材把 scanf 描述成“从键盘读入数据”这句话放在初学者阶段勉强能用但一旦你遇到输入残留、跳过输入、死循环就会知道这个定义不够。scanf 全称是 scan formatted做的事情是从标准输入流中读取字符然后按照格式串去解析这些字符把解析结果存到指定的地址中。它更像一个解析器而不是一个简单的“读取器”。举个例子。你在终端输入123键盘输入进入程序缓冲区时缓冲区里实际存的是三个字符1、2、3以及一个换行符\n。这时候执行int n; scanf(%d, n);scanf 看到格式串%d会先跳过前导空白字符也就是空格、Tab、换行然后从输入流里找连续的数字字符。它读到1、2、3拼出整数 123存入变量 n 的地址。但注意它读到3后面不是数字字符而是一个换行符就不再继续了。它不会把换行符消费掉。这个换行符就留在了缓冲区里成为下一次输入时的“残留垃圾”。如果下一次你用的是scanf(%d, m)因为%d会自动跳过前导空白所以换行符会被跳过好像没影响。但如果下一次你用的是scanf(%c, ch)那么%c不会跳过空白换行符就会被当成一个有效字符赋给 ch。这就是网上很多人说的“scanf 跳过了我的输入”的本质。它不是跳过了你这次的输入而是把你上次输入留下的换行符读进去了。理解到这一层scanf 就不再是黑盒了。它会按模板去匹配输入流的字符序列匹配不上就失败匹配成功就消费掉对应的字符。这不是简单“读键盘”而是一场字符解析过程。2. 让 printf 听话格式控制符、转义和常见乱码2.1 常用占位符到底应该怎么写零基础最容易犯的错误是占位符和变量类型不匹配。编译的时候可能不报错但运行结果完全不对。原因很简单printf 并不强制要求格式串和参数类型匹配它是按照格式串去解释内存里的二进制数据。一旦类型不匹配解释方式就会出错。常见的占位符我整理成一张表格式串含义常见对应类型%d有符号十进制整数int%ld有符号十进制长整数long%lld有符号十进制长长整数long long%u无符号十进制整数unsigned int%f十进制浮点数float/double输出%lf十进制浮点数doublescanf 输入%c单个字符char%s字符串char[]或字符指针%x十六进制整数int等整数类型%o八进制整数整数类型%p地址指针%%输出一个百分号无这里有一个特别容易混的点float和double。在 printf 中float类型参数会被自动转换为double所以输出float和double都用%f通常没问题。但是在 scanf 中float必须用%fdouble必须用%lf。因为 scanf 需要知道把解析出来的数字存到多大的内存空间里格式串写错会把内存写坏而不是简单地输出不对。比如double x; scanf(%f, x); // 错误%f 期望 float*不是 double*这个错误编译时往往会有警告但很多人不读警告运行结果就变成了随机数甚至程序崩溃。2.2 宽度、对齐、小数位从够用到可控printf 格式串后面可以跟修饰控制项。完整形式大致是%[flags][width][.precision][length]specifier不必一次性记全只需要知道几个常用场景。第一限制输出宽度printf(%10d\n, 42);这个会输出 10 列数字42靠右左边有 8 个空格。如果把%10d改成%-10d数字靠左右边补空格。这在做表格对齐时很有用。第二控制小数位printf(%.2f\n, 3.14159);输出3.14。.2表示保留两位小数。注意它做的是四舍五入不是截断。第三补零printf(%05d\n, 42);输出00042。0这个 flag 表示宽度不足时用0填充常用于编号、时间输出等场景。第四限制字符串长度printf(%.3s\n, hello);输出hel。这种写法在做截断显示时很实用。零基础阶段建议先掌握%d、%f、%c、%s和%.2f、%5d这几个就够了。剩下的等真正需要时再查不影响入门进度。2.3 中文乱码通常不是 printf 本身的问题很多人搜“printf 中文乱码”以为是 printf 编码问题。实际上printf 没有任何编码转换能力它只负责把字符字节输出到标准输出。中文乱码的根源通常是源代码文件编码、编译器默认编码、终端/控制台代码页这三者不一致。在 Windows 下特别常见源文件保存为 UTF-8编译器按 UTF-8 读取中文字符串控制台默认使用 GBK 代码页于是 UTF-8 的中文字节被 GBK 解码出现乱码。解决思路有几个在 Windows 控制台项目里把源文件编码改成 ANSI/GBK再重新编译这是比较省事的方法。使用 Windows APISetConsoleOutputCP(CP_UTF8);把控制台代码页切到 UTF-8但需要包含windows.h只适用于 Windows。或者不要在代码里写中文先全部用英文输出排除编码干扰。要记住这不是 printf 的 bug而是环境编码不一致。遇到乱码先检查源文件编码和控制台编码而不是去改 printf 参数。3. scanf 的坑几乎都来自输入缓冲区3.1 为什么你的程序会跳过一次输入很多初学者写过这段代码int age; char gender; printf(请输入年龄); scanf(%d, age); printf(请输入性别(M/F)); scanf(%c, gender);运行之后会发现程序还没有等你输入性别就已经把性别读走了甚至输出一个换行。原因就是我们前面说的第一个scanf(%d, age)读走数字后把用户按回车留下的\n留在了缓冲区。第二个scanf(%c, gender)期望读一个字符但它不会跳过空白于是直接读到\ngender变成了换行符。解决方法很简单在%c前面加一个空格scanf( %c, gender);这里的空格不是装饰而是告诉 scanf先跳过一个或多个空白字符再读真正的字符。这个空格可以匹配缓冲区里残留的回车、空格、Tab。类似的情况还有%d后面紧接着%c、%s后面紧接着%c。只要记住一个规律%c不跳过空白其他常用的%d、%s、%f会自动跳过前导空白。如果你需要明明一个字符而前面可能有残留空白就显式在格式串里补一个空格。3.2 返回值为什么要检查scanf 的返回值是成功赋值的参数个数。如果读取成功两个%d就返回 2如果一开始格式就不匹配返回 0如果读到输入流末尾返回 EOF。很多教学代码不检查返回值因为运行题目数据总是正常的。但在真实的交互场景里用户可能乱输入比如你让他输入整数他打了一个abc。此时scanf(%d, n)返回 0n没有被赋值程序却继续往下执行。这个n的值是是未初始化的垃圾值后续计算全部白算。一个更严谨的写法是int n; printf(请输入一个整数); while (scanf(%d, n) ! 1) { // 清掉当前行剩余字符 int c; while ((c getchar()) ! \n c ! EOF) {} printf(输入格式不正确请重新输入); } printf(你输入的整数是%d\n, n);这段代码看起来比单行 scanf 麻烦但它是工程习惯的起点输入之后必须检查是否成功解析。这个习惯越早建立后面踩的坑越少。3.3 清空缓冲区的常见做法以及它适合什么场合网上有个古老的说法用fflush(stdin)清空输入缓冲区。但这个行为在 C 标准里是未定义的。也就是说标准并没有规定fflush能用于输入流。某些编译器确实能工作但换一个环境可能就失效。对于想要写可移植代码的人来说不建议依赖它。可移植的清空办法是读走缓冲区里直到换行符的所有字符int c; while ((c getchar()) ! \n c ! EOF) {}这个循环把当前行剩下的字符全部吃掉包括最后的换行符于是下一次输入面对的是一个干净的缓冲区。还有一种更稳妥的方案是不要直接用 scanf 做行读取而是先用fgets读一行字符串再用sscanf从字符串里解析数据。例如char line[128]; int n; fgets(line, sizeof(line), stdin); if (sscanf(line, %d, n) 1) { // 解析成功 }好处是输入和解析分开缓冲区残留的问题会减少很多。零基础阶段可以先不用这种方式但要知道还有这个选择。这一章其实可以沉淀成一个“输入排查四步法”我建议你把它写进自己的笔记里先看格式串和变量类型是否匹配。再检查 scanf 的参数有没有写字符串数组名除外。再看看输入缓冲区里有没有上一次残留的空白或换行尤其是%c之前。最后检查 scanf 的返回值确认你到底成功读到了几个值。这四个顺序是固定的从格式串、到参数、到缓冲区、到返回值。遇到任何 scanf 异常按这个顺序排查比盲目加getchar()靠谱得多。4. 从单次输入输出走向真正的小程序4.1 一个典型例子计算日期是该年的第几天输入一个日期的年、月、日计算并输出这是这一年的第几天。这个题目很常见搜 C 语言练习题时经常能看到它很适合用来巩固 printf 和 scanf。完整代码可以这样写#include stdio.h int main(void) { int y, m, d; int days_in_month[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int total 0; printf(请输入年 月 日空格分隔); while (scanf(%d %d %d, y, m, d) ! 3) { int c; while ((c getchar()) ! \n c ! EOF) {} printf(输入格式不正确请重新输入); } if ((y % 4 0 y % 100 ! 0) || (y % 400 0)) { days_in_month[1] 29; } for (int i 0; i m - 1; i) { total days_in_month[i]; } total d; printf(%d年%d月%d日是这一年的第%d天\n, y, m, d, total); return 0; }这里有一个细节scanf(%d %d %d, y, m, d) ! 3。因为输入格式要求三个整数所以只有当返回值是 3 时才表示全部读入成功。如果用户输入2025 13只给了两个数返回值就是 2循环会提醒用户重新输入。另外要注意这个例子还没有对月份做边界校验。如果用户输入2025 13 1循环会退出但循环里的m - 1会访问数组下标 12已经越界。真实程序里应该再检查y 1、m 1 m 12、d 1 d days_in_month[m - 1]。零基础阶段可以先不写但应该意识到“输入合法”不等于“输入有效”。4.2 printf 和 scanf 返回值的用途很多人以为 printf 没有返回值其实它返回的是已经输出的字符数量。如果输出过程中发生错误会返回一个负数。这个返回值平时看起来没用但在日志系统、文件输出、串口发送场景里很有价值。比如你要确认一条关键日志是否完整发送就可以检查 printf 的返回值是否等于预期字符数。scanf 的返回值在前面已经讲过它表示成功赋值的变量个数。这个值不是摆设在题目要求“多组输入读到文件末尾为止”时通常写成while (scanf(%d, n) ! EOF) { // 处理 n }或者在数据条数固定时while (scanf(%d %d, a, b) 2) { // 处理 a b }理解这两个函数的返回值能帮你写出不会因为一次异常输入就崩溃的小程序也能让你以后读别人代码的时候更顺畅。4.3 在嵌入式场景里printf 为什么总是被重定向搜索热词里有“printf重定向”这通常是嵌入式开发会遇到的事。很多人第一次看到 printf 在单片机上不能直接使用甚至编译链接不通过会觉得很奇怪printf 不是标准库函数吗标准库函数没错但它要输出到“标准输出”。在 PC 上标准输出默认是控制台在单片机上根本没有控制台。于是你需要在底层提供一个字符输出函数把 printf 要输出的每一个字符转发到串口、屏幕或日志缓冲区。这个过程叫重定向。常见的做法是重写_writeARM/Keil 环境或fputc某些 GCC 环境让你自己的串口发送函数被 printf 内部调用。零基础阶段不需要马上掌握这些但可以借此理解一个关键点printf 是分层设计中的上层它不直接操作硬件而是依赖“底层把字符送出去”的能力。这个思维以后学嵌入式、学文件 I/O、学网络日志都能用上。同样scanf 也可以被重定向让程序从串口接收数据并解析。到那时候你学过的格式串、缓冲区、返回值都会原封不动地迁移过去。这就是为什么我说现在花时间搞懂 printf 和 scanf不是在背 API而是在建立格式化解析的思维方式。4.4 从“会格式化”到“会调试”对零基础来说printf 是最好的调试工具。程序运行到哪里了某个变量的值是什么循环执行了几次都可以用 printf 打印出来。我一般会在不确定的代码位置加一行printf(checkpoint: n %d, total %d\n, n, total);运行后看输出就能快速定位到底哪一步不符合预期。这种调试法尤其适合循环和分支逻辑。不过也有两个建议调试用的 printf 不要一堆堆地写打印太多反而分不清关键变量。调试完记得删掉或者用注释标记不要留在最终代码里。等到你熟悉了 IDE 的断点调试可以把 printf 调试和断点调试结合使用。前者适合观察连续变化后者适合定位单点异常。5. 零基础最容易误判的几条边界5.1 scanf 的输入类型必须与格式串严格匹配格式串和变量类型不匹配在 scanf 里不只是输出不对还可能造成内存写入越界。比如double x; scanf(%d, x);%d会把输入解析成 int然后把结果写到一个double大小的内存区域。但由于指针类型不同写入方式是int*的写入方式最终x的内存布局完全错乱。程序不一定会立刻崩溃但它已经是一个有未定义行为的程序了。更隐蔽的问题超出范围。int通常占用 4 字节能表示的范围有限。如果用户输入的数字超出 int 的范围结果也是未定义的。所以正规程序不仅要检查 scanf 返回值还要判断数值是否在合理范围内。5.2 字符串输入%s 的天然边界scanf(%s, str)看起来简单但有两个致命问题。第一它不能读取带空格的字符串。输入hello world它只会把hello存入strworld留在缓冲区。因为%s遇到空白就停止。第二如果不指定宽度它可能越界。假如char str[10]用户输入abcdefghijklscanf 会把 12 个字符写进数组数组只有 10 个字节内存越界。正确的做法是让 scanf 最多读入指定数量的字符例如char str[10]; scanf(%9s, str);9表示最多读 9 个字符最后一个位置留给字符串结束符\0。这样能有效防止溢出。如果确实要读一行带空格的文本不要用%s改用fgetschar line[100]; fgets(line, sizeof(line), stdin);5.3 这些内容暂时不用学但知道以后会少走弯路到了这里你会看到 printf 和 scanf 只是标准 I/O 的一部分。后续你还会遇到fprintf/fscanf输出到文件或从文件读取格式串逻辑和 printf / scanf 一模一样的。sprintf/sscanf把格式串写到字符数组或从字符数组里解析数据。fgets/getchar/putchar面向字符和行的输入输出更贴近底层。它们的共同点都是围绕“格式化”这个概念。所以现在掌握好 printf 和 scanf其实是在为文件读写、日志处理、协议解析打下基础。另外有两条红线要记住不要用gets这个函数在 C11 标准里已经被移除因为它无法限制输入长度极其容易越界。不要用fflush(stdin)来清空输入缓冲区因为它不是标准行为换一个平台可能就会出问题。5.4 适用边界这个教程适合谁不适合谁这一套内容最适合的是刚学 C 语言的零基础读者尤其是准备参加学校机考、等级考试或者刚开始做简单交互式编程题的人。你只需要把格式串写对、把别漏、把返回值看清楚就能解决绝大多数入门问题。但它不适合所有场景。如果你是在做高性能数据解析需要每秒处理几十万行文本那么手写解析可能比 scanf 更合适因为 scanf 的格式解析有额外开销。如果你在做底层嵌入式驱动需要精确控制每个字节的收发那么也不应该依赖 scanf而是直接用寄存器或驱动接口读取数据。这意味着printf 和 scanf 不是万能钥匙它们是“入门到工程之间的第一层阶梯”。把这一层走稳再去理解更底层的读写会顺很多。如果一定要给一个下一步建议我会说从今天开始每次写完 scanf都问自己四个问题——格式串和变量类型匹配吗参数有没有写缓冲区里有没有残留scanf 的返回值有没有检查把这四个问题变成条件反射比你多做十道题都管用。