
学了C语言几年的朋友回头再看scanf和printf往往会感叹一句这两个函数天天见但真要说出个所以然来还真不容易。我自己带过不少初学者发现大家卡住的往往不是链表、指针那些“大东西”反而是scanf输入不对、printf输出乱格式、中文字符乱码这类基础问题。这篇文章就把这两个函数掰开揉碎讲一遍从最基础的调用逻辑到缓冲区、返回值、格式控制符再到中文乱码、重定向、嵌入式场景下的printf改造全部串起来讲。无论你是刚学C语言的大学生还是准备计算机二级、蓝桥杯这类竞赛或者工作中需要用C做嵌入式开发这篇文章都值得收藏慢慢翻。1. 先把基本功盘明白scanf、printf的调用逻辑与格式说明符速查1.1 scanf和printf到底是什么关系很多人把scanf和printf当成两个孤立的函数实际上它们是一对“镜像”操作printf负责把内存中的数据按指定格式“格式化”后输出到标准输出通常是屏幕scanf负责从标准输入通常是键盘读取字符流按指定格式“解析”后存进内存变量。打个比方printf像是在盖房子时把图纸画出来给别人看scanf则是看图纸把房子结构记进脑子里。一个向外表达一个向内解析。两者共用同一套“格式说明符”语法也就是%d、%f、%s这些标记但使用规则有本质区别尤其是传参方式这个后面详细说。我还想强调一个底层概念C语言中不存在真正意义的“输入输出语句”一切IO都是函数调用。printf和scanf只是标准库函数它们的核心工作依赖操作系统提供的文件描述符机制stdin、stdout、stderr是三个默认打开的“流”。理解这一点后面理解fprintf、freopen重定向会顺畅很多。1.2 格式说明符速查表我在教学中习惯让学生先背一张速查表把常用格式说明符和它的含义对应起来。这里整理出最常用的部分也是笔试面试最爱考的格式说明符含义典型用法%d有符号十进制整数printf(%d, n);%i等价于%dscanf中略有区别scanf(%i, n);%u无符号十进制整数printf(%u, n);%f / %lf单精度/双精度浮点数printf(%f, f);%c单个字符printf(%c, ch);%s字符串按\0结束printf(%s, str);%x / %X十六进制整数小写/大写字母printf(%x, n);%o八进制整数printf(%o, n);%p指针地址printf(%p, ptr);%e / %E科学计数法浮点数printf(%e, f);%g / %G自动选%f或%e中更短的形式printf(%g, f);%%输出一个百分号printf(100%%);注意表格里%f和%lf的对应关系。在printf中float参数会被自动提升为double所以%f可以输出float和double但scanf必须严格区分float用%fdouble用%lf搞混了会直接导致内存写入错位程序崩溃或者数据完全不对。这一点极容易踩坑后面章节会用示例代码验证。1.3 最小可运行的例子先给个最简单的例子把输入输出串起来跑一遍#include stdio.h int main(void) { int age; float height; printf(请输入年龄和身高用空格或回车分隔); scanf(%d%f, age, height); printf(年龄%d岁身高%.2f米\n, age, height); return 0; }这里有几个细节值得注意scanf的格式串里%d%f之间没有空格但实际输入时可以用空格、Tab或回车分隔因为scanf对于数值类型会自动跳过空白字符。printf里的%.2f表示保留两位小数这是格式控制的常见用法。scanf必须传变量的地址age、height而printf直接传值age、height这个差异背后是C语言的“值传递”机制下一章详细讲。2. scanf的隐藏关卡缓冲区残留、返回值与传地址这三个绕不开的坎2.1 输入缓冲区回车键按下之后究竟发生了什么很多初学者会碰到一个经典问题连续两次调用scanf读取字符第一次输入完按回车第二次scanf根本没执行就跳过了。这就是缓冲区残留问题。键盘输入的数据并不会直接送到程序里而是先进入一个叫“输入缓冲区”的内存区域。你按下回车键时\n字符也会被写进缓冲区。scanf读取数据时会根据格式说明符“消费”缓冲区里的部分内容但消费规则各有不同%d、%f会自动跳过前导空白字符包括空格、Tab、换行所以数值输入时回车残留在缓冲区通常不是问题。%c只读取一个字符不会跳过空白。第一次输入字符后按下的回车符\n会被第二次的%c原样读走。来看这个经典反例#include stdio.h int main(void) { char a, b; printf(请输入第一个字符); scanf(%c, a); printf(请输入第二个字符); scanf(%c, b); printf(a%c b%d\n, a, b); return 0; }程序运行后你输入x加回车期待第二次输入y结果发现第二个scanf直接读取到了\n。输出结果往往是b10也就是换行符的ASCII码。解决办法也不复杂在第二次scanf之前把缓冲区残留的清掉常见做法while (getchar() ! \n);这行代码会不断读取字符直到遇到换行符为止相当于把缓冲区“冲刷”干净。另一个方法是scanf格式串里加一个空格让%c前导空格跳过空白字符scanf( %c, b);格式串开头的空格告诉scanf先跳过所有空白字符再读取。这也是我在实际开发中最常用的做法简单可靠。2.2 返回值判断成功还是要死循环的关键scanf的返回值是成功匹配并赋值的输入项个数。比如scanf(%d%d, a, b)成功读到两个整数就返回2只读到一个就返回1没读到就返回0读到文件结尾或发生错误时返回EOF即-1。这个返回值在很多场景下是程序逻辑的核心。典型应用是多组数据输入的循环读取#include stdio.h int main(void) { int sum 0; int num; while (scanf(%d, num) 1) { sum num; } printf(总和%d\n, sum); return 0; }运行这段程序不断输入整数程序持续累加直到你输入CtrlZWindows或CtrlDLinux表示输入结束循环退出。很多在线评测系统的多组输入题目就是这个套路。还有一种写法是while (scanf(%d, n) ! EOF)这两种在绝大多数情况下等价但严格来说! EOF和 1有细微差别如果输入了一个无法解析的字符比如abcscanf返回0! EOF仍会继续循环导致死循环 1则会直接退出。从工程角度我建议用 成功匹配的数量这种写法更严谨。那么问题来了如果真输入了abcscanf返回0之后abc还留在缓冲区里你怎么处理这时候需要用while(getchar() ! \n);清掉这行残留程序才能继续运行。否则下一次scanf读到a还是失败陷入死循环。2.3 为什么scanf传地址而printf传值这个问题几乎每个初学者都会问。本质上C语言只有值传递一种参数传递方式scanf要修改变量的值就必须拿到变量在内存中的地址通过指针间接写入。而printf只读取变量的值不需要修改所以直接传值即可。我用一个类比帮助理解你让快递员帮你取件scanf得告诉他地址a你只是向别人展示你手里的东西printf直接把东西递过去a就行。数组名在这个规则下有点特殊。char str[100]作为参数传给scanf时直接写scanf(%s, str)而不是scanf(%s, str)因为数组名在表达式中会退化为指向首元素的指针本身就是一个地址值。写成str反而会得到“指向整个数组的指针”类型不匹配在部分编译器下会有警告。这也是C语言初学者容易混淆的地方记住一句话数组名本身就是地址。2.4 scanf读取字符串的边界问题%s读字符串会遇到一个限制遇到空格、Tab、换行就停止。所以scanf(%s, str)根本无法读入“Hello World”这样的带空格字符串只会读到“Hello”。如果要读入一整行可以用gets极不推荐因为它不检查缓冲区长度容易溢出C11标准已删除或者fgetschar str[100]; fgets(str, sizeof(str), stdin);fgets会连换行符一起读入所以输出时经常要多处理一个\n。也可以使用scanf的%[^\n]这种扫描集语法scanf(%[^\n], str);这个语法表示匹配除换行符以外的任意字符可以读入带空格的整行。但注意它不会消费尾部的换行符下一次读取前需要手动清掉。我个人的建议是工程代码里尽量用fgets读行再用sscanf解析这是最稳健的组合后面第四章会专门讲sscanf的用法。3. printf的格式控制打磨宽度、精度、标志位与常见组合的真面目3.1 格式串的基本结构%[标志][宽度][.精度][长度修饰符]类型很多人看printf格式串时只关注%d、%f这些类型符其实格式串还有一套完整的“修饰语法”%[标志][最小宽度][.精度][长度修饰符]类型四部分都可以组合我用几个例子拆解%-10d-是标志表示左对齐10是最小宽度不足10位用空格补齐。%08x0标志表示用0填充8表示最小宽度为8位。%8.3f8是最小宽度.3是精度表示保留3位小数。%ldl是长度修饰符表示long类型。%zuz是长度修饰符表示size_t类型。这几个部分可以任意组合理解这个结构后看到再复杂的格式串也能一眼拆解。3.2 宽度与对齐%5d、%-5d、%05d背后的意义宽度控制在实际项目中特别常用尤其是打印表格、对齐日志时。看这段代码的输出结果#include stdio.h int main(void) { printf(|%5d|\n, 42); // 右对齐宽度5输出 | 42| printf(|%-5d|\n, 42); // 左对齐宽度5输出 |42 | printf(|%05d|\n, 42); // 右对齐宽度5空位补0输出 |00042| printf(|%5d|\n, 42); // 显示正号输出 | 42| return 0; }宽度指定的数值如果小于实际位数宽度会被忽略不会截断数据。比如%2d输出1234结果就是完整的“1234”不要指望它只显示两位。0补位标志和-左对齐标志是冲突的因为左侧对齐后右侧都是空格没有补零的余地实际行为由编译器决定标准没有明确要求。所以别写%-05d这种无意义代码。对齐格式在实际打印中很实用比如打印学生成绩表printf(%-10s %6s %6s\n, 姓名, 语文, 数学); printf(%-10s %6.2f %6.2f\n, 张三, 87.5, 92.0); printf(%-10s %6.2f %6.2f\n, 李四, 91.5, 88.5);名字左对齐、成绩右对齐整个表格看着非常舒服。3.3 精度不是只对浮点数有效说到精度很多人只会想到%.2f保留两位小数。实际上精度对字符串和整数也有作用printf(%.5s\n, hello world); // 只输出前5个字符hello printf(%.5d\n, 42); // 等价于%05d输出00042 printf(%.2f\n, 3.14159); // 输出3.14字符串截断功能在处理超长字符串时很实用可以防止一大片文字刷屏。在处理用户输入或日志输出时我经常用它限制长度比如printf(%.80s\n, log_message)只打印前80个字符。3.4 浮点输出的显示策略与十六进制风格浮点数输出有几个容易混淆的点。%f默认保留6位小数%e用科学计数法%g会自动在两者之间选择更短的形式printf(%f\n, 3.1415926535); // 3.141593四舍五入保留6位 printf(%.2f\n, 3.14159); // 3.14 printf(%e\n, 12345.6789); // 1.234568e04 printf(%g\n, 12345.6789); // 12345.7自动选择十六进制输出对调试非常有帮助%x配合#标志可以输出带0x前缀的形式int n 255; printf(%#x\n, n); // 0xff printf(%#X\n, n); // 0XFF printf(%#o\n, n); // 0377这里#标志的作用是让输出带上C语言风格的进制前缀。我在调试协议数据时经常用%02x这种格式把字节以两位十六进制输出方便对照协议文档unsigned char buf[] {0xAA, 0x01, 0x02, 0x3C}; for (int i 0; i sizeof(buf); i) { printf(%02x , buf[i]); } printf(\n);输出aa 01 02 3c协议帧一目了然。4. 从终端到文件和字符串fprintf、fscanf、sprintf、snprintf的定位与取舍4.1 printf和fprintf的关系只不过流对象不同理解了printf只是fprintf的一个特例整个格式化输出家族就串起来了。printf本质上就是fprintf(stdout, ...)把格式化数据写到标准输出流。而fprintf可以把数据写到任何FILE*流最常见的就是文件。反过来scanf就是fscanf(stdin, ...)从标准输入读取格式化数据。这个认知在日常开发里非常有用。比如程序需要同时输出到屏幕和日志文件fprintf可以一个操作同时搞定FILE *log fopen(log.txt, a); if (log) { fprintf(log, [INFO] %s:%d 用户登录成功\n, __FILE__, __LINE__); fclose(log); }4.2 用fprintf/fscanf保存和加载带格式数据我来写一个完整案例把一组学生成绩保存到文件再从文件读出来。这个过程展示了fprintf和fscanf的配合。#include stdio.h #include string.h typedef struct { char name[20]; int score; } Student; int main(void) { Student students[] { {张三, 87}, {李四, 92}, {王五, 78} }; int count 3; // 写入文件 FILE *fp fopen(scores.txt, w); if (fp NULL) { perror(打开文件失败); return 1; } for (int i 0; i count; i) { fprintf(fp, %s %d\n, students[i].name, students[i].score); } fclose(fp); // 读回文件 Student read_students[10]; int read_count 0; fp fopen(scores.txt, r); if (fp NULL) { perror(打开文件失败); return 1; } while (fscanf(fp, %19s %d, read_students[read_count].name, read_students[read_count].score) 2) { read_count; } fclose(fp); // 打印读回结果 for (int i 0; i read_count; i) { printf(%s: %d\n, read_students[i].name, read_students[i].score); } return 0; }这里有几个细节值得展开%19s限制字符串读取长度防止超出name[20]缓冲区这是很多开发者容易忽略的安全点。fscanf的返回值判断用的是 2确保每次迭代都完整读到了姓名和分数。如果文件意外损坏循环能安全退出不会用未初始化的数据。我特意用了结构体存储数据结构化读写可以让代码更清晰。4.3 字符串格式化sprintf的隐患、snprintf的正确姿势sprintf把格式化的内容写入字符串缓冲区它和printf的关系可以类比为printf输出到屏幕sprintf输出到内存字符串。这个函数在生成消息、拼接SQL、构建协议报文时非常常用。但sprintf有个著名缺陷不检查缓冲区长度很容易导致缓冲区溢出这是无数安全漏洞的根源。C99标准引入了snprintf它多了一个size参数指定最大写入字符数包括末尾的\0char buf[64]; snprintf(buf, sizeof(buf), 温度%.2f湿度%.2f%%, 25.36, 60.5);这里sizeof(buf)告诉snprintf最多只能写63个有效字符加一个\0超出的部分被安全截断。我见过不少老代码还在用sprintf每次看都替他们捏把汗一旦拼接的字符串长度超出预期轻则数据损坏重则程序崩溃甚至被利用执行任意代码。另外一个容易忽略的点snprintf的返回值是“如果缓冲区足够大应当写入的字符数”而不是实际写入的字符数。如果返回值为负数表示编码错误如果返回值大于等于缓冲区大小说明发生了截断。在严格要求完整性的场景比如拼网络协议报文你要检查这个返回值int len snprintf(buf, sizeof(buf), %s:%d, host, port); if (len 0) { // 编码错误 } else if (len (int)sizeof(buf)) { // 截断了说明buf太小 }4.4 sscanf的“完整消费”检查sscanf从字符串中解析数据它的灵活度和fscanf一样同时有一个很少有人用到的技巧通过检查返回值加尾随字符占位判断字符串是否被完整解析。最常见的场景是解析用户输入判断输入是否合法#include stdio.h int main(void) { char input[100]; int year, month, day; char extra; printf(输入日期格式2024-05-20); fgets(input, sizeof(input), stdin); // %d-%d-%d 解析日期%c捕获后面多余字符 if (sscanf(input, %d-%d-%d%c, year, month, day, extra) 3) { printf(解析成功%d年%d月%d日\n, year, month, day); } else { printf(格式不对请按 2024-05-20 格式输入\n); } return 0; }关键点在extra这个“陷阱字符”。如果用户输入2024-05-20abcsscanf的返回值是4因为%c匹配到了a这样就知道输入后面还有多余内容可以拒绝处理。如果返回值是3说明日期解析完正好到了字符串末尾后面没有任何垃圾字符。这个思路可以有效过滤掉“用fgets读入字符串再用sscanf验证格式”场景下的无效输入比单纯用%d%d%d解析要稳健得多。5. 高频实战问题处理中文乱码、printf重定向和循环读入的一揽子方案5.1 中文乱码的根因与实用解法中文乱码是C语言初学阶段最让人崩溃的问题之一字符串明明写的是中文运行出来全是锟斤拷或者。出现这个问题的根本原因是“三方编码不一致”源文件保存时的编码、编译器编译时认定的编码、控制台显示代码页这三者只要有一个不匹配就会乱码。Windows下最容易出问题。我用几种常见环境说明MSVCVisual Studio默认源文件可能是GBK/GB2312保存控制台默认代码页936GBK一般没事。但如果你把源文件另存为UTF-8不带BOMMSVC会按本地代码页去解释中文就变乱码了。MinGW GCC VSCodeVSCode默认UTF-8GCC也按UTF-8解释源文件但Windows终端默认代码页是GBK输出UTF-8的字节序列就被解析成乱码。Linux GCC整个链路几乎都是UTF-8一般不会乱码。最简单的处理思路统一到UTF-8。源文件保存为UTF-8编译器按UTF-8处理终端代码页切到65001UTF-8。具体操作上Windows终端可以在程序里调用API切换代码页#include stdio.h #ifdef _WIN32 #include windows.h #endif int main(void) { #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); #endif printf(你好世界\n); return 0; }也可以直接在终端执行chcp 65001手动切换。如果用的是VSCode把终端设置成默认65001或者在设置里加terminal.integrated.profiles.windows配置项也可以避免手动切换的麻烦。还有一个坑很多人没意识到UTF-8的中文字符串占用的字节数和GBK不一样。比如sizeof(你好)在UTF-8下是76个字节加结尾\0在GBK下是54个字节加结尾\0。如果你用char str[5]去存“你好”在UTF-8环境下会溢出。处理中文字符串时务必预留足够空间不要按字符数估算字节数。5.2 printf重定向嵌入式串口、文件日志与自定义输出printf默认输出到标准输出流但在嵌入式开发中单片机没有屏幕printf输出的内容默认会被丢弃。这时候就需要“重定向printf”让它输出到串口、LCD显示屏或其他外设。不同编译环境的实现方式不同。ARM GCC环境下典型做法是重实现_write函数#include stdio.h #include stdarg.h // 假设这是你的串口发送单字节的函数 void uart_send_char(char c); int _write(int file, char *ptr, int len) { for (int i 0; i len; i) { uart_send_char(ptr[i]); } return len; }有了这个底层重定向printf就会自动把内容逐字节发送到串口。这是嵌入式开发中最常用的调试手段看串口助手打印日志比用烧录器断点调试方便得多。另一种“重定向”是把printf输出转到文件借助freopen#include stdio.h int main(void) { // 把stdout重定向到文件 FILE *fp freopen(output.txt, w, stdout); if (fp NULL) { perror(freopen失败); return 1; } printf(这条内容会写入文件而不是屏幕\n); fclose(fp); return 0; }这个技巧在自动化测试、批量处理数据时非常好用程序逻辑完全不变只是输出的流向变了。如果是希望printf同时输出到多个地方或者想增加时间戳、日志级别直接重定向就不够了。更通用的做法是在printf外面包一层自己的日志函数利用C语言的可变参数机制实现。这个我在第6章给出完整模板。5.3 scanf循环读入的标准模板与异常输入处理很多做题和实际开发场景下需要连续读取未知数量的输入直到文件结束。前面提到的while (scanf(...) n)是最基础的模板但真正的工程场景要考虑“输入错了怎么办”。这段代码展示了一个相对健壮的循环读入#include stdio.h int main(void) { int num; int result; int count 0; printf(请输入若干整数输入非数字结束\n); while ((result scanf(%d, num)) 1) { count; printf(第%d个数%d\n, count, num); } if (result 0) { printf(检测到非数字输入程序结束\n); // 清掉残留的无效输入避免影响后续逻辑 while (getchar() ! \n); } else { printf(检测到EOF程序结束\n); } return 0; }这个例子里result保存了scanf的返回值分三种情况处理返回1说明读取成功返回0说明缓冲区里第一个字符就无法解析通常意味着输入了非数字返回EOF说明输入流已经结束CtrlZ/CtrlD。分开处理能给出更明确的提示也方便后续清理残留。另外提一个scanf与gets混淆的经典错误scanf(%d, n)可以读取数字但数字后面的换行符会留在缓冲区里。如果你紧接着用gets或fgets读字符串会直接读到一个空行。这时候还是老办法在切换读取方式前用while(getchar() ! \n);清掉缓冲。6. 进阶认知与个人习惯参数提升、未定义行为与日志打印的工程化落地6.1 printf背后的可变参数与默认参数提升printf能处理任意数量和类型的参数靠的是C语言的“可变参数”机制。它的原型是int printf(const char *format, ...);...代表不定数量的参数。在可变参数函数内部参数会经历“默认参数提升”float提升为doublechar和short提升为int。这就解释了为什么printf里%f可以同时输出float和double因为float进函数时已经变成double了。同理%d也可以输出char类型。这也解释了为什么scanf浮点类型不能混用%f和%lfscanf接收的是指针不存在参数提升它必须知道指针指向的是4字节的float还是8字节的double才能正确写入数据。如果你用%f去读double变量scanf只会写入前4字节整个变量的值就废了。理解这一点后你还能看懂一个经常出现在面试题里的坑printf使用%d输出long类型或者用%f输出float变量时如果精度不对都是类型不匹配的问题。正确的做法是注意长度修饰符long用%ldlong long用%lldsize_t用%zu。6.2 参数求值顺序是未定义行为别在printf里写i有一个经典坑几乎人人都跳过在printf参数里写自增表达式会怎样int i 5; printf(%d %d\n, i, i);这段代码在不同编译器下可能输出5 7、6 6、5 6等各种结果因为C语言标准规定函数参数的求值顺序是“未指定行为unspecified behavior”编译器可以自由决定从左到右还是从右到左求值。正常情况下顺序不重要但一旦参数里有i、i这类有副作用的表达式结果就完全不可预测了。我自己用GCC和MSVC实测过同一个表达式在两套编译器下输出不一样。所以遇到这种代码最好的处理方式是两个字别写。把自增拆到独立的语句里printf(%d\n, i); i;这个原则也适用于表达式里的嵌套赋值、函数调用等副作用操作。面试问“这段代码输出什么”的时候可以直接回答“未定义行为无法确定”这才是正确答案。6.3 工程化日志打印的一个小模板了解了printf的重定向和可变参数机制后可以做一个实用的小工具一个带日志级别和时间戳的日志函数。这个模板我在各类项目中反复用无论PC还是嵌入式都能改造适配。#include stdio.h #include stdarg.h #include time.h #define LOG_DEBUG 0 #define LOG_INFO 1 #define LOG_WARN 2 #define LOG_ERROR 3 static int log_level LOG_DEBUG; void set_log_level(int level) { log_level level; } void log_message(int level, const char *fmt, ...) { if (level log_level) { return; } const char *level_str[] {[DEBUG], [INFO], [WARN], [ERROR]}; time_t now time(NULL); struct tm *tm_now localtime(now); char time_str[32]; strftime(time_str, sizeof(time_str), %Y-%m-%d %H:%M:%S, tm_now); printf(%s %s , time_str, level_str[level]); va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); printf(\n); } int main(void) { log_message(LOG_INFO, 程序启动); log_message(LOG_DEBUG, 调试信息变量x%d, 42); log_message(LOG_WARN, 警告内存使用率%.1f%%, 85.3); log_message(LOG_ERROR, 错误文件打开失败); set_log_level(LOG_WARN); log_message(LOG_DEBUG, 这条不会显示); log_message(LOG_ERROR, 这条会显示); return 0; }这里用到了vprintf它是printf的“可变参数版”接收va_list类型的参数。整个模式可以灵活扩展把vprintf换成vfprintf(fp, ...)输出到文件或者换成自定义的重定向函数输出到串口日志系统就搭起来了。实际工作中我会在这个基础上再加三个能力按日期自动切换日志文件、日志文件大小限制、线程安全加锁。对于小型项目和嵌入式设备这个雏形已经足够用了。6.4 我在实际使用中积累的几个小习惯到这里scanf和printf的用法基本讲透了。最后分享几个我个人写C代码多年的小习惯不一定人人都认同但确实帮我省了很多调试时间。第一scanf的返回值永远不忽略。凡是用了scanf就一定判断返回值。哪怕只是单变量输入返回0或者EOF都意味着程序状态已经不受控制继续往下走只会产生更多问题。第二能用snprintf绝不用sprintf。这个没有任何妥协余地缓冲区安全是现代C编码的底线。第三printf的宽度精度控制尽量用上。调试数据按固定宽度输出十六进制用%02x统一两位浮点用%.2f限制小数位数这些细节一旦养成习惯输出会规范很多排查问题也快。第四日志输出保留时间和级别信息。不要打印裸的消息加上时间戳和级别问题定位效率翻倍。这不需要多复杂的框架上面那段模板就够了。第五遇到scanf和printf相关的问题先查缓冲区、再查格式串、最后查类型匹配。这个排查顺序能覆盖九成以上的格式输入输出问题。十几年前我自己学C语言时也被scanf的缓冲区坑得死去活来当时没有这么系统的梳理全靠一点一点踩坑积累。现在把这些经验写出来希望你能少走一些弯路。这两个函数虽然基础但用好了整个程序的健壮性和可维护性都会上一个台阶。