C/C++中gets函数被废弃的原因与安全替代方案fgets详解 1. 问题概述为什么我的代码里gets用不了了如果你最近刚从老版本的开发环境比如 VC6.0、VS2010切换到 Visual Studio 2013 或更高版本或者刚开始在 VS Code 里配置 C/C 环境大概率会撞上这个经典的“拦路虎”error C3861: ‘gets’: identifier not found。编译器冷冰冰地告诉你它不认识gets这个函数了。这感觉就像你家里的老工具箱一直用得好好的螺丝刀突然有一天告诉你这工具太危险不让你用了。gets函数在 C/C 的历史上就是这么一把“危险但好用”的螺丝刀。它用于从标准输入比如键盘读取一行字符串直到遇到换行符或文件结束符。它的“好用”在于极其简单一行代码就能搞定输入而它的“危险”则臭名昭著——它完全不检查目标字符数组的边界。如果你定义了一个char str[10]却输入了超过 9 个字符还要留一个给字符串结束符\0gets会毫不犹豫地把多余的数据写到str后面的内存里这就是缓冲区溢出。轻则程序崩溃重则成为安全漏洞的入口历史上许多著名的攻击如 Morris 蠕虫都利用了类似的缺陷。正因为其固有的安全性问题在 C11 标准ISO/IEC 9899:2011和 C14 标准中gets函数被正式废弃。随后主流的编译器厂商开始在其新版本中响应这一变化。微软在 Visual Studio 2013对应 MSVC 编译器版本 12.0中将gets从标准库头文件中移除这就是你遇到C3861错误的根本原因。编译器不是“找不到”而是“故意不提供”。这个错误提示其实是编译器在保护你督促你使用更安全的方法。所以当你看到C3861时首先要明白这不是你的代码语法写错了也不是头文件没包含而是一个标志着编程实践进步的“时代更迭”信号。解决它的方法不是想方设法让gets起死回生而是学习并迁移到更安全、更现代的替代方案。2. 核心解决方案用fgets替代gets既然gets已被废弃那么标准答案就是使用它的安全替代品fgets。这个函数在 C89/C90 标准中就存在了因此兼容性极好是所有现代编译器的“座上宾”。2.1fgets函数详解与基本用法fgets的函数原型如下char *fgets(char *str, int num, FILE *stream);str: 指向用于存储读取字符串的字符数组的指针。num: 这是最关键的安全参数指定了最多读取的字符数包括结尾的\0。stream: 输入流从标准输入读取就传入stdin。它的工作逻辑是从指定的流stream中读取最多num-1个字符到str指向的数组中。读取会在以下三种情况之一停止读取到num-1个字符。读取到换行符\n并且换行符会被存储到数组中。遇到文件结束符EOF。无论哪种情况停止fgets都会在字符串末尾自动添加一个空字符\0。这个“自动添加结束符”和“检查边界”的行为彻底杜绝了缓冲区溢出的可能。现在我们来看如何将一段使用gets的老代码进行改造。原始代码会报错 C3861#include stdio.h int main() { char name[20]; printf(Enter your name: ); gets(name); // 危险不检查边界 printf(Hello, %s!\n, name); return 0; }安全改造后的代码#include stdio.h int main() { char name[20]; printf(Enter your name: ); fgets(name, sizeof(name), stdin); // 安全最多读19个字符 printf(Hello, %s!\n, name); return 0; }这里sizeof(name)的值是 20fgets最多读取 19 个字符并预留第 20 个位置给\0。这是一个好习惯即使你以后把数组大小从 20 改成 100这里也无需手动修改数字。2.2 处理fgets带来的“小尾巴”换行符fgets有一个与gets不同的特性如果它是因为读取到换行符而停止的那么这个换行符\n也会被存入字符串。而gets会丢弃换行符。这个差异有时会导致输出或后续字符串比较出现意想不到的结果。例如输入Amy然后回车gets(name)得到的字符串是Amy\0。fgets(name, 20, stdin)得到的字符串是Amy\n\0。当你用printf(Hello, %s!\n, name);输出时后者会显示为Hello, Amy !多了一个空行。在将输入与固定字符串比较时如if (strcmp(name, Amy) 0)也会因为多出的\n而失败。因此我们通常需要在fgets后“修剪”掉这个可能的换行符#include stdio.h #include string.h // 需要用到 strcspn 或 strlen int main() { char name[20]; printf(Enter your name: ); fgets(name, sizeof(name), stdin); // 方法1使用 strcspn 找到换行符位置并替换为结束符 name[strcspn(name, \n)] \0; // 方法2使用 strlen 找到字符串末尾检查并替换 // size_t len strlen(name); // if (len 0 name[len-1] \n) { // name[len-1] \0; // } printf(Hello, %s!\n, name); return 0; }strcspn(name, “\n”)函数会计算name中从头开始不包含“\n”的字符长度。如果存在\n该函数返回其索引位置我们将其替换为\0如果不存在比如输入填满了缓冲区则返回字符串长度此操作不会越界。实操心得我强烈推荐使用strcspn的方法它一行代码就能优雅地处理两种情况有换行符和没有换行符且是标准库函数效率有保障。这比手动用strlen再判断再替换更简洁、更不易出错。3. 深入探究为什么编译器要“除掉”gets理解编译器背后的动机能帮助我们建立更好的编程安全意识。这不仅仅是微软 MSVC 编译器的个别行为而是整个 C/C 生态进化的结果。3.1 安全标准的演进与编译器实现C11/C14 标准的废除国际标准化组织ISO在制定新标准时将gets列为“过时且危险”的函数并从标准库中移除。这意味着符合新标准的编译器可以不再提供它。编译器的安全合规像 GCC、Clang、MSVC 这样的主流编译器在新版本中遵循最新标准是必然选择。它们通过将gets从头文件中移除或将其标记为不可用来推动开发者使用更安全的函数。SDL安全开发生命周期要求对于微软这样的厂商其产品本身也遵循严格的安全开发规范。默认禁用不安全的 API是减少由其开发工具产生的软件漏洞的重要一环。当你使用 VS2015 及以上版本编译包含gets的代码时编译器前端在解析头文件后根本找不到gets的声明因此在编译阶段直接报错C3861标识符未找到。这发生在链接之前所以即使你想通过手动声明函数来绕过也行不通因为链接时也找不到对应的库实现。3.2 其他不安全的函数与替代方案gets只是冰山一角。C 标准库中有一批因其安全性问题而备受诟病的函数它们通常以str或mem开头且不接收目标缓冲区大小作为参数。了解它们并知道替代方案是资深 C/C 程序员的基本素养。不安全函数危险原因安全替代函数 (C11)平台特定安全函数 (Windows)gets(str)无边界检查缓冲区溢出fgets(str, size, stdin)-strcpy(dest, src)可能覆盖dest之后的内存strncpy_s(dest, dest_size, src, count)strcpy_s(dest, dest_size, src)strcat(dest, src)可能超出dest缓冲区strncat_s(dest, dest_size, src, count)strcat_s(dest, dest_size, src)sprintf(dest, fmt, ...)可能产生超长字符串snprintf(dest, size, fmt, ...)sprintf_s(dest, dest_size, fmt, ...)scanf(“%s”, str)与gets类似无长度限制scanf(“%widths”, str)或fgetsscanf_s(“%s”, str, size)注意事项上表中以_s结尾的函数如strcpy_s是 C11 标准附录 K 中定义的“边界检查接口”但它们在所有平台上的实现和支持程度不一。snprintf是广泛支持且更便携的选择。对于scanf即使在支持scanf_s的编译器上使用fgets读取整行再解析如用sscanf也是更稳健的做法。4. 高级场景与疑难排查在实际项目中你可能会遇到更复杂的情况或者尝试了一些“野路子”来解决问题。我们来分析几种常见场景。4.1 场景一维护遗留代码需要快速编译通过有时你手头有一段非常古老的、充斥着gets的代码你暂时不想或不能逐一修改只想先让它编译通过进行测试或评估。错误方法手动声明gets你可能会在网上看到有人说“在文件顶部加上char *gets(char *str);声明一下就好了。” 这在 VS2013 之前或许可行因为那时库文件里还有它的实现。但在 VS2013 及以后库文件里已经移除了gets的二进制代码。即使编译通过链接时也会报错LNK2019: unresolved external symbol gets因为链接器找不到这个函数的实体。可行但极其不推荐的临时方法使用预处理器宏替换在包含标准头文件之前定义自己的gets实现。#include stdio.h #include string.h // 警告此方法仅为临时测试有严重缺陷 #define gets(str) my_unsafe_gets(str) char *my_unsafe_gets(char *str) { int i 0; char ch; while ((ch getchar()) ! \n ch ! EOF) { str[i] ch; } str[i] \0; return str; } int main() { char buf[10]; gets(buf); // 实际上调用了 my_unsafe_gets printf(%s\n, buf); return 0; }严重警告这个my_unsafe_gets函数和原始的gets一样危险它同样没有边界检查。这只是一个演示绝对不要在产品代码或任何严肃的场合使用。它唯一的价值是让你理解为什么gets危险以及为什么必须用fgets。正确的临时方案使用fgets包装宏一个相对好一点的临时方案是用宏将gets调用替换为fgets调用并处理掉换行符。这至少能保证安全。#include stdio.h #include string.h // 将代码中所有的 gets(str) 替换为 fgets_safe(str) #define gets(str) fgets_safe(str, sizeof(str)) void fgets_safe(char *str, size_t size) { if (fgets(str, size, stdin) NULL) { str[0] \0; // 处理输入错误 return; } // 去除换行符 char *newline strchr(str, \n); if (newline) *newline \0; } // 注意这个宏严重依赖 sizeof(str) 能正确获取数组大小。 // 如果 str 是指针而非数组sizeof(str) 得到的是指针大小会导致严重错误。即使这个方案也有巨大局限对指针无效。它再次证明老老实实修改源代码才是王道。4.2 场景二在 VS Code 等编辑器中使用 MSVC 编译器很多初学者在 VS Code 中配置 C/C 环境安装了 Microsoft C/C 扩展和 MSVC 编译器工具链通常通过安装 “Desktop development with C” 的 Visual Studio Build Tools 获得。在这种情况下你使用的编译器就是 MSVC同样遵循新标准因此gets不可用。排查步骤确认编译器在 VS Code 的终端Terminal中运行cl /?或cl /Bv查看输出中的版本号。如果版本号大于等于 18.00对应 VS2013则gets不被支持。修改 tasks.json确保你的编译任务在.vscode/tasks.json中正确调用了cl编译器并且没有设置一些兼容老标准的标志如/D_CRT_SECURE_NO_WARNINGS可以禁用安全函数警告但无法让不存在的gets出现。一劳永逸的解决将代码中的gets替换为fgets这是唯一正确且长期有效的路径。4.3 场景三与其他编译器GCC Clang的交叉对比你可能会好奇在 Linux 上用 GCC 或者 macOS 上用 Clang 编译同样的代码会怎样GCC/Clang 默认行为它们通常会发出一个警告warning而不是错误error。警告信息类似于warning: implicit declaration of function ‘gets’; did you mean ‘fgets’?或更直接的warning: the ‘gets’ function is dangerous and should not be used.。代码通常仍能编译链接并运行因为出于兼容性考虑这些编译器在标准库中暂时还保留了gets的实现。如何让 GCC/Clang 报错你可以通过添加严格的编译选项来将警告升级为错误强制自己改正。例如gcc -stdc11 -Werrorimplicit-function-declaration -o program program.c使用-stdc11指定遵循 C11 标准-Werror...将特定警告视为错误。根本建议无论编译器是否允许出于代码安全性和可移植性你的代码未来可能在 MSVC 下编译的考虑都应该主动将gets替换为fgets。5. 最佳实践与经验总结经过上面的分析我们可以提炼出处理C3861错误以及编写安全输入代码的一套最佳实践。5.1 安全的用户输入处理模板对于控制台程序的用户输入我推荐使用一个经过实战检验的辅助函数#include stdio.h #include string.h #include stdlib.h // 为了 realloc /** * brief 从标准输入安全地读取一行动态分配内存。 * param prompt 可选的提示符字符串。 * return 指向读取字符串的指针。使用后需调用 free() 释放。 * 如果发生错误或 EOF返回 NULL。 */ char* read_line(const char* prompt) { if (prompt) { printf(%s, prompt); fflush(stdout); // 确保提示信息立即显示 } size_t capacity 16; // 初始缓冲区大小 size_t len 0; char* buffer (char*)malloc(capacity * sizeof(char)); if (!buffer) return NULL; while (1) { if (fgets(buffer len, capacity - len, stdin) NULL) { // 遇到 EOF 或错误 free(buffer); return NULL; } len strlen(buffer len); // 更新当前长度 // 检查是否读到了完整的行即缓冲区中有换行符 if (len 0 buffer[len - 1] \n) { buffer[len - 1] \0; // 去掉换行符 break; } // 如果没有换行符说明缓冲区满了但行还没读完 // 扩大缓冲区 size_t new_capacity capacity * 2; char* new_buffer (char*)realloc(buffer, new_capacity); if (!new_buffer) { free(buffer); return NULL; } buffer new_buffer; capacity new_capacity; } // 可选收缩缓冲区到刚好合适的大小 char* result (char*)realloc(buffer, (len 1) * sizeof(char)); return result ? result : buffer; // 如果 realloc 失败返回原 buffer } // 使用示例 int main() { char* name read_line(Enter your name: ); if (name) { printf(Hello, %s!\n, name); free(name); // 切记释放内存 } else { printf(Failed to read input or EOF reached.\n); } return 0; }这个函数的优点绝对安全使用fgets无缓冲区溢出风险。处理任意长度输入动态分配内存用户输入多长都没问题。接口友好自动去除换行符返回的字符串可直接使用。资源管理清晰调用者负责free责任明确。5.2 项目迁移与代码审查清单当你接手一个老项目或进行代码审查时可以按照以下清单来检查和清理不安全的输入函数全局搜索在代码库中搜索gets、strcpy、strcat、sprintf不带长度限制的等函数。评估风险对于每个找到的实例检查目标缓冲区的大小是否明确以及输入源是否可控。如果缓冲区大小是硬编码的常量如char buf[256]而输入来自不可信的用户或文件则风险较高。制定替换策略gets-fgets 换行符处理。strcpy(dest, src)-strncpy_s(dest, dest_size, src, _TRUNCATE)(Windows) 或snprintf(dest, dest_size, “%s”, src)。sprintf(dest, ...)-snprintf(dest, size, ...)。测试验证替换后必须进行充分的测试特别是边界测试输入恰好等于缓冲区大小、输入远超缓冲区大小等。5.3 编译器警告是你的朋友最后请养成将编译器警告级别调到最高的习惯在 MSVC 中是/W4在 GCC/Clang 中是-Wall -Wextra并把警告视为错误/WX或-Werror。编译器是发现潜在问题包括使用废弃函数的第一道防线。C3861错误虽然直接阻止了编译但更多的问题是以警告形式出现的。严肃对待每一个警告是写出健壮、安全代码的重要一步。