☰
从 gets 到 fgets:C语言安全输入与缓冲区溢出防控
2026/10/1 5:56:03 网站建设 项目流程

前几天帮同事看一段从老系统里扒出来的 C 代码,编译的时候 GCC 直接甩了一句warning: the 'gets' function is dangerous and should not be used.,同事盯着屏幕问我:这函数到底干了什么,为什么连编译器都要当面骂它?这个问题其实很有代表性——gets和fgets这对函数,几乎每个学 C 的人都用过,但真正能把它们的行为边界、读到的字节数、缓冲区里到底剩下什么讲清楚的人并不多。很多线上事故、段错误、输入粘包、程序莫名跳飞,根子都在这一行看似简单的读取代码上。这篇内容我会按一个踩过坑的人的角度,把gets为什么会被标准除名、fgets每个参数背后的约定、换行符和截断怎么处理、缓冲区清理为什么必须用int、以及怎么封装出一个能直接进项目的安全读取函数,全部拆开讲一遍。适合刚写 C 不久想搞懂输入函数的同学,也适合已经在维护老代码、需要做安全整改的同学对照着看。

1. 从编译器的那句警告说起:gets 到底危险在哪

1.1 一次输入如何把返回地址改掉

先看一个最朴素的例子,也是当年教科书里反复出现的写法:

#include <stdio.h> int main(void) { char buf[8]; gets(buf); puts(buf); return 0; }

这段代码在编译时会有警告,但很多老编译器在宽松模式下还是会让你编过去。问题不在语法,而在于gets的签名:char *gets(char *s);。它只有一个参数,没有长度。也就是说,gets根本不知道buf有多大,它会一直从标准输入读字符,直到遇到换行符或者文件末尾,然后把中间的所有字符一个不落地写进buf指向的内存,最后补一个'\0'。

buf只有 8 个字节,你输入 7 个字符以内是安全的,输入 8 个、9 个、100 个字符呢?gets照样往后面写。写到哪里去了?在典型的栈帧布局里,局部数组buf位于栈的低地址,往高地址方向依次是编译器插入的对齐填充、保存的寄存器、保存的帧指针,再往上就是函数返回地址。你输入的超长字符串会一层一层把这些东西全部覆盖掉,包括那个返回地址。函数执行到return的时候,CPU 会从栈上取出被污染的返回地址并跳过去,程序要么直接段错误崩溃,要么执行到一段你完全无法预期的指令流。

这里最要命的不是"崩溃",崩溃至少还是可见的。真正的风险是:输入的内容能决定跳转目标,这意味着外部输入可以间接控制程序的执行走向。这类问题在安全领域有个非常经典的名字,叫栈溢出,或者更形象一点,叫"栈粉碎"(stack smashing)。现代编译器默认打开的-fstack-protector就是靠一个魔数(canary)来检测这种覆盖行为,一旦发现函数返回前 canary 被改动,就会打印*** stack smashing detected ***然后主动终止进程。但记住,栈保护是事后检测,是"发现被人打了就报警",不是"不让人打"。真正堵住漏洞的办法,是从一开始就不给越界写入的机会。

1.2 从弃用到除名:标准演进的时间线

很多人以为gets只是"不推荐使用",实际上它的处境比这严重得多。C99 标准里就已经把它列为过时特性(obsolescent),意思是标准委员会已经明确建议不要再用了,但为了兼容旧代码还留着。到了 C11,gets被正式从标准中移除——注意,是移除,不是标记弃用。也就是说,一份严格遵循 C11 的编译器完全可以不提供这个函数。C++ 那边动作也一致,std::gets在 C++14 中被删掉。

那你可能会问:为什么我在本地用 GCC 编gets还能编过?因为 glibc 为了兼容大量已经编译好的老二进制程序,仍然在动态库里保留了gets这个符号,但头文件<stdio.h>里已经不再声明它了。所以你直接写gets(buf)会报"隐式声明"的警告,链接的时候还会再给你一句"dangerous"。一旦你显式声明char *gets(char *s);强行绕过,链接是能过的,程序也能跑——但这相当于你亲手把安全护栏拆了。

提示:如果你的老项目里还有gets,整改方式不是在原地加个长度检查,而是整体替换成fgets或getline。因为gets的接口设计里压根没有长度这个信息,任何"检查"都只能是事后发现。

