写代码这么多年,我经常跟刚入门的朋友说:scanf和printf这对输入输出函数,看起来简单,其实是新手最容易翻车的地方。很多人照着书上的例子敲了一遍,能跑通就以为自己会了,结果一旦开始写真正有点逻辑的程序,就被各种莫名其妙的输入问题搞得焦头烂额。比如 scanf 隔一次才生效、输入数字之后字符串被跳过、缓冲区里残留的回车把下一次读取搞挂。这些问题的根源,往往就在于没有真正理解这两个函数的底层工作方式。这篇文章就把我这些年处理输入输出踩过的坑、总结出来的经验系统性梳理一遍,希望能帮你把这对"入门函数"彻底吃透。
1. 被低估的两个函数:scanf和printf到底在做什么
1.1 格式化输入输出的本质:数据的双向翻译
很多人把 printf 理解为"打印东西",把 scanf 理解为"读东西",这么理解当然没错,但不准确。准确地说,printf和scanf是一对格式化翻译器:它们在程序内部的数据表示(二进制整数、浮点数、字符等)和人类可读的文本形式之间做双向转换。
printf 做的是"内存数据 → 格式化文本"的翻译。比如printf("%d", 42),本质是把内存中以补码形式存储的整数 42 转换成字符 '4' 和 '2',然后写入标准输出流。反过来,scanf 做的是"格式化文本 → 内存数据"的翻译。scanf("%d", &num),是从输入流中读一个字符 '4'、一个字符 '2',把它们转换成二进制补码 42,再写入 num 变量对应的内存地址。
有些人可能会觉得这个区别无关紧要,但恰恰相反,它解释了很多现象。比如说,为什么 scanf 需要传变量的地址(&num),而不是变量本身?因为 scanf 要把读取到的值写入调用者作用域里的变量,C语言是值传递,如果不传地址,函数内部改的只是形参副本,根本回不来。这跟 printf 截然不同,printf 只需要值,不需要写回。关于地址传递这个机制,我见过大量新手在这里栽跟头,写着scanf("%d", num)忘了取地址符,程序多半直接崩溃或行为诡异,因为传给 scanf 的是一个垃圾地址。
1.2 这两个函数出现的历史背景与核心定位
scanf 和 printf 诞生于上世纪 70 年代的 C 语言早期实现里,它们的设计目标非常明确:在资源极其有限的硬件条件下,提供一种极简、灵活、不依赖运行时额外配置的输入输出方式。这也决定了它们后续几十年的基调:功能强大但接口原始,极度灵活但安全性要靠使用者自己负责。
理解这一点很重要。因为现在很多教程会告诉你"scanf 有安全隐患,别用",这话只说对了一半。你需要意识到,C 标准本身只定义了函数行为,而编译器、运行库实现、终端设备的差异,会让同一种写法在不同平台上表现不同。比如某些库实现的 scanf 对浮点数格式的支持细节就不完全一致,Windows 上和 Linux 上的控制台缓冲区刷新策略也有差别。你只有理解了它的设计哲学——简单、直接、把控制权交给你——才能从容应对这些差异,而不是被某个环境下的特殊表现吓到。
2. printf格式输出:格式控制符里的门道,比你想象的多
2.1 格式控制符全解:占位符的本质是"模板"
printf 的第一个参数是格式字符串,本质上是一个带有占位符的普通文本模板。遇到%开头的位置就按规则进行替换,普通字符则原样输出。核心格式如下:
printf("%[标志][宽度][.精度][长度修饰符]转换字符", 参数);大多数人只用过%d%f%s这几个,实际上每个部分都有讲究。
先看转换字符,它决定了参数被解释成什么类型:
| 转换字符 | 含义 | 典型参数类型 |
|---|---|---|
| d / i | 有符号十进制整数 | int |
| u | 无符号十进制整数 | unsigned int |
| o | 无符号八进制(不含前导0) | unsigned int |
| x / X | 无符号十六进制(小写/大写) | unsigned int |
| f / F | 十进制浮点数 | double(float会自动提升) |
| e / E | 科学计数法浮点数 | double |
| g / G | 自动选择f或e中较短者 | double |
| c | 单个字符 | int(字符通过int传入) |
| s | 字符串 | const char *(指向字符数组) |
| p | 指针地址 | void * |
| % | 输出字面量% | 无参数 |
这里有个极易踩的坑:printf 中 float 会被隐式转换为 double 再传入,但这是编译器在调用约定层面的处理,不会因为格式写错就帮你纠正。如果你用%f去输出一个 double,没问题;但如果你用%d去输出一个 double,行为就是未定义的,可能打印出完全没意义的大数字。
宽度和精度是格式化输出的两个重头戏。宽度指最小输出宽度,如果内容不够宽,用空格(或标志指定的字符)补齐,超过宽度则原样输出。精度对浮点数来说是小数位数,对字符串来说是最大输出字符数。
// 宽度和精度示例 printf("%8.2f\n", 3.14159); // 输出 " 3.14",总宽8,小数2位 printf("%-8.2f|\n", 3.14159); // 输出 "3.14 |",左对齐 printf("%08.2f\n", 3.14159); // 输出 "00003.14",补零 printf("%.3s\n", "hello"); // 输出 "hel",字符串截断2.2 容易被忽视的printf特性:返回值、动态宽度和潜在的格式串漏洞
printf 的返回值很多人用了十年都没用过一次。它返回的是实际写入的字符总数,如果发生输出错误就返回负数。这个返回值在某些场景非常有用,比如生成格式化字符串后需要知道其长度来分配缓冲区,或检测输出设备错误。
int len = snprintf(buf, sizeof(buf), "%d-%d-%d", year, month, day); // len 是被写入的字符数(不含结尾'\0')动态宽度和精度是另一个被低估的功能。格式字符串里可以用*代替宽度或精度,实际值从参数列表中按顺序取:
printf("%*d\n", width, value); // 宽度由 width 变量决定 printf("%.*f\n", precision, value); // 精度 由 precision 变量决定这在需要动态控制对齐和显示精度时非常实用,比如你要做表格输出,列宽要根据数据最大值动态计算,动态宽度特性就能派上用场。
还有个不得不提的安全问题:不要把用户输入直接作为格式字符串。我之前见过有人图省事写printf(str)而不是printf("%s", str),如果 str 里恰好包含%d%s这类占位符,printf 会把它当格式串解析,把栈上随机值打印出来,甚至可能导致程序崩溃或安全漏洞(著名的格式串漏洞就源于此)。任何时候,用户可控的内容只能作为参数传入,绝不能作为格式串本体。
3. scanf的读取机制:深入理解后才能绕开那些坑
3.1 空白字符处理策略:scanf到底如何决定取多少个字符
scanf 的读取策略可以概括为三条规则:
- 跳过前导空白:除了
%c、%[]、%n这三个特殊格式,其他所有格式在读取前都会跳过连续的空格、制表符和换行符。 - 按格式匹配读取:每个格式说明指定了读取的最大长度与方式。
- 读到不匹配字符即停:比如
%d读整数时遇到非数字字符就停止,而且不会消费这个字符,它留在输入流里。
这三条规则解释了大量"灵异现象"。比如:
int num; scanf("%d", &num);你输入123abc,scanf 能读到 123,返回 1,而abc三个字符会留在缓冲区内。下一次任何读取操作都可能撞上这残留的内容。
再看%s的细节。%s读取时跳过前导空白,然后持续读取非空白字符,直到遇到空白结束,并在末尾自动补'\0'。所以scanf("%s", buf)天然无法读入包含空格的一整个句子,它最多读到第一个空格前。如果你需要读"Hello World"这样带空格的整行内容,%s单选型根本做不到。
而%c则是另一条轨道:它不跳过前导空白,直接读取一个字符。这就是为什么很多人写完scanf("%d", &n)之后再写scanf("%c", &ch)会发现 ch 的值是换行符——因为%d读完数字后,把紧跟其后的回车符留在了缓冲区内,而%c会直接把这个回车符读走。
%[]字符集合格式也很冷门但很实用。它可以定义"只读这些字符"或"读到这些字符为止":
// 只读小写字母组成的字符串 char name[100]; scanf("%[a-z]", name); // 读到逗号为止(逗号不读入) scanf("%[^,]", name);这在一些结构化文本解析场景比%s好用得多。
3.2 返回值是进度条:scanf的返回值才是灵魂
scanf 的返回值代表成功赋值的输入项个数。这是它的灵魂所在,但被绝大多数人忽略了。
int a, b; int ret = scanf("%d %d", &a, &b);- 如果你输入
10 20,ret 是 2,两个变量都拿到了值。 - 输入
10 x,ret 是 1,a 拿到 10,b 没拿到值,x 留在缓冲区里。 - 输入
x 10,ret 是 0,两个变量都没拿到值,从 x 开始的内容都留在缓冲区。 - 如果到达文件末尾(比如从文件重定向输入),ret 是 EOF。
永远不要无视返回值。任何依赖 scanf 的输入逻辑,都应该先判断返回值是否符合预期,再使用变量内容。否则,你以为读到了 20,实际上变量 b 里是上一次调用留下的垃圾值,甚至根本没初始化,这类 bug 在编译器中往往不会报错,运行时也未必立即崩溃,纯粹是逻辑悄悄出错,等到你排查时才是最欲哭无泪的。
3.3 匹配失败的连锁反应与最小匹配原则
还有一种经常把新手搞懵的情形:当 scanf 的格式串包含多个转换说明时,它是逐个尝试匹配的,前面的成功了后面的失败,这并不影响前面成功的结果,但失败的转换会让未赋值的变量保持原状。像scanf("%d%d", &a, &b)第一次读到正确数字赋值给 a,第二次失败则 b 不会被赋值。这里第二个匹配失败后,那个不能作为数字的字符同样留在缓冲区里。
另一个概念是"最长匹配但最小必要":对于%d而言,它尽可能多地读取有效数字字符,但一旦遇到非法字符立即停止,不会回头匹配。比如123abc456,%d只会读到 123,后面的abc456全留在缓冲区,下一次%d也读不到东西,因为缓冲区开头是字母 a,不是数字,scanf 会立刻匹配失败返回,连 a 都不消费。这意味着如果你不清理缓冲区,后续的整数读取会一直失败。这个特性直接造成了很多程序里"输入一个错误之后,后面全都错了"的连锁反应。
4. 缓冲区残留与清空策略:解决80%的输入恶心场景
4.1 输入缓冲区的生命周期:为什么回车键阴魂不散
scanf 的输入来源通常是一个缓冲输入流。在行缓冲模式下,数据从终端进入程序是按下回车后才发生的。你输入42并回车,缓冲区里实际上有'4' '2' '\n'三个字符。%d读完4和2后停下来,'\n'留在缓冲区里。这是行缓冲模式的固有行为,不是 bug,但它造成的后果确实是 bug 级的。
最常见的例子:
int age; char name[32]; printf("请输入年龄:"); scanf("%d", &age); printf("请输入姓名:"); scanf("%s", name); // %s 会跳过前导空白,所以这里没坑%s自己会跳过前导空白,所以第二个读取能正常工作。但如果第二个是%c或%[^\n],问题就来了——回车符还在缓冲区里,%c不跳过空白,直接把回车读走了,你甚至没有机会键入姓名,程序就已经把回车当成输入了。
这就是为什么会有这样的经典操作:在%c前面加一个空格——scanf(" %c", &ch)。格式串里的空白字符在 scanf 中代表"跳过任意数量的空白字符(包括换行)",所以前面加空格可以抵消掉缓冲区中残留的回车。我强烈建议,凡是读取单个字符的地方,都养成在%c前加空格的习惯:
scanf(" %c", &cmd);4.2 清空缓冲区的几种做法和它们的局限性
网上流传的清空输入缓冲区的办法五花八门,但不少都埋着坑。
方案一:fflush(stdin)
这是流传最广也最错误的做法。C标准明确说fflush 只对输出流和更新流有定义,对输入流调用是未定义行为。很多Windows环境配合特定运行库能用,但在Linux/macOS的 glibc 下并没有效果,行为无定义。千万不要在自己的代码里去依赖这种未定义行为。
方案二:循环 getchar 读到换行
这是可移植的做法:
int c; while ((c = getchar()) != '\n' && c != EOF) ;这段代码会把缓冲区里残留的字符一直读到换行符(含换行符本身)或文件末尾为止。它不会像 fflush(stdin) 那样引发未定义行为,也是我本人实际项目中使用最多的清理方式。需要注意的是,执行这段代码后缓冲区里的残留就被清空了,下一次正常读取不受影响。
方案三:"%*c"吃掉一个字符
scanf("%*c"); // 丢弃一个字符*表示赋值抑制,意思是按格式读取但不对应任何变量,值被丢弃。这个方案的问题是它只丢掉一个字符,如果缓冲区残留不只一个字符(比如上次输入了一整串无效内容),清理不干净。适合用在明确知道缓冲区里只有一个回车残留的场景,%d之后往往就是这种情况。
4.3 用取词法解决"输入错误后程序崩溃"的经典问题
很多人遇到的问题是:用户输入了一个不是数字的内容,scanf 返回 0,程序没有对返回值做检查,继续往下走,后面逻辑全乱。或者更典型的,程序要求输入数字,用户手滑按了个字母,scanf 失败,字母留在缓冲区,下一次 scanf 继续失败——程序直接死循环。
正确的处理方式是用取词法模式:每次用 scanf 读取前,先丢掉缓冲区里可能存在的非法字符,然后尝试读取:
int read_number(int *out) { // 丢弃缓冲区中所有非数字和非负号的残留字符 int c; while ((c = getchar()) != EOF && !isdigit(c) && c != '-' && c != '+') ; if (c == EOF) { return 0; // 没读到任何东西 } ungetc(c, stdin); // 把读到的数字字符放回去,让 scanf 处理 return scanf("%d", out) == 1; }这种方法的思路不是"清空缓冲区"这种模糊操作,而是"丢弃确定无用的字符后再读取",行为完全可控。核心逻辑是:把当前位置之前不属于合法数字组成部分的字符全部消费掉,再把合法的首字符放回流中,让 scanf 完成后面的解析。配合返回值判断,这个读数字函数能稳定处理各种乱输入。
5. 更安全高效的输入替代方案:fgets + sscanf 组合
5.1 为什么fgets更稳:整行读取与边界控制
"读整行"是解决输入问题最根本的思路。与其让 scanf 在缓冲区里钻来钻去,不如先用 fgets 把一整行读进字符串,再用 sscanf 在字符串内做格式化解析。这样缓冲区永远保持干净,即使一行解析失败,影响的也只是这一行字符串,不会污染后续输入。
char line[256]; char name[64]; int age; if (fgets(line, sizeof(line), stdin) != NULL) { // 在字符串层面做解析 if (sscanf(line, "%63s %d", name, &age) == 2) { // 解析成功 } else { // 解析失败,处理这一行的错误,缓冲区不受影响 } }为什么 fgets 比 gets 安全?因为 fgets 需要提供缓冲区大小参数sizeof(line),最多读取sizeof(line) - 1个字符,并保证末尾加上'\0'。gets 没有大小限制,读多长的输入都往固定的缓冲区里写,一旦超出就是缓冲区溢出——这是经典的软件安全漏洞来源。
fgets 读取后,缓冲区里可能有也可能没有换行符:如果一行内容太长,超过缓冲区容量,fgets 只读前size-1个字符,整行没读完,后续剩余部分还在输入流里,而且读进来的字符串里没有换行符。这个时候你需要判断是否需要继续排空剩余内容。一个很有用的写法:
char line[256]; fgets(line, sizeof(line), stdin); // 去掉尾部换行符(如果存在) line[strcspn(line, "\n")] = '\0';strcspn返回第一次出现目标字符集的偏移位置,如果没有换行符,line[strcspn(line, "\n")]会定位到字符串尾的'\0',把'\0'覆盖成'\0',安全无害;如果存在换行,就把换行替换成'\0',一行文本就成了干净的 C 字符串。这是嵌入式场景和命令行工具场景里最常用的裁剪换行方式。
5.2 sscanf在字符串层解析:格式串保持一致
sscanf 的语法与 scanf 完全一致,只是第一个参数从"标准输入流"变成了"字符串"。对于 "10 abc" 这种混合类型的一行内容,sscanf 是最舒服的解析工具:
char *line = "42 3.14 hello"; int n; float f; char word[32]; int ret = sscanf(line, "%d %f %31s", &n, &f, word); // ret == 3 表示三个值都解析成功需要注意的是:sscanf 也会在字符串内部跳过空白(除%c和%[]),并使用同样的匹配失败规则。所以我在前面讲的 scanf 的那些匹配机制,在 sscanf 中同样适用,只是操作对象从流变成了内存字符串,不会再有缓冲区的残留问题。理解这一个共通的解析核心,就能同时掌握 scanf 和 sscanf。
还要警惕一种边界:sscanf 中的%s没有缓冲区大小限制的版本是%s本身,直接用sscanf(line, "%s", buf)会把一段超长文本写入 buf,同样可能溢出。要限制长度必须用%31s这种写法,31是最大读取字符数(不含结尾的'\0')。我在所有涉及字符串的 sscanf 用法里都强烈建议加长度限制符,不要偷懒。
5.3 用fgets + sscanf重构一个健壮的菜单交互程序
下面用一套完整的小案例来说明这种组合的实际价值。假设要写一个具有菜单功能的命令行程序:用户可以选择查询、添加、删除、退出等操作。传统 scanf 写法是这样的:
// 传统写法:容易出问题 int op; printf("请选择操作(1查询 2添加 3删除 0退出):"); scanf("%d", &op); // 如果用户输入了"abc",op 未赋值,缓冲区残留 abc,后续操作全乱改用 fgets + sscanf 的写法:
#include <stdio.h> #include <string.h> int menu_select(void) { char line[64]; int op = -1; while (1) { printf("请选择操作(1查询 2添加 3删除 0退出):"); fflush(stdout); // 确保提示信息立即显示 if (fgets(line, sizeof(line), stdin) == NULL) { return -1; // 输入流结束 } int ret = sscanf(line, "%d", &op); if (ret == 1 && op >= 0 && op <= 3) { return op; } printf("输入无效,请重新输入。\n"); } }这里的核心逻辑差别:不管用户输入什么,fgets 总是会读掉整行,程序的状态永远不会因为输入错误而卡住或污染。即使输了"abc",这一行文本已经被 fgets 完整消费掉了,下一次循环重新读一行,没有任何残留问题。fflush(stdout)是为了在交互场景下强制把提示信息刷新到终端,避免输出被缓冲不显示。
5.4 在真实项目中如何取舍:scanf、sscanf、fgets各自定位
很多人问:那到底还该不该用 scanf?我的看法是:分场景。
如果你的程序是算法竞赛或刷题,输入格式严格已知、数据干净可靠,直接用 scanf 完全没问题,它的性能在标准库实现中是经过优化的,比 fgets + sscanf 还要快一点。刷题场景追求的就是极致的读取效率和简洁性。
如果你的程序是命令行交互工具,面对的是真实用户,输入不可控,那就应该以 fgets 为主,配合 sscanf 做解析。用户在终端里敲什么都有可能,必须保证程序对任何输入都不会死循环、不会崩溃、不会产生不可预期的状态。
如果你的程序是从文件中读取结构化数据,比如 CSV、配置文件,我的建议更细分:格式非常规整的,scanf/fscanf 可以胜任;格式允许缺失、有多余空格、可选字段的,fgets 读行 + sscanf/手动解析更省心。
假如你要解析一个简单的配置文件,每行格式是key = value,其中 value 含空格,纯 scanf 就不好写(%s会被空格切断),fgets 读整行再手动拆分反而是最清晰的:
char line[256]; while (fgets(line, sizeof(line), fp) != NULL) { char key[64]; char value[192]; if (sscanf(line, "%63[^=] = %191[^\n]", key, value) == 2) { // 读取 key 到 '=' 为止,以及 '=' 后到换行为止的内容 // 可以处理 "username = root" 和 "username=root" } }%63[^=]这种格式表示"读取最多 63 个非等号字符",而%191[^\n]表示"读取最多 191 个非换行字符",这样才能在字符串层面把 value 中的空格保留下来。这些技巧综合运用,文件解析的覆盖面会非常广。
6. 从踩坑到理解:一个程序员对scanf/printf的最终心得
6.1 我再复盘几个高频翻车现场
把这些年我见过的最高频翻车现场集中整理成一张表,方便你对号入座:
| 翻车现象 | 根因 | 解决方案 |
|---|---|---|
用scanf("%d", num)忘记取地址 | scanf需要写回内存,必须传指针 | 写成&num |
%c读到回车而非想要的字符 | %c不跳过前导空白,残留回车被读走 | 格式串加空格:" %c" |
| 输入字符串读到空格就断了 | %s遇空白即停 | 用%[^\n]或 fgets |
| 输入错误后程序死循环 | scanf失败残留字符,后续读取一直失败 | 处理返回值 + 清空残留 |
用fflush(stdin)清输入缓冲区 | 标准库未定义行为,跨平台不可靠 | getchar 循环清空 |
| 把用户输入当printf格式串 | 格式串漏洞,可能被注入读取栈内容 | 一律用printf("%s", str) |
用%d打印 long 类型 | 格式符与实参类型不匹配 | 用%ld |
| 用 gets 接收输入 | 无边界检查,溢出风险极大 | 用 fgets 带缓冲区大小 |
这些都是"看起来简单,实际够坑"的典型案例。其中相当一部分问题的根源就是我在第3节讲的那几条 scanf 规则——跳过前导空白、读取到不匹配即停、不消费不匹配字符、返回成功赋值个数。把这四条规则刻进脑子里,很多问题就不需要死记答案,当场就能推断出原因。
6.2 调试输入输出问题的基本套路
我自己的排查套路通常是这样的。如果程序出现输入相关的诡异行为,第一件事不是改代码,而是确定当前缓冲区里到底有什么。怎么确定?写一个临时的小循环把缓冲区内容打印出来:
int c; while ((c = getchar()) != '\n' && c != EOF) { printf("[%d:%c] ", c, c); // 打印字符和它的十进制值 }这样你能立刻看清缓冲区中残留的到底是什么字符,比如回车是 '\n'(10),空格是 32,字母 'a' 是 97。看到的比猜的准确一万倍。
第二件事是确认 scanf 的返回值。任何一个你不确定是否读成功的 scanf,都把它返回值打出来看:
int ret = scanf("%d", &n); printf("scanf返回值:%d\n", ret);如果 ret 是 0,就是格式匹配失败,不要再去使用 n 的值;如果 ret 是 EOF,说明输入流已经到末尾,可能是文件重定向结束或发生了读取错误。
第三件事是排除编译器和库版本差异。同样的代码,在某些环境中行为不同,是常有的事情。遇到这种情况,不要怀疑自己"是不是写错了",先用最小复现代码定位到底是标准规定如此还是库实现差异。比如 fflush(stdin) 在 Windows 上的某些编译环境能工作,在 Linux 的 glibc 下却没有效果,就是典型的实现差异。
6.2.1 一段定位缓冲区残留的小工具
上面提到的方式可以直接做成一个工具函数,放进调试代码里:
void dump_stdin(void) { int c; fprintf(stderr, "[残留检查开始] "); while ((c = getchar()) != '\n' && c != EOF) { fprintf(stderr, "0x%02X(%c) ", c, (c >= 32 && c < 127) ? c : '?'); } fprintf(stderr, "[残留检查结束]\n"); }拿它去检查程序卡住的入口点附近,通常一执行,就能立刻看到缓冲区里残留了什么。执行完这段之后缓冲区是干净的(读到了换行或EOF),这就为后续测试提供了一个干净的起始状态。
6.3 超越scanf/printf:什么时候该转向更专业的输入输出
说完这些,最后聊聊另一个层面的思考。scanf 和 printf 是标准输入输出的缔造者,但它们不是终点。遇到以下情况时,我会主动转向更专业的方向:
- 处理Unicode多语言文本时,标准
%s和%c对宽字符的支持比较麻烦。Windows 下我一般考虑用专门的宽字符版本,Linux/macOS 则配合库函数处理UTF-8。 - 需要高速读取海量数据时,直接使用低层读取接口做自定义解析,比 scanf 逐项转换更可控。某些竞赛选手会使用自定义快速读入函数,就是为了绕过标准 iostream 和 scanf 的额外开销。
- 需要安全的国际化格式化时,我通常用可复用的格式化库来替代裸 printf 的直接拼装逻辑。不同平台的语言环境、代码页大小写规则、数字分隔格式差异,使得简单的 printf 在大型项目里维护成本直线上升。
但不管进阶到哪一步,scanf 和 printf 背后的那套核心思想——格式化数据描述、类型匹配、缓冲区的生命周期——是所有输入输出处理的通用基础。理解了它们,你理解其他语言里的格式化输出、序列化与反序列化、文本协议解析时,都能融会贯通。
我个人在带新人时的体会是:与其让他们死记一堆"scanf要加&"、"%c要加空格"之类的口诀,不如逼着他们真正把格式解析的机制、缓冲区的行为、返回值的含义搞明白一次。搞明白之后,哪怕换一种语言、换一套库,遇到输入输出问题也能很快定位到根本原因。这就是我写这篇文章的初衷,希望对你有同样的帮助。