
1. 项目概述与核心价值1.1 为什么scanf和printf是C语言的“命门”在C语言的学习和开发过程中scanf和printf几乎是最先接触、使用频率最高却也是问题最多的两个函数。我见过很多人能够熟练操作指针、玩转结构体结果在输入输出环节反复踩坑——中文乱码、浮点数精度丢失、scanf跳过输入、缓冲区残留问题哪个不是从新手期一路踩过来的。坦白讲这两个函数是真的“入门容易精通难”。printf负责格式化输出scanf负责格式化输入它们都被声明在标准头文件stdio.h中。视项目需求还需要加stdlib.h、string.h等头文件去配合处理字符和内存。这些函数的本质是C标准库提供给开发者的“数据翻译官”——把程序内存中的二进制数据与用户可以读写的字节流屏幕、键盘、文件之间做双向转换。翻译规则就是格式控制字符串里的那些占位符比如%d、%f、%s、%c、%x等。本篇文章不会泛泛而谈API列表而是把这两大函数放进真实开发场景里从格式说明符的底层机制、内存对齐、缓冲区行为、重定向到实战排查逐一拆开揉碎。无论你是刚学完hello world的初学者还是已经在嵌入式、算法题目里绕了好几圈的进阶者本篇文章都将帮助你打通“输入输出”这最后一公里的经脉。1.2 这篇文章能解决什么问题通过这篇文章你将搞清楚下面这些平时零散搜到、容易混淆的知识点%d、%u、%f、%lf、%s、%c、%x、%o等格式说明符的精确含义和匹配规则为什么用错类型会导致乱码和崩溃。printf格式化输出的完整套路宽度、精度、左对齐、补零、%n、%%、*修饰符等进阶能力。scanf的返回值、缓冲区残留、空白符处理、%s读取风险、%[^\n]整行读取技巧。printf重定向到串口、LCD、日志文件的完整思路——这几乎是嵌入式开发者的必修课。中文乱码的根因和解决方式以及在不同IDE和环境下的细节差异。常见高频笔试面试题背后的原理比如printf(%d, n)与printf(%d, n)有什么区别printf的求值顺序为什么不能依赖。fprintf、sprintf、snprintf、fscanf、sscanf等系列函数的定位和区别避免瞎用。2. 格式说明符的底层逻辑与选型2.1 从“翻译官”视角理解格式控制字符串printf和scanf的第一个参数都是格式控制字符串format string后面的参数个数是可变的。格式控制字符串由两类内容组成普通字符和转换说明conversion specification。普通字符在printf中就是要原样输出的文字在scanf中则是需要从输入流中精确匹配的字符。转换说明以%开头后面跟着格式化要求。举一个最简单的例子printf(Age: %d\n, age);Age:是普通字符%d是转换说明\n是换行转义。在底层printf会逐个解析格式字符串遇到%就分析后面的转换说明然后从可变参数列表中取对应数量、对应类型的数据按照指定格式转换成字符串写入标准输出流。这个机制有一个极其重要的推论printf本身并不检查参数类型是否正确。它完全相信程序员给的格式说明符和参数是匹配的然后在运行时从栈上或寄存器中取决于调用约定按类型所占字节数读取数据。如果你给了%f但传了一个int它读到的字节解释成浮点数的位模式输出必然是垃圾值。更危险的是读取字节数不一致还会导致后续可变参数的错位这在调试时往往表现得“莫名其妙”。在C11标准中编译器可以对printf做格式字符串检查。GCC和Clang都有-Wformat编译选项默认在-Wall中开启。一旦出现printf(%f, intVar)这样的不匹配编译器会给出警告。这个警告在平时练习时可以忽略在生产代码里必须当作错误对待。2.2 常用格式说明符对照表下面是实际开发中最常用的一组格式说明符我把它们按类别、读取/输出类型、典型示例整理成了表格便于查阅。格式说明符对应数据类型输出/读取方向说明示例%dint有符号十进制整数最常用整数格式printf(%d, -42);%uunsigned int无符号十进制整数输出无符号数scanf要与unsigned int*配套printf(%u, 3000000000U);%ldlong长整数输出/读取64位或32位取决于平台printf(%ld, 123456789L);%lldlong long64位有符号常用于大整数运算printf(%lld, 1LL 50);%ffloat读入、double输出自动转printf中float会提升为doublescanf必须用%f配float*printf(%f, 3.14);%lfdouble读入printf中%f和%lf等价scanf中必须区分scanf(%lf, d);%cchar输出单个字符scanf读取单个字符不跳过空白scanf(%c, ch);%schar*字符串首地址输出以\0结尾的字符串scanf读取非空白字符序列自动补\0printf(%s, name);%x/%Xunsigned int十六进制输出小写/大写字母printf(%x, 255); // ff%ounsigned int八进制输出printf(%o, 8); // 10%pvoid*输出指针地址常配合取地址printf(%p, var);%e/%Edouble科学计数法输出printf(%e, 12345.678); // 1.234568e04%g/%Gdouble简洁输出自动选择%f或%eprintf(%g, 0.00001);%%无输出一个百分号printf(100%%);这里有一个很多人都会混淆的点关于float和double在printf和scanf中的不对称规则。在printf中float类型的实参会被自动提升为double这是C语言默认实参提升规则因为可变参数列表中的类型没有原型信息所以%f和%lf都可以输出double结果一致。在scanf中传入的是指针没有默认实参提升。%f需要对应float*%lf需要对应double*。如果你用%f去读一个double变量scanf只会写入前4个字节后4个字节保留垃圾数据读取结果完全不可预测。注意scanf的所有格式说明符除%c、%[、%n之外都会自动跳过输入流中的空白字符空格、制表符、换行。%c会老老实实读入包括空格、换行在内的任意字符。2.3 为什么%s在scanf中最危险scanf配合%s读取字符串时会跳过前导空白然后持续读入非空白字符直到遇到空白字符为止然后在末尾自动追加\0。问题在于它根本不知道目标缓冲区有多大一旦输入内容超过数组长度就会发生缓冲区溢出直接覆盖相邻内存。char name[10]; scanf(%s, name); // 如果输入超过10个字符内存就被写穿了安全做法是使用宽度限制修饰符%9s这里的数字表示最多读取9个字符加上末尾的\0共10个字节刚好填满name[10]scanf(%9s, name); // 最多读取9个字符防止越界在GCC和Clang环境下-Wformat-truncation和-Wformat-overflow这类编译选项会主动检测某些printf/scanf的缓冲区截断和溢出隐患建议在编译时打开。对于生产级项目更推荐fgetssscanf的组合这一点在后面的章节详述。3. printf进阶从基础输出到高精度控制3.1 标志、宽度、精度与长度的完整组合printf格式说明符的完整形式为%[flags][width][.precision][length]specifierflags-左对齐、强制显示正负号、空格正数前加空格、0数字前补零、#备选格式如0x前缀、保留小数点。width最小输出宽度。若实际字符数不足默认右对齐并用空格填充可用0标志补零。.precision对于整数是最少数字位数对于浮点数是小数点后位数对于字符串是最大字符数。lengthh、l、ll、L等修饰符用于指定整数或浮点数的长度如%hd短整型、%ld长整型、%lld长长整型、%Lf长双精度。specifier最终的转换字符。下面用几个例子直观展示效果printf([%5d]\n, 42); // [ 42]宽度5右对齐 printf([%-5d]\n, 42); // [42 ]宽度5左对齐 printf([%05d]\n, 42); // [00042]宽度5补零 printf([%d]\n, 42); // [42]强制显示正号 printf([%.3f]\n, 3.14159); // [3.142]保留3位小数四舍五入 printf([%.2s]\n, hello); // [he]最多输出2个字符 printf([%#x]\n, 255); // [0xff]带0x前缀的十六进制 printf([%8.3f]\n, 3.14159); // [ 3.142]宽度8小数点后3位我在实际调试中经常用这种格式化输出打印结构体表格配合%-20s对齐字段名称、%8.2f对齐数值列一眼就能对齐数据。这类控制符在写报表、日志、辅助测试工具时非常实用。3.2*修饰符的妙用宽度和精度除了写在格式字符串里还可以用*占位从参数列表中动态传入。这个技巧在做日志系统时特别有用因为日志的缩进层级是运行时决定的不可能写死在格式字符串里。int width 10; int precision 3; printf([%*.*f]\n, width, precision, 3.14159); // 等价于 printf([%10.3f]\n, 3.14159);*对应width第二个*对应precision它们各自从可变参数中按int类型提取值。注意顺序是先宽度后精度再后面才是要输出的数据本身。3.3 printf的返回值与副作用求值printf返回一个int值表示成功输出的字符总数。如果发生输出错误返回负数。这个返回值在日常开发中经常被忽略但在日志系统和串口通信中可以用于判断是否写入成功。还有一个高频面试题printf(%d %d\n, n, n);输出什么答案是不确定。C语言标准没有规定函数实参的求值顺序不同编译器、不同优化级别可能产生不同结果。有些编译器先算左边的n有些先算右边的n甚至可能交错执行。标准把这个称为未指定行为unspecified behavior。所以任何依赖函数实参求值顺序的代码都是不可移植的。如果真要分析这类题目换一种等价写法就清晰了int a n; // 先用后加 int b n; // 先加后用 printf(%d %d\n, a, b);在笔试中遇到printf里面夹带、--、函数调用的题目请直接把这类写法视为“不该出现在合格代码里”的坏味道。3.4 printf中文乱码的根因与修复中文乱码是搜索热词中反复出现的问题原因归根结底是编码不匹配。程序里的字符串常量在源码文件中以某种编码保存常见为UTF-8或GBK而终端或控制台按另一种编码解释显示。在Windows下的Visual Studio和旧版控制台默认使用GBK/936代码页而现代源码文件很多采用UTF-8。两者对同一个字节序列的解释不同就会出现“锟斤拷”、“烫烫烫”等经典乱码。常见修复手段Windows控制台执行chcp 65001切换到UTF-8代码页再运行程序。Visual Studio中把源码文件保存为“带有BOM的UTF-8”。在代码开头调用Windows APISetConsoleOutputCP(CP_UTF8)需要windows.h。Linux/macOS系统默认UTF-8一般不需要额外处理如果确实出现乱码检查终端字符集和源码编码是否一致。这里要强调printf本身不关心编码它只是忠实地把字节写到标准输出。乱码问题是字符编码环境的锅不是printf的锅。4. scanf深度解析读取机制与缓冲区陷阱4.1 scanf的工作原理空白、匹配与失败scanf从标准输入流中读取数据按照格式字符串解析。它的工作方式是依次读取格式字符串中的每个转换说明。对于大多数转换说明如%d、%f、%s先跳过所有空白字符空格、\t、\n再尝试匹配一个合法的数据项。匹配成功则把数据存储到对应的指针参数指向的地址匹配失败则立即返回不再处理后续格式说明。返回成功赋值并读取的输入项数量。例如int a, b; int ret scanf(%d %d, a, b);输入1 2返回值是2。输入1 x第二次匹配%d失败返回值是1b保持原值。这个返回值是判断读取是否成功的核心依据但大部分初学者都忽视了它。4.2 缓冲区残留问题%c与%d的混用这是新手最容易踩的坑之一。考虑以下代码int age; char gender; printf(请输入年龄); scanf(%d, age); printf(请输入性别); scanf(%c, gender);程序运行后第二个scanf往往不等你输入就直接返回了。原因是第一个scanf(%d)读取age时输入的换行符\n仍留在输入缓冲区中。第二个scanf(%c)读到缓冲区中第一个字符就是\n所以gender直接被赋值为换行符。解决方案有几种在%c前面加一个空格scanf( %c, gender);这样scanf会先跳过空白字符再读取可见字符。在%d后面加上getchar()手动吞掉换行符。统一使用fgets读取整行再用sscanf解析。这是最稳健的方式稍后详细说明。另外scanf(%d, age)还有一个常见的失败场景如果用户输入的是字母而不是数字scanf会返回0age不变而且那个字母还在缓冲区里下一次scanf又会尝试读取同一个字母形成死循环。解决思路是每次scanf返回不为1时用while(getchar() ! \n);清空缓冲区再重新提示用户输入。4.3 用%[^\n]实现整行读取scanf支持扫描集scanset%[abc]、%[^abc]、%[^\n]这类特殊格式。%[^\n]表示读取除换行符以外的所有字符直到遇到换行符为止。这可以当作一种简易的fgets替代方案但同样需要指定最大宽度防止溢出char line[100]; scanf(%99[^\n], line); getchar(); // 手动消费掉残留的换行有个细节值得注意%[^\n]一旦输入流的第一个字符就是\n匹配立即失败返回0line不会被赋值。所以它并不适合直接代替fgets。更稳妥的组合是getchar()逐个读取或者直接使用fgets。4.4 fgets sscanf更安全的输入方案对于不确定用户会输入什么的场景我推荐统一采用“整行读取 字符串解析”的模式char line[128]; int age; double score; if (fgets(line, sizeof(line), stdin) NULL) { // 读取失败通常是EOF return; } if (sscanf(line, %d %lf, age, score) 2) { // 解析成功 } else { // 解析失败可以重新提示 }fgets会读取包括换行符在内的整行并自动追加\0。只要缓冲区足够大就不会有残留问题。如果一行长度超过了缓冲区大小fgets会先读取缓冲区大小-1个字符剩下的留在输入流中需要额外判断末尾是否是换行符如果不是说明这一行还没读完。这种方案的好处是输入和解析分离行为可预期不会因为缓冲区残留而互相干扰。很多生产级命令行工具都采用类似思路。5. printf重定向与嵌入式场景实战5.1 什么是printf重定向为什么需要它printf默认把输出写到标准输出流stdout。在裸机嵌入式开发中stdout往往没有对应的“屏幕”而开发板上有串口、LCD、OLED等显示设备。要让printf能够把格式化文本发到这些设备上就需要重定向redirect。重定向的本质是替换底层负责输出单个字符或字符串的“钩子”。在STM32等ARM Cortex-M平台上通常需要重写fputcC标准库向标准输出写单个字符时调用的回调函数或_writeARM C库的底层写函数。int fputc(int ch, FILE *f) { // 把ch通过串口发送出去 HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; }重写fputc之后printf内部逐个字符调用fputc最终所有输出都会从串口发出。同理重定向scanf需要重写fgetc或_read。5.2 Keil MDK与GCC环境下的重定向差异在Keil MDK中使用MicroLIB时重定向fputc通常就可以了。如果用的是标准C库有时候还需要处理半主机模式semihosting的问题——默认情况下printf会尝试通过仿真器把输出转交到主机调试器这在没有调试器的嵌入式环境下会导致程序卡死。解决方式是关闭半主机或者重新实现fputc并禁用半主机相关代码。在GCC工具链如arm-none-eabi-gcc newlib下需要重写的是_writeint _write(int file, char *ptr, int len) { for (int i 0; i len; i) { HAL_UART_Transmit(huart1, (uint8_t *)ptr[i], 1, HAL_MAX_DELAY); } return len; }_write接收缓冲区指针和长度一次性把整个缓冲区的内容通过串口发送出去效率比逐个调用fputc要高。如果使用printfnewlib可以考虑将_write和fputc同时实现确保不同版本的C库都能正常工作。5.3 用vfprintf实现更灵活的重定向如果你需要把格式化输出同时发送到串口、日志文件、网络调试助手等多个目标可以考虑直接用vfprintf。vfprintf接受一个FILE*参数我们只需要构造一个自定义的FILE结构并绑定自己的读写回调就可以在任意目标上使用fprintf/vfprintf的格式化能力。这种方法比重定向fputc更灵活因为重定向只影响全局的stdout而vfprintf可以创建多条“定制输出流”。不过裸机环境下的FILE结构在C标准库中并不总是易于定制尤其在Keil MDK和newlib中需要查阅对应运行时的文档。5.4 printf与fprintf、sprintf、snprintf之间的分工在需要进行格式化输出的开发场景并非所有情况都适合直接使用printf。printf族、fprintf族、sprintf族标准库函数定位不同函数输出目标典型用途printf(format, ...)stdout标准输出屏幕、终端、经过重定向后的串口fprintf(stream, format, ...)指定文件流写入日志文件、自定义输出流sprintf(buf, format, ...)内存中的字符数组把格式化结果存入字符串后续再做处理snprintf(buf, size, format, ...)内存中的字符数组限长安全地构造字符串防溢出fscanf(stream, format, ...)从文件流读取格式化数据读取配置文件、数据文件sscanf(buf, format, ...)从内存字符串读取解析已读入的文本snprintf在构造日志字符串时几乎是必选项因为它的size参数可以防止缓冲区溢出。sprintf因为没有长度限制一旦格式化结果超过缓冲区大小就是经典的缓冲区溢出漏洞——在网络安全领域这属于高危缺陷应该避免使用。这里给一个日志系统常用的使用模式char logbuf[128]; snprintf(logbuf, sizeof(logbuf), [%s] code%d, temp%.2f, level_str, error_code, temperature); my_log_write(logbuf);先把日志格式化到内存字符串再统一走日志写入函数这样可以把格式化逻辑和传输逻辑彻底解耦。6. 高频笔试题与实战排查技巧6.1 先看几道高频笔试题在网上搜“C语言笔试题”scanf和printf相关的题目数量惊人而且变体多。我把最常考的几个类型整理出来并解释原理。题目1printf(%d, n);输出什么已知int n 5;printf(%d, n);输出5因为n是后置自增表达式值是自增前的旧值5副作用是n变为6。而printf(%d, n);输出6表达式值是自增后的新值。题目2判断int和unsigned int混用输出。int a -1; unsigned int b 1; printf(%d\n, a b); // 输出0原因在于C语言的隐式类型转换规则当int和unsigned int比较时int会转换为unsigned int。-1转换成unsigned int是一个很大的数4294967295必然大于1所以a b为假即0。这一类“符号数与无符号数比较”的坑在笔试和实际开发中都是重点。题目3printf求值顺序与副作用。int i 3; printf(%d %d\n, i, i);这是未指定行为。C标准不保证参数的求值顺序所以可能输出4 3也可能输出3 3甚至其他结果取决于编译器。最佳答案是“结果未定义/不可移植”。类似的还有printf(%d %d\n, i, i);这种组合。题目4scanf返回值的利用。int num; if (scanf(%d, num) ! 1) { // 处理非法输入 }这种题目考察的是对返回值的理解。scanf返回成功转换并赋值的输入项个数用这个数目做分支控制是健壮代码的标配。6.2 实际开发中的中文乱码与编码排查在Windows下写完printf(你好);出现乱码排查步骤可以按以下顺序确认源码文件编码用VS Code/Notepad查看文件编码是UTF-8还是GBK。确认编译器如何读取源码GCC默认按UTF-8解析源码MSVC旧版按系统代码页新版配合/utf-8选项可以统一。确认终端代码页chcp查看当前活动代码页chcp 65001切到UTF-8chcp 936切到GBK。确认运行时编码Windows下可使用SetConsoleOutputCP系列API。我个人的经验是统一使用UTF-8编码保存源码在程序入口设置控制台代码页为UTF-8或者干脆在Windows下使用VS Code 终端配合能减少大多数中文乱码问题。6.3 常见编译告警unreferenced label搜索热词里有一条“c语言编程编译后出现unreferenced label 后怎么改”。这个告警通常不是printf/scanf直接引起的而是在分支或跳转逻辑中出现没有使用到的标签label常见于goto语句调试后忘记删除的情况。解决方式是检查代码中的label:行删除未被goto引用的标签。如果编译器同时报-Wunused-label这个警告本质上是一个代码清理提示不影响运行但会破坏编译输出的整洁度。6.4 速查表常见问题与排查方向现象根因解决方向printf输出的浮点数一直是0.000000格式符用了%d或变量类型不匹配检查格式说明符double用%fscanf读取字符时跳过不等待用户输入缓冲区残留换行符在%c前加空格或使用getchar()清空输入数字后按回车程序直接结束scanf返回值未检查且缓冲区残留检查返回值必要时用while(getchar()!\n);清空printf输出中文乱码源码编码/终端代码页/编译器解析不一致统一UTF-8chcp 65001检查源码保存格式printf输出负数显示为超大正数有符号数被无符号格式解释检查%d/%u是否匹配变量类型scanf(%s)输入过长导致崩溃缓冲区溢出使用宽度限制%99s或改用fgetsprintf在STM32上卡死半主机模式未关闭重定向fputc/_write关闭半主机7. printf重定向的更多落地细节7.1 重定向到串口的完整示例STM32 HAL很多嵌入式初学者在配置好串口之后直接调用printf结果发现串口助手什么也不显示。问题往往在于没有重定向底层输出函数。用STM32CubeIDE HAL库时常见做法是#include stdio.h int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; } int fputc(int ch, FILE *f) { return __io_putchar(ch); }有的工程还需要添加int _write(int file, char *ptr, int len) { for (int i 0; i len; i) { __io_putchar(*ptr); } return len; }7.2 重定向到LCD或OLED显示屏如果想在OLED屏幕上看到printf格式化的字符思路是准备一个支持显示ASCII字模的底层函数例如OLED_ShowChar(uint8_t x, uint8_t y, char ch)。维护一个虚拟的光标位置在fputc中遇到\n时换行遇到\r时回到行首其余字符调用OLED_ShowChar逐个显示。然后把fputc重定向到这个函数。这种方法的核心是让printf的普通字符与转义字符如\n都能被底层正确翻译成屏幕上的像素移动。7.3 重定向到日志文件的毫秒级时间戳打印在稍复杂一点的MCU日志系统中经常需要打印毫秒级时间戳uint32_t tick HAL_GetTick(); printf([%lu.%03lu] , tick / 1000, tick % 1000);这里用%lu搭配unsigned long是稳妥的写法。如果HAL_GetTick()返回uint32_t而平台unsigned long是32位那么%lu可正常工作。若想严格匹配可以强转成unsigned long再打印。7.4 关于keil5 _file_只显示相对路径的延伸搜索热词里出现“keil5fileprintf只显示相对路径”这其实是很多嵌入式开发者在调试日志时遇到的问题。__FILE__是编译器的预定义宏展开为当前源文件的路径字符串。在Keil MDK中如果编译时传入的是相对路径__FILE__就会显示相对路径如果传入的是绝对路径则显示绝对路径。日志系统里常常这样用printf([%s:%d] error code %d\n, __FILE__, __LINE__, err);如果你希望日志里显示的是简短文件名而不是冗长路径可以在构建系统中把编译器的当前目录切到源码根目录或用字符串处理函数提取__FILE__中最后一个/之后的部分。Keil MDK中的具体控制在项目的C/CAC6选项卡或分散加载文件中调整并不复杂但容易绕进去。8. 字符串处理函数与格式化输入的配合8.1 sprintf与snprintf的工程化建议snprintf的函数原型是int snprintf(char *str, size_t size, const char *format, ...);返回值为“如果缓冲区足够大本应写入的字符总数”。这意味着即使返回值为负数或大于等于size也说明输出被截断了需要根据返回值判断是否发生了内容丢失。char buf[32]; int n snprintf(buf, sizeof(buf), %d:%d, 123456, 789012); if (n 0) { // 编码错误 } else if ((size_t)n sizeof(buf)) { // 输出被截断 }这种检查在CVE级别安全要求较高的代码中非常常见。在C语言中sprintf存在缓冲区溢出风险是各类安全扫描工具的重点告警项。无论是普通后台程序还是MCU固件我都建议直接使用snprintf。8.2 用sscanf解析结构化文本sscanf能从内存字符串中按格式读取数据非常适合解析日志行、配置行、通信协议帧。例如char line[] name:alice age:18 score:95.5; char name[32] {0}; int age; float score; int n sscanf(line, name:%31s age:%d score:%f, name, age, score);sscanf同样存在缓冲区溢出的风险所以%31s中的宽度限制必不可少。我习惯在解析外部输入之前先用fgets把整行读入再用sscanf提取字段。这样做的好处是如果解析失败原始输入还可以用来打印调试信息不会像scanf直接读流那样丢失现场。8.3 处理%%与动态格式串动态构造格式字符串本身要小心。如果格式字符串来自外部用户输入直接传给printf会引出一个经典漏洞格式字符串漏洞format string vulnerability。攻击者可以通过%x、%n等格式符读取甚至改写内存。正确做法是永远不要把用户输入作为格式串printf(user_input); // 危险用户输入被当作格式串 printf(%s, user_input); // 安全把用户输入当作字符串数据这一点在面试中偶尔会作为安全题出现在实际开发中更是红线。9. 一个综合小项目命令行成绩统计工具为了把前面的知识点串起来我做一个极简的“命令行成绩统计工具”作为实战演示。它读取用户输入的若干组成绩计算总分、平均分、最高分并格式化打印输出。重点是用到了scanf的返回值检查、fgetssscanf安全解析、printf的格式化控制。#include stdio.h #define MAX_NUM 100 int main(void) { int scores[MAX_NUM]; int count 0; char line[64]; printf(请输入成绩输入end结束\n); while (count MAX_NUM) { if (fgets(line, sizeof(line), stdin) NULL) { break; } if (line[0] e || line[0] E) { break; } int val; if (sscanf(line, %d, val) 1) { scores[count] val; } else { printf(无效输入请重新输入一个整数或输入end结束。\n); } } if (count 0) { printf(没有有效成绩。\n); return 0; } int sum 0; int max scores[0]; int min scores[0]; for (int i 0; i count; i) { sum scores[i]; if (scores[i] max) max scores[i]; if (scores[i] min) min scores[i]; } printf( 成绩统计 \n); printf(有效成绩个数%d\n, count); printf(总分%d\n, sum); printf(平均分%.2f\n, (double)sum / count); printf(最高分%d\n, max); printf(最低分%d\n, min); printf(\n); return 0; }这个程序里至少用到了五个讲解过的知识点fgets读取整行避免scanf残留问题。sscanf解析配合返回值判断输入是否为整数。判断line[0]是否为e来支持输入end结束同时保留了else分支处理非法输入。printf的格式化输出%.2f控制平均分小数位表格线用普通字符对齐。(double)sum / count强制转换让整数除法变成浮点除法避免结果为0。我把这个程序在Linux终端和Windows控制台下都跑过效果稳定。读者可以在此基础上扩展功能比如增加按分数段统计人数、支持从文件读取成绩等顺带练习fscanf与fprintf。10. 我的几点实战总结与避坑心得到这里scanf和printf的核心知识点基本覆盖全了。最后分享几条最想提醒读者的经验。第一一定要检查scanf的返回值。很多人写代码时完全不看返回值用户输入非法数据时程序行为完全不可控。任何涉及用户输入的程序不检查返回值就等于把程序的稳定性交给运气。第二能用fgetssscanf就不要直接用scanf读取复杂输入。scanf适合读取高度结构化、格式可靠的数据流而面对人机交互的随意输入fgets整行读取再解析几乎可以解决所有“缓冲区残留”和“非法输入”的难题。代价只是多写一行代码收益却非常可观。第三printf不是做日志的终点而是日志格式化的起点。在大型项目中日志通常需要被采集、过滤、分级。直接printf输出虽然方便但很难统一控制。更工程化的做法是封装一个日志模块内部用snprintf先构造完整日志行再由统一的写日志接口落盘或发送这样后续要加时间戳、日志级别、模块名、文件行号都比较方便。第四编译器的格式检查选项一定要开。GCC/Clang的-Wall和-Wextra默认就能检测大部分printf/scanf格式不匹配问题。把这些警告当作错误处理可以避免很多低级bug。第五遇到乱码先检查编码环境不要急着改业务代码。其实很多乱码问题在源码文件编码、编译器编码、终端代码页三者统一之后就消失了。与其在代码里做各种编码转换不如先把开发环境的编码链路理顺。最后再分享一个小细节在Windows的Visual Studio中调试C语言项目时程序刚运行完控制台窗口就关闭了很多人以为是程序崩了。解决方法是程序末尾加getchar();或者在调试器里设置“调试结束后暂停”。这虽然和scanf/printf没有直接关系但确实是我当年初学时被卡住很久的坑。希望这篇关于scanf与printf的实践总结能帮你少走一些弯路。如果读完能让你在笔试、作业、嵌入式调试中更快定位问题那这篇文章就没有白写。