顺带说一句,scanf("%s", buf)其实和gets是一路货色——%s同样不限制宽度,遇到空白字符才停,同样可以溢出。区别只是它把换行符留在输入流里而已。所以看到scanf读字符串,警觉程度要跟看到gets一样高。

2. fgets 的三个参数,每一个都有约定

2.1 第二个参数到底传 sizeof(buf) 还是 sizeof(buf)-1

先看fgets的签名:char *fgets(char *s, int n, FILE *stream);。它做的事是:从stream读字符,最多读n - 1个,遇到换行符就停,最后在末尾补一个'\0'。这个"最多 n-1 个"的设计,就是要给结尾的空字符留位置。

于是引出一个几乎每次 review 都会看到的争论:

char buf[64]; fgets(buf, sizeof(buf), stdin); /* 写法 A */ fgets(buf, sizeof(buf) - 1, stdin); /* 写法 B */

正确写法是 A。因为fgets内部已经扣掉了那个给'\0'的位置,你传sizeof(buf),它能读到的最大字符数自然就是 63 个,刚好把数组占满又不会越界。写 B 的话,你实际最多只能读到 62 个字符,白白浪费了一个字节。这种行为跟snprintf是一致的——snprintf(buf, sizeof(buf), ...)也是传完整的sizeof,很多人在这里下意识地减一,纯属多余。

这个细节之所以值得单独拎出来说,是因为它反映了一个通用规律:凡是接口约定里明确说明了"会自己留终止符"的函数,长度参数都应该传缓冲区总大小。反过来说,像memcpy(dst, src, n)这种没有终止符概念的函数,n就是实打实的字节数,必须精确。把这两类函数混为一谈,就是各种 off-by-one 的来源。

2.2 动态缓冲区上的 sizeof:一个静默的经典 bug

比上面那个更隐蔽的是这种:

char *buf = malloc(1024); fgets(buf, sizeof(buf), stdin); /* 错! */

这行代码在很多项目里都出现过,编译不报错,运行大概率也不崩,但它是错的。因为buf是一个指针,sizeof(buf)得到的是指针本身的大小,在 64 位机器上是 8,在 32 位机器上是 4——跟malloc申请的那 1024 字节毫无关系。所以这个fgets实际最多只能读 7 个字符(64 位下),剩下的一律被截断丢弃。

麻烦的是它不会崩溃,不会报错,只是默默地少读数据。如果你的逻辑是"读一整行然后解析",输入稍微长一点就开始出现解析失败、字段错位,排查起来极其费劲,因为你会盯着解析代码看半天,压根想不到问题出在那行读取上。所以我在自己的代码里有个习惯:只要缓冲区是通过malloc或函数参数传进来的指针,长度一律用独立的变量显式传递,绝不在这一行用sizeof。

char *buf = malloc(1024); size_t cap = 1024; fgets(buf, (int)cap, stdin);

多写一个变量,能省掉一下午的调试。

2.3 返回 NULL 其实有两种含义

fgets在失败时返回NULL,但"失败"包含了两种情况:到达文件末尾和发生读取错误。光看返回值分不出来,必须配合feof()和ferror():

if (fgets(buf, sizeof(buf), fp) == NULL) { if (feof(fp)) /* 正常读到结尾 */ ; else if (ferror(fp)) /* 出错了,errno 里可能有线索 */ perror("fgets"); }

还有一个经常被误解的点:如果fgets在遇到文件末尾之前已经读到了至少一个字符,它返回的是非 NULL,而不是 NULL。也就是说,文件的最后一行如果没有以换行符结尾,fgets依然会把这行内容读进缓冲区并返回成功,只是缓冲区末尾没有'\n'。只有当你这次调用一个字符都没读到、就撞上了 EOF,才返回NULL。这个行为直接决定了"如何判断缓冲区里的内容是不是一行完整的行"——不能只看返回值,还得看末尾有没有换行符。

至于"一行的内容可能分多次读回来"这件事,我放到第 6 节展开,因为它和循环写法强绑定。

3. 换行符这件事,比大多数人想的麻烦

3.1 保留 '\n' 是设计,不是 bug

第一次用fgets的人几乎都会踩这个坑:printf("你输入的是:%s\n", buf);输出结果莫名其妙多了一个空行。原因很简单——fgets会把行尾的换行符一起读进缓冲区,它不丢弃。而gets是会丢弃换行符的,所以从gets迁移到fgets的人特别容易中招。

这个设计是有意为之的。因为fgets面向的是"任意字节流按行切分"的场景,保留换行符,调用方才能判断"这一行是不是真的到行尾了"。如果它悄悄把换行符吃掉,你就没法区分"我读到了完整的这一行"和"缓冲区满了被迫截断"。所以这不是缺陷,是它把信息交给你,由你来决定怎么处理。

处理方式里,我见过最啰嗦的写法是这样一个字符一个字符地找:

size_t len = strlen(buf); if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';

能跑,没毛病,但每次都要写四行,而且忘了判len > 0的时候,空行(只有一个'\n')会走到buf[-1],虽然大概率不会立刻崩,但这是实打实的越界写。我自己的习惯是直接用strcspn:

3.2 用 strcspn 一行完成行尾清理

buf[strcspn(buf, "\r\n")] = '\0';

这一行同时干掉了'\n'和'\r',而且不会越界。strcspn(s, reject)的语义是:返回s中第一个出现在reject集合里的字符的下标;如果整个字符串里一个都没有,就返回字符串的长度(也就是'\0'所在的下标)。所以当末尾没有换行时,表达式退化成buf[len] = '\0',等于把结尾的空字符又写了一遍,完全无害。这个技巧我用在很多项目里,包括解析配置文件的场景。

需要注意的是,如果一行里存在内嵌的'\r'(比如从 Windows 拷过来的文件),strcspn会从第一个\r开始截断。对"按行读取"这个场景来说这通常正是你想要的,但如果你的数据里合法地包含\r,就得换成别的策略。

3.3 Windows 文本模式下的 '\r\n'

跨平台代码里还有一个高频坑:同样的文件,在 Linux 上读出来干净利落,在 Windows 上用文本模式打开时,运行时库会把\r\n自动转换成一个\n,看不出问题;但如果你用"rb"二进制模式打开,\r\n会原样出现,fgets读回来的一行末尾就是\r\n,你用strcspn清掉之后没问题,但如果忘了清,后面做字符串比较、拼路径、写日志,就会多出一个不可见的\r,导致两个"看起来完全一样"的字符串比较结果不相等。这类问题排查起来极其痛苦,因为你在屏幕上根本看不到那个字符。

一个实用的排查手段是在调试打印时把不可见字符可视化出来,比如用%02x逐字节打印,或者干脆在比较前对两边都做一次行尾清理。我在跨平台项目里会强制规定:凡是fgets读回来的行,进入业务逻辑之前必须经过一次统一的 trim 处理,绝不把原始行直接往下传。这条规矩省掉了很多莫名其妙的字符串比较失败。

4. 清空输入缓冲区:getchar 循环为什么必须用 int

4.1 用 char 接收 getchar 返回值的隐患

聊完读取,绕不开"清空剩余输入"这个需求。最常见的写法是:

int c; while ((c = getchar()) != '\n' && c != EOF) ;

这里有一个被反复讲、但依然被反复写错的地方:变量c必须声明为int,不能是char。原因在于getchar的返回类型是int,它需要能表示所有可能的字符值(当作unsigned char解释)再加上一个额外的值EOF(通常是 -1)。如果你声明成char:

  • 在char默认为有符号的平台上(比如 x86 Linux),字符0xFF会被解释成 -1,和EOF撞车,循环会在遇到这个字节时提前结束,输入流里还剩一堆数据没清掉;
  • 在char默认为无符号的平台上(比如部分 ARM 工具链),char值域是 0 到 255,提升为int之后永远不可能是 -1,c != EOF恒为真,循环永远不会因 EOF 退出,撞上文件结尾就进入死循环。

两种情况都很糟糕:前者留下脏数据导致后续读取读到的不是你以为的内容,后者直接卡死。这个坑的源头其实是 C 语言的历史遗留——char的符号性是实现定义的。规避方式也很简单:凡是接收getchar、fgetc返回值的变量,一律用int。我甚至会在代码规范里直接写明这一条,因为它的出错概率实在太高了。

4.2 scanf("%*[^\n]") 这条路能不能走

另一种清空写法是用scanf的赋值抑制和扫描集:

scanf("%*[^\n]"); scanf("%*c");

