| 特性 | scanf |
gets (已废弃) |
|---|---|---|
| 功能 | 格式化输入,可以读取多种类型的数据(整数、浮点数、字符、字符串)。 | 专门读取一行字符串,直到遇到换行符 \n 或文件结尾。 |
| 输入缓冲区 | 会留下换行符 \n 在缓冲区中,容易导致后续输入函数(如 gets, getchar)出错。 |
会消耗掉换行符 \n,读取后缓冲区是干净的。 |
| 安全性 | 不安全,容易因格式不匹配导致意外行为,且无法限制输入长度,极易导致缓冲区溢出。 | 极度不安全。完全无法限制输入长度,是缓冲区溢出的重灾区,已被 C11 标准从标准库中移除。 |
| 现代替代方案 | 使用时注意处理缓冲区残留的 \n,并用 %ns (如 %10s) 限制输入长度。 |
绝对不要使用 gets!使用 fgets 替代。 |
scanf 函数详解
scanf 是 "scan formatted" 的缩写,意为“扫描格式化输入”,它是一个非常强大但也非常“狡猾”的函数。

函数原型
int scanf(const char *format, ...);
- 返回值:成功匹配并赋值的参数个数,如果读取失败(如遇到文件结尾 EOF),则返回
EOF。 format:格式化字符串,用于告诉scanf期望接收什么样的输入。- :可变参数列表,用于存放读取到的值,必须是变量的地址(使用
&运算符)。
常用格式说明符
| 格式说明符 | 含义 | 示例 |
|---|---|---|
%d |
十进制整数 | int a; scanf("%d", &a); |
%f |
单精度浮点数 | float b; scanf("%f", &b); |
%lf |
双精度浮点数 | double c; scanf("%lf", &c); |
%c |
单个字符 | char ch; scanf("%c", &ch); |
%s |
字符串(遇到空白符停止) | char str[20]; scanf("%s", str); |
scanf 的主要“坑”和特点
坑1:缓冲区残留的换行符 \n
这是 scanf 最常见的问题,当你使用 %d, %f, %c 等读取基本类型数据后,用户按下回车键,这个回车符(\n)会留在输入缓冲区中。
示例代码:
#include <stdio.h>
int main() {
int age;
char name[20];
printf("请输入你的年龄: ");
scanf("%d", &age); // 用户输入 20 并按下回车
printf("请输入你的名字: ");
scanf("%s", name); // 问题出在这里!
printf("年龄: %d\n", age);
printf("名字: %s\n", name);
return 0;
}
执行流程分析:

- 程序运行,打印 "请输入你的年龄: "。
- 用户输入
20并按下回车。scanf("%d", &age)成功读取了数字20。- 回车符
\n被留在了输入缓冲区里。
- 程序打印 "请输入你的名字: "。
scanf("%s", name)开始读取,它遇到缓冲区里残留的\n,而%s的规则是“读取非空白字符”,\n对它来说是“空白符”。%s认为输入已经结束,它没有读取到任何字符,name数组没有被赋值(或者被赋值为空字符串 )。- 程序继续执行,打印出 "年龄: 20" 和 "名字: " (名字为空)。
解决方案:
在读取非字符串(如 %d, %f)之后,如果后面紧跟着读取字符串(%s 或使用 getchar),需要手动清空缓冲区。
// 方法1:使用一个空的 getchar() 来消耗掉 \n
scanf("%d", &age);
getchar(); // 读取并丢弃缓冲区中的 \n
// 方法2:更健壮的清空方式,可以清空所有残留字符
while (getchar() != '\n'); // 循环读取直到遇到换行符
坑2:%s 的安全性问题
scanf("%s", name); 会一直读取,直到遇到空白符(空格、制表符、换行符),它不会检查输入的长度,如果用户输入的字符超过了 name 数组的大小(name[20] 但用户输入了50个字符),就会发生缓冲区溢出,破坏内存,导致程序崩溃或安全漏洞。
解决方案:
使用 %ns 的格式,n 是一个最大读取字符数(包括结尾的 \0)。

