C/C++中gets函数被废弃的原因与安全替代方案fgets详解
2026/7/14 12:48:01 网站建设 项目流程

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 个字符(还要留一个给字符串结束符\0),gets会毫不犹豫地把多余的数据写到str后面的内存里,这就是缓冲区溢出。轻则程序崩溃,重则成为安全漏洞的入口,历史上许多著名的攻击(如 Morris 蠕虫)都利用了类似的缺陷。

正因为其固有的安全性问题,在 C11 标准(ISO/IEC 9899:2011)和 C++14 标准中,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指向的数组中。读取会在以下三种情况之一停止:

  1. 读取到num-1个字符。
  2. 读取到换行符\n,并且换行符会被存储到数组中。
  3. 遇到文件结束符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)的值是 20,fgets最多读取 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 安全标准的演进与编译器实现

  1. C11/C++14 标准的废除:国际标准化组织(ISO)在制定新标准时,将gets列为“过时且危险”的函数,并从标准库中移除。这意味着符合新标准的编译器可以不再提供它。
  2. 编译器的安全合规:像 GCC、Clang、MSVC 这样的主流编译器,在新版本中遵循最新标准是必然选择。它们通过将gets从头文件中移除或将其标记为不可用,来推动开发者使用更安全的函数。
  3. SDL(安全开发生命周期)要求:对于微软这样的厂商,其产品本身也遵循严格的安全开发规范。默认禁用不安全的 API,是减少由其开发工具产生的软件漏洞的重要一环。

当你使用 VS2015 及以上版本编译包含gets的代码时,编译器前端在解析头文件后,根本找不到gets的声明,因此在编译阶段直接报错C3861(标识符未找到)。这发生在链接之前,所以即使你想通过手动声明函数来绕过也行不通,因为链接时也找不到对应的库实现。

3.2 其他不安全的函数与替代方案

gets只是冰山一角。C 标准库中有一批因其安全性问题而备受诟病的函数,它们通常以strmem开头,且不接收目标缓冲区大小作为参数。了解它们并知道替代方案,是资深 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(“%<width>s”, 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不可用。

排查步骤:

  1. 确认编译器:在 VS Code 的终端(Terminal)中,运行cl /?cl /Bv,查看输出中的版本号。如果版本号大于等于 18.00(对应 VS2013),则gets不被支持。
  2. 修改 tasks.json:确保你的编译任务(在.vscode/tasks.json中)正确调用了cl编译器,并且没有设置一些兼容老标准的标志(如/D_CRT_SECURE_NO_WARNINGS可以禁用安全函数警告,但无法让不存在的gets出现)。
  3. 一劳永逸的解决:将代码中的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 -std=c11 -Werror=implicit-function-declaration -o program program.c
    使用-std=c11指定遵循 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; }

这个函数的优点:

  1. 绝对安全:使用fgets,无缓冲区溢出风险。
  2. 处理任意长度输入:动态分配内存,用户输入多长都没问题。
  3. 接口友好:自动去除换行符,返回的字符串可直接使用。
  4. 资源管理清晰:调用者负责free,责任明确。

5.2 项目迁移与代码审查清单

当你接手一个老项目或进行代码审查时,可以按照以下清单来检查和清理不安全的输入函数:

  1. 全局搜索:在代码库中搜索getsstrcpystrcatsprintf(不带长度限制的)等函数。
  2. 评估风险:对于每个找到的实例,检查目标缓冲区的大小是否明确,以及输入源是否可控。如果缓冲区大小是硬编码的常量(如char buf[256]),而输入来自不可信的用户或文件,则风险较高。
  3. 制定替换策略
    • gets->fgets+ 换行符处理。
    • strcpy(dest, src)->strncpy_s(dest, dest_size, src, _TRUNCATE)(Windows) 或snprintf(dest, dest_size, “%s”, src)
    • sprintf(dest, ...)->snprintf(dest, size, ...)
  4. 测试验证:替换后,必须进行充分的测试,特别是边界测试(输入恰好等于缓冲区大小、输入远超缓冲区大小等)。

5.3 编译器警告是你的朋友

最后,请养成将编译器警告级别调到最高的习惯(在 MSVC 中是/W4,在 GCC/Clang 中是-Wall -Wextra),并把警告视为错误(/WX-Werror)。编译器是发现潜在问题(包括使用废弃函数)的第一道防线。C3861错误虽然直接阻止了编译,但更多的问题是以警告形式出现的。严肃对待每一个警告,是写出健壮、安全代码的重要一步。

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

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

立即咨询