第一行的意思是"匹配一串非换行字符但不要赋值给任何人"(*抑制赋值),把当前行剩下的部分吃掉;第二行吃掉那个换行符。思路是对的,但边界条件比较难受:

  • 如果流里的下一个字符已经就是换行符,%*[^\n]一个字符都匹配不到,scanf会因为这次匹配失败而提前返回,第二行才把换行符消费掉。这种情况逻辑上还能接受。
  • 如果流已经到了 EOF,第一个scanf返回EOF,第二个scanf会因为无法读取而直接返回EOF,不会像你期待的那样"什么也不做"。如果外层循环没有正确处理这个返回值,就可能出现死循环或者多消耗一次输入的情况。

更根本的问题是:需要"清空缓冲区"这件事,本身就是用scanf读字符串带来的副作用。用fgets读整行的话,一行要么读完,要么被明确截断并主动丢弃剩余部分,根本不存在"遗留半行脏数据"的状态。所以我在项目里的态度很直接:需要读字符串就用fgets,scanf只用来做纯数值解析,而且输入通常先经过fgets拿到一整行,再用sscanf去解析。这样从源头上就不需要清缓冲区的技巧了。

5. 封装一个能直接进项目的安全读取函数

5.1 先想清楚要区分几种结果

裸用fgets的问题在于,调用方需要自己处理"换行符在不在末尾""是不是被截断了""是不是 EOF"这些判断。写一次还好,写十次就开始有人偷懒漏判断。所以正确的做法是封装。但封装之前得先把返回值语义定清楚,我一般会区分四种结果:

返回值含义调用方该怎么处理
正数成功读到一行(可能为空行)正常解析,行尾已清理干净
0流已结束,且本次没读到任何字符退出循环
-1发生读取错误报错、记录 errno、决定是否重试
-2行太长被截断,剩余部分已丢弃视业务决定是报错还是接受截断

把"截断"单独列出来是很有必要的。很多业务的输入是有长度上限的(比如用户名、单行配置项),一旦出现截断,说明输入异常,这种异常如果被静默接受,后面会引发更难查的问题——比如某个本该 128 字节的字段被硬生生截成 64 字节,业务逻辑还以为是完整数据。

5.2 完整实现与逐行说明

下面这个版本我在好几个项目里用过,逻辑比较简单,直接可抄:

#include <stdio.h> #include <string.h> #include <limits.h> enum { READ_OK = 1, /* 成功读到一行(行尾换行符已被去掉) */ READ_EOF = 0, /* 流结束,本次未读到任何字符 */ READ_ERROR = -1, /* 读取出错 */ READ_TRUNCATED = -2 /* 行过长,已截断并丢弃剩余部分 */ }; /* * 从 stdin 读取一行到 buf(长度 size,含结尾 '\0')。 * 要求 size >= 2。 */ int read_line(char *buf, size_t size) { if (buf == NULL || size < 2) return READ_ERROR; /* fgets 的第二个参数是 int,必要时收窄,避免隐式截断 */ if (size > (size_t)INT_MAX) size = (size_t)INT_MAX; if (fgets(buf, (int)size, stdin) == NULL) { buf[0] = '\0'; if (ferror(stdin)) return READ_ERROR; return READ_EOF; /* feof 且一个字符都没读到 */ } size_t len = strlen(buf); /* 情况一:末尾是换行,说明这行完整读完了 */ if (len > 0 && buf[len - 1] == '\n') { buf[len - 1] = '\0'; return READ_OK; } /* 情况二:缓冲区没被填满,却也没读到换行 —— 文件最后一行 */ if (len < size - 1) return READ_OK; /* 情况三:缓冲区被填满,行还没结束,丢弃剩余部分 */ int c; while ((c = getchar()) != '\n' && c != EOF) ; return READ_TRUNCATED; }

几个关键点解释一下。size < 2直接判错,是因为fgets在n == 1时什么都不会读,只会往缓冲区写一个'\0',这种调用毫无意义,早点拦住比让它默默返回空串好。INT_MAX那次收窄是为了防止size_t到int的隐式转换截断——虽然现实中很少有人拿一个超过 20 亿字节的缓冲区调这个函数,但这类问题一旦出现就是静默的、无法排查的,加两行代码换掉这个风险很划算。

最容易被写错的是"情况三"的判定。有人会写成len == size - 1,这其实和len < size - 1互为补集,逻辑上等价,但用len < size - 1更直观:缓冲区还没满就结束了读取,那只可能是撞上了文件末尾或换行。真正需要警惕的是"最后一行恰好把缓冲区填满且没有换行符"这种极端情况,它会走进情况三,被误判为截断并且多读一次getchar。因为此刻流已经在 EOF,getchar立刻返回EOF,不会吃掉任何有效数据,所以后果仅仅是状态码被标成截断而已,数据本身是完整且正确的。这个取舍我认为可以接受,代码里用注释说明一下就够,不必为了这个边界把逻辑搞得更复杂。

5.3 拿几个用例把边界跑一遍

写完不测等于没写。这个函数的测试用例我一般会覆盖这几组:

  • 输入hello\n:返回READ_OK,buf是hello(无换行)。
  • 输入\n(空行):返回READ_OK,buf是空串,长度 0。这一组专门检验空行和 EOF 没有被混淆。
  • 输入之后立刻 EOF(比如< /dev/null):返回READ_EOF,buf是空串。
  • 输入恰好size - 1个字符加换行:应该返回READ_OK,因为换行符占掉了读取的最后一个位置之外的……等等,这里要小心。如果size是 8,输入1234567\n,fgets最多读 7 个字符,也就是1234567,换行符留在流里没被读走。此时len == 7 == size - 1,末尾不是换行,会走进情况三被误判为截断,并且把剩下的\n吃掉。所以这个用例的结果是READ_TRUNCATED,但缓冲区里的1234567是完整的。这是个已知的行为,用size时要清楚它意味着"单行最大可用字符数是 size-1",业务上如果要求单行能装下 7 个字符,缓冲区就得开 9 个。
  • 输入远长于缓冲区的行:返回READ_TRUNCATED,并且下一次调用read_line应该从新的一行开始,而不是接着读到上一行的残渣。这一条是验证"丢弃剩余部分"逻辑有没有生效的关键用例。

6. 按行扫描大文件:循环里那些容易忽略的细节

6.1 固定缓冲区 + 超长行的静默截断

处理日志、CSV、配置文件的标准套路是这样:

char line[4096]; while (fgets(line, sizeof(line), stdin) != NULL) { /* 处理这一行 */ }

问题在于:如果某一行超过了 4095 个字符,这个循环不会报错,它会默默地把这行拆成好几次迭代来处理。你的业务逻辑如果假设"每次迭代拿到的是一整行",那超长的那一行就会被当成好几行解析,比如 CSV 的字段被切断、JSON 的引号被切断,结果轻则解析失败,重则写进数据库的是残缺记录。

要判断有没有发生截断,可以在循环里加一个检查:

size_t len = strlen(line); if (len == sizeof(line) - 1 && line[len - 1] != '\n') { /* 这行还没读完,需要把剩余部分读完并丢弃,或者拼接 */ }

至于丢弃还是拼接,取决于业务。日志分析场景里,超长行通常是异常数据,直接丢弃并记录一条统计就够了;而配置解析场景里,超长行可能意味着配置项写得太长,应该直接报错让用户改。最不该做的就是不管不顾地继续往下走。

6.2 getline 的取舍

如果平台支持 POSIX.1-2008,getline是比fgets舒服得多的选择:

char *line = NULL; size_t cap = 0; ssize_t n; while ((n = getline(&line, &cap, stdin)) != -1) { if (n > 0 && line[n - 1] == '\n') line[--n] = '\0'; /* 处理 line */ } free(line);

它自动扩容,能读下任意长的行,还直接告诉你读到了多少字节(n),不用再strlen一遍。但它有两个代价:一是它不是 C 标准库的一部分,属于 POSIX,在纯 Windows 环境(比如 MSVC 默认配置)下没有;二是它内部realloc,内存由你负责free,忘记释放就是泄漏。我的经验是:跨平台库代码用fgets+ 长度管理,Linux 专属的工具程序用getline。混着用最容易出的问题是在 Windows 上编不过,然后有人在代码里加一堆条件编译,最后两边行为不一致。

getline还有一个细节值得记一下:它分配的内存会跨调用复用,循环里那句free(line)必须在循环外,放在循环里面会导致第二次调用时line变成野指针——getline会按照*lineptr和*n的当前值来判断是否需要在已有缓冲基础上扩展。

6.3 memchr 和 strlen 的性能差异

fgets之后马上strlen,实际上是对同一段数据扫了两遍:第一遍fgets内部逐字符找换行,第二遍strlen再逐字符找'\0'。对绝大多数场景这点开销无所谓,但如果你在处理几个 GB 的日志,这个差异会变得可观。优化的做法是直接用memchr在已知长度范围内找换行,但前提是你得知道实际读了多少字节——而fgets偏偏不告诉你。所以真到了这个量级,用getline(它返回长度)或者fread+ 自己切分反而是更合适的选择。

我用fgets的定位一直很明确:面向人的、行长度可控的输入场景,比如交互式命令行、配置文件、单条记录不超过几 KB 的文本协议。超出这个范围就该换工具,硬撑着用反而处处别扭。

7. 让 fgets 和 scanf 分工:一个更稳的组合拳

前面提到过,scanf直接读字符串和gets一样危险,而且失败时残留的输入极难清理。那是不是就不能用scanf了?也不是。我的做法是让两者分工:fgets负责把一整行安全地拿进来,sscanf负责在这一行内部做格式解析。

char line[128]; int a, b; char name[32]; while (read_line(line, sizeof(line)) == READ_OK) { if (sscanf(line, "%d %d %31s", &a, &b, name) == 3) { /* 三个字段都解析成功 */ } else { fprintf(stderr, "格式不对,跳过:%s\n", line); continue; } }

这个模式的好处有三个。第一,长度风险在fgets那一层就已经被彻底管住了,sscanf面对的是一个有限长度、以'\0'结尾的字符串,不会去读流。第二,解析失败时不需要清缓冲区,因为整行已经在内存里了,丢掉换成下一行就行。第三,错误处理变得非常清晰——sscanf返回成功赋值的字段个数,数量不对就是格式有问题,直接给出有意义的报错,而不是像scanf那样返回一个 0 让你猜是匹配失败还是输入结束。

对于%s,sscanf里还是要写宽度,比如%31s配合 32 字节的数组。这个宽度必须和数组大小对应,而且要在脑子里算准——%31s意味着最多写入 31 个字符加一个'\0',刚好塞满 32 字节。sscanf和scanf用的是同一套转换规则,所以这个宽度限制是有效的,别偷懒省掉。要解析更结构化的内容,比如带引号的字符串、固定分隔符的字段,那是另外的话题了,但"先fgets后解析"这个大框架是不变的。

8. 怎么确认自己真的写对了:手工输入 + 运行时检测

说了这么多,最后落到"怎么验证"。光靠看代码很难确认没有越界,我一般用两层手段。

第一层是手工构造边界输入。准备一个文件,里面依次放入:空行、刚好塞满缓冲区的行、比缓冲区长一倍的行、末尾不带换行的最后一行、包含\r\n的行。然后跑一遍你的读取函数,把每次调用的返回值和strlen(buf)都打出来对照。这个动作花不了十分钟,但能一次性暴露绝大部分行处理和截断判定的问题。特别是"末尾不带换行的最后一行",这个用例非常容易被忽视,而它恰恰是很多文件处理程序在最后一条记录上出错的原因。

第二层是打开运行时检测。用 GCC 或 Clang 编译时加上-fsanitize=address,undefined -g -O0,AddressSanitizer 会在越界读写发生的那一刻直接报错并打印完整的调用栈,而不用等到程序崩溃或者结果异常。对于检验fgets的长度参数、缓冲区清理逻辑是否越界,这东西非常管用。另外把-Wall -Wextra打开,编译器对fgets第二个参数类型不匹配、缓冲区可能不够大这类问题会给出提示,不要嫌警告多就关掉。

注意:-fsanitize版本只用于测试,不要带着它发布——它会显著增加内存占用和运行开销,而且引入额外的运行时依赖。

我自己在这上面栽过的最深的一次,是早期写一个配置解析器时用fgets读行,缓冲区开的是 256,注释里写着"配置项不会超过 200 字符"。结果有人写了一行超长的 key-value,被截成两半,后半段又恰好长得像另一条合法配置,程序安安静静地把一条错误的配置生效了。那次之后我的习惯就固定了:只要用到fgets,就必须显式判断截断,别让"长度足够"成为一种假设。长度这种东西,你以为的永远是"在你这台机器上、用你这份测试数据"的长度,换一个环境就不成立了。

还有一个我自己用着顺手的小经验:把read_line这类基础输入函数集中放在一个文件里,项目里所有地方都调它,不要在业务代码里散落fgets调用。散落之后,换行符处理有的一套写法一套,截断判定有的写有的不写,等出问题时你要一个个去翻。集中起来之后,规则是一处定义、全项目生效,改起来也就是改一个函数的事——这类看似不起眼的收敛,往往比多写几个高级特性更能决定一个 C 项目能维护多久。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询