char name[20];
scanf("%19s", name); // 最多读取19个字符,留1位给字符串结束符 '\0'
gets 函数详解
重要声明:gets 函数已经被 C11 标准废弃,并且从标准库中移除,在编译器(如 GCC, Clang)中,如果使用它,会收到警告或错误信息。
绝对不要在生产代码中使用 gets!
函数原型
char *gets(char *str);
- 功能:从标准输入(通常是键盘)读取一行字符,直到遇到换行符
\n或文件结尾EOF,它会读取换行符\n,但不会将其存入目标字符串,而是在字符串末尾添加一个\0。 - 返回值:成功返回指向
str的指针,失败或遇到EOF时返回NULL。
gets 的致命缺陷:缓冲区溢出
gets 函数没有提供任何方式来限制它要读取的字符数,它只会傻傻地读取,直到遇到 \n 或 EOF,如果用户输入的字符超过了目标缓冲区的大小,就会发生灾难性的缓冲区溢出。
示例代码(极度危险,切勿运行!):
#include <stdio.h>
int main() {
char buffer[10]; // 只有10个字节的空间
printf("请输入一些文字: ");
gets(buffer); // 危险!用户输入超过10个字符,程序就会崩溃
printf("你输入的是: %s\n", buffer);
return 0;
}
如果用户输入 "Hello, this is a very long string!",程序会尝试将这整个字符串写入 buffer 数组,从而覆盖掉 buffer 之后的内存区域,导致不可预测的行为(程序崩溃、数据损坏、甚至被黑客利用执行恶意代码)。
安全的替代方案:fgets
fgets 是 gets 的安全替代品,它限制了读取的字符数。
fgets 函数原型
char *fgets(char *str, int n, FILE *stream);
str:存储读取内容的字符数组。n:要读取的最大字符数(包括结尾的\0),这是安全的关键!stream:输入流,通常使用stdin表示标准输入(键盘)。
fgets 与 gets 的区别
- 安全性:
fgets通过参数n限制了读取的最大长度,从根本上杜绝了缓冲区溢出的风险。 - 换行符的处理:
fgets会保留换行符\n,如果一行输入的字符数小于n-1,\n会被存入str的末尾,如果一行输入的字符数等于或大于n-1,则str不会包含\n。
示例代码:
#include <stdio.h>
#include <string.h>
int main() {
char buffer[20];
printf("请输入你的名字: ");
fgets(buffer, sizeof(buffer), stdin); // 安全地读取最多19个字符
// 去掉 fgets 读取时可能带有的 \n
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
printf("你好, %s!\n", buffer);
return 0;
}
总结与最佳实践
-
gets:- 彻底忘记
gets的存在,它是一个历史遗留的、有严重安全漏洞的函数。 - 替代:统一使用
fgets来读取一行字符串。
- 彻底忘记
-
scanf:scanf仍然有其用途,尤其是在读取格式化数据(如数字)时,但必须小心谨慎。- 何时用:
- 读取整数、浮点数等基本类型时。
- 当输入格式非常严格且可控时。
- 如何安全地用:
- 读取数字后要读取字符串:务必使用
while(getchar() != '\n');来清空输入缓冲区。 - 使用
%s时:一定要指定宽度,如scanf("%19s", str);,防止溢出。 - 读取一行:如果只是想读取一行任意内容(包括空格),
scanf不适合,应该用fgets。
- 读取数字后要读取字符串:务必使用
-
现代 C 语言的输入方案:
- 读取一行字符串(包含空格):首选
fgets。 - 读取格式化数据(数字):可以使用
scanf,但之后要处理缓冲区。 - 混合读取(数字+字符串):一种更稳健的方法是,全部使用
fgets读取整行,然后用sscanf从字符串中解析数据,这样可以避免scanf和输入缓冲区的直接交互带来的问题。
混合读取的最佳实践示例:
#include <stdio.h> #include <string.h> int main() { int age; char name[50]; char line_buffer[100]; // 用于临时存放整行输入 printf("请输入你的年龄和名字( 25 张三): "); fgets(line_buffer, sizeof(line_buffer), stdin); // 使用 sscanf 从 line_buffer 中解析数据 int items_read = sscanf(line_buffer, "%d %49s", &age, name); if (items_read == 2) { printf("解析成功!年龄: %d, 名字: %s\n", age, name); } else { printf("输入格式不正确!\n"); } return 0; }这种方法将输入和解析分离,逻辑更清晰,也避免了
scanf直接与输入流交互带来的各种问题。 - 读取一行字符串(包含空格):首选
