核心思想:一句话总结
gets()是“危险的”,因为它无法限制读取的字符数,极易导致缓冲区溢出,从而引发程序崩溃或安全漏洞。在现代 C 编程中,应绝对避免使用gets()。fgets()是“安全的”,因为它允许你指定最大读取字符数,可以有效地防止缓冲区溢出,是读取字符串的标准和推荐方法。
gets() 函数 (不安全 - 已废弃)
gets 函数的名字是 "get string"(获取字符串)的缩写。

函数原型
char *gets(char *str);
功能
从标准输入(通常是键盘)读取一行字符,直到遇到换行符 \n 或文件结束符 EOF,它会读取换行符 \n,但不会将其存储到字符串中,它会自动在字符串末尾添加一个空字符 '\0' 来表示字符串结束。
参数
char *str: 一个指向字符数组的指针,用于存储读取到的字符串。
返回值
- 成功时,返回指向
str的指针。 - 如果读取失败(例如遇到文件结束符
EOF),则返回NULL指针。 - 如果一开始就遇到
EOF,str指向的内容不会被修改。
为什么 gets() 是危险的?
gets() 的最大、也是最致命的缺陷是:它不知道目标缓冲区有多大,它会一直读取,直到遇到换行符或 EOF,完全不考虑 str 指向的数组是否装得下这么多字符。
这会导致缓冲区溢出。
缓冲区溢出的后果:

- 程序崩溃:如果写入的数据超出了分配的内存空间,可能会覆盖掉其他重要数据(如函数返回地址),导致程序异常终止。
- 安全漏洞:这是最严重的问题,恶意用户可以利用这个漏洞,向程序中注入并执行恶意代码,通过精心构造的超长输入,覆盖函数的返回地址,使其指向恶意代码所在的内存位置。
gets() 的使用示例 (危险!)
#include <stdio.h>
int main() {
char name[10]; // 定义一个只能存放9个字符+1个'\0'的缓冲区
printf("请输入你的名字: ");
gets(name); // 危险!用户输入超过9个字符就会导致溢出
printf("你好, %s\n", name);
return 0;
}
如果用户输入 Alexander (9个字符):
- 程序正常工作。
name数组内容为'A','l','e','x','a','n','d','e','r','\0'。
如果用户输入 Johann Sebastian Bach (超过9个字符):
gets()会将所有字符(包括空格)读入name数组,直到遇到换行符。- 它会无情地写入
name[10],name[11]... 等等,这些内存并不属于name数组。 - 结果就是缓冲区溢出,程序行为不可预测,可能崩溃。
注意:GCC 编译器在编译使用
gets()的代码时,会直接给出警告:warning: 'gets' is deprecated and not portable [-Wdeprecated-declarations]这是在强烈警告你不要使用它。
fgets() 函数 (安全 - 推荐)
fgets 函数的名字是 "file get string"(文件获取字符串)的缩写,虽然名字里有 "file",但它也可以用于标准输入。

函数原型
char *fgets(char *str, int n, FILE *stream);
功能
从指定的输入流(可以是文件 stdin,即标准输入)中读取一行字符,或者读取最多 n-1 个字符。
参数
char *str: 一个指向字符数组的指针,用于存储读取到的字符串。int n: 要读取的最大字符数(包括结尾的空字符'\0')。这是fgets安全的关键。FILE *stream: 输入流,当要从键盘读取时,使用stdin。
返回值
- 成功时,返回指向
str的指针。 - 如果读取失败或遇到文件结束符
EOF,则返回NULL指针。
fgets() 的安全机制
fgets 的第二个参数 n 限制了它最多只会读取 n-1 个字符,它会在以下情况之一发生时停止读取:
- 读取了
n-1个字符。 - 读取到换行符
\n。 - 到达文件结束符
EOF。
如果读取到换行符 \n,fgets 会将换行符 \n 也存储到字符串中,它在最后添加一个空字符 '\0'。
fgets() 的使用示例 (安全!)
#include <stdio.h>
#include <string.h> // 用于 strlen()
int main() {
char name[10]; // 同样定义一个只能存放9个字符+1个'\0'的缓冲区
printf("请输入你的名字: ");
// 安全!最多读取 sizeof(name) - 1 个字符,防止溢出
fgets(name, sizeof(name), stdin);
// 问题:fgets会把换行符'\n'也读进来,我们需要去掉它
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0'; // 将换行符替换为字符串结束符
}
printf("你好, %s\n", name);
return 0;
}
分析上面的代码:
sizeof(name)的值是 10。fgets(name, 10, stdin)告诉函数:最多从stdin读取 9 个字符(因为第10个位置要留给'\0')。- 如果用户输入
Alexander(9个字符):fgets会读取全部 9 个字符,然后在后面加'\0'。\n没有被读取。name数组内容为'A','l','e','x','a','n','d','e','r','\0'。strlen(name)返回 9。name[8]是'r',不是'\n',if条件不成立,字符串不变。
- 如果用户输入
Johann Sebastian Bach(超过9个字符):fgets会读取前 9 个字符,然后在后面加'\0'。\n也没有被读取。name数组内容为'J','o','h','a','n','n',' ','S','\0'。- 程序安全,没有溢出。
处理换行符 \n 的小技巧:
上面的 if 判断是标准做法,你也可以用一个更简洁的库函数 strcspn:
#include <stdio.h>
#include <string.h>
int main() {
char name[10];
printf("请输入你的名字: ");
fgets(name, sizeof(name), stdin);
// strcspn 找到 name 中第一个出现 '\n' 的位置,并返回其索引
// 如果没找到,返回字符串的长度
name[strcspn(name, "\n")] = '\0';
printf("你好, %s\n", name);
return 0;
}
gets() 和 fgets() 的详细对比
| 特性 | gets() |
fgets() |
|---|---|---|
| 安全性 | 不安全,极易导致缓冲区溢出 | 安全,通过参数 n 限制读取长度 |
| 缓冲区大小 | 不知道,由程序员自己保证 | 明确知道,由参数 n 指定 |
换行符 \n |
不保留,读取后丢弃 | 保留,作为字符串的一部分存入 |
| 输入源 | 只能是标准输入 stdin |
可以是任何文件流,如 stdin, fp 等 |
| 参数个数 | 1个 (char *str) |
3个 (char *str, int n, FILE *stream) |
| 现代标准 | C99 标准已废弃,从 C11 开始正式移除 | C89 标准引入,所有现代标准都支持 |
| 推荐度 | 绝对不要使用 | 强烈推荐,读取字符串的标准方法 |
如何替代 gets()? (完整示例)
假设你的旧代码是这样:
// 旧代码 (危险) char buffer[100]; gets(buffer);
你应该用 fgets() 来替换它,并处理掉多余的换行符。
#include <stdio.h>
#include <string.h>
#define BUFFER_SIZE 100
int main() {
char buffer[BUFFER_SIZE];
printf("请输入一行文字: ");
// 1. 使用 fgets 安全地读取
fgets(buffer, BUFFER_SIZE, stdin);
// 2. 去掉 fgets 读取的换行符 '\n'
// 注意:如果输入的字符数正好等于 BUFFER_SIZE - 1,'\n' 可能不会被读入
// 所以检查一下是否存在 '\n' 是一个好习惯
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
printf("你输入的是: \"%s\"\n", buffer);
return 0;
}
记住这个核心原则:
永远不要使用
gets()。 无论是在什么情况下,无论你的代码看起来多么简单,都不要用它。总是优先使用
fgets()来从标准输入读取字符串。 记得要指定缓冲区的大小(sizeof(你的数组)),并且记得处理掉它可能读入的换行符\n。
这是编写健壮、安全 C 程序的基本功。
