好的巴达克斯,欢迎来到C语言的角斗场!scanf 读取字符串就像一把锋利但需要小心使用的双刃剑,用得好,它高效快捷;用得不好,它会给你带来无尽的“段错误”(Segmentation Fault)噩梦。

下面我将为你详细解析 scanf 输入字符串的方方面面,从基础到进阶,再到最终的王者之道。
基础用法:%s 的简单陷阱
scanf 使用 %s 格式说明符来读取一个字符串。
代码示例 1:最简单的读取
#include <stdio.h>
int main() {
char name[20]; // 定义一个能容纳20个字符的数组(包括字符串结尾的'\0')
printf("请输入你的名字: ");
scanf("%s", name); // 注意:name前面没有&,因为数组名本身就是地址
printf("你好, %s!\n", name);
return 0;
}
运行结果:
请输入你的名字: Sparta
你好, Sparta!
看起来很简单,对吗?但这里藏着第一个陷阱!

陷阱 1:缓冲区溢出 (Buffer Overflow)
scanf("%s", ...) 的工作原理是:它会一直读取字符,直到遇到空白字符(空格、Tab、换行符 \n)或者达到输入缓冲区的限制。
问题在于,它不会检查目标数组的大小。
代码示例 2:演示缓冲区溢出
#include <stdio.h>
int main() {
char name[5]; // 只能容纳5个字符(包括'\0')
printf("请输入一个很长的名字: ");
scanf("%s", name); // 危险!
printf("你输入的名字是: %s\n", name);
return 0;
}
运行结果:

请输入一个很长的名字: Alexander the Great
你输入的名字是: Alexander
发生了什么?
- 你输入了
Alexander the Great。 scanf遇到第一个空格,停止读取。- 它将
Alexander这个字符串(包括结尾的\0)存入name数组。 Alexander有9个字母,加上\0共10个字符。- 但你的
name数组只有5个字节的大小! - 结果就是,
Alexander的前5个字符被硬塞进去,覆盖了数组边界之外的内存,这会导致:- 程序崩溃(最幸运的情况)。
- 数据损坏(其他变量被意外修改)。
- 安全漏洞(攻击者可以注入恶意代码)。
这是 scanf 读取字符串时最常见、最危险的错误!
进阶用法:带宽度限制的 %s
为了解决缓冲区溢出的问题,%s 可以指定一个最大读取宽度。
语法:%ms,m 是一个整数,表示最多读取 m-1 个字符。
为什么是 m-1? 因为 scanf 会自动在读取的字符串末尾添加一个 \0(字符串结束符),所以必须为它预留一个位置。
代码示例 3:安全的字符串读取
#include <stdio.h>
int main() {
char name[20]; // 定义一个20字节的数组
printf("请输入你的名字: ");
// 最多读取 19 个字符,留1个给 '\0'
scanf("%19s", name);
printf("你好, %s!\n", name);
return 0;
}
运行结果 1(正常输入):
请输入你的名字: Sparta
你好, Sparta!
运行结果 2(超长输入):
请输入你的名字: Alexander the Great
你好, Alexander!
这次,即使你输入了很长的字符串,scanf 也只会读取前19个字符(Alexander),安全地存入 name 数组,而 the Great 则会留在输入缓冲区中,等待下一次读取操作。
高级用法:%[] - 自定义字符集
%s 只能读取到空白符为止,如果你想读取包含空格的整行("New York"),%s 就无能为力了,这时,你需要 %[ 格式说明符。
%[...] 表示读取一个字符集合,直到遇到不在集合中的字符为止。
常见用法:
-
读取一行(直到换行符
\n)%[^\n]的意思是:读取所有字符,直到遇到\n为止。^在[]内部表示“非”。#include <stdio.h> int main() { char address[100]; printf("请输入你的地址: "); scanf("%[^\n]", address); // 读取直到换行符 printf("你的地址是: %s\n", address); return 0; }运行结果:
请输入你的地址: 123 Main Street, Apt 4B 你的地址是: 123 Main Street, Apt 4B -
只读取数字
%[0-9]表示只读取0到9的数字。#include <stdio.h> int main() { char id[20]; printf("请输入你的ID(只包含数字): "); scanf("%[0-9]", id); printf("你的ID是: %s\n", id); return 0; }运行结果:
请输入你的ID(只包含数字): user12345 你的ID是: 12345
%[] 的注意事项:
- 同样,它没有内置的宽度限制,你必须自己指定宽度,否则同样有缓冲区溢出的风险!
- 正确用法:
%99[^\n](在一个100字节的数组中读取)。 - 它会消耗掉缓冲区中的换行符
\n,这会影响后续的scanf或getchar调用。
终极王者之道:抛弃 scanf,拥抱 fgets
虽然通过限制宽度可以安全地使用 scanf,但对于读取整行输入(尤其是用户输入),fgets 通常是更安全、更可靠、更推荐的选择。
fgets 函数专门为读取一行文本而设计。
fgets 的原型:
char *fgets(char *str, int n, FILE *stream);
str: 存储读取内容的字符数组。n: 最多读取n-1个字符(和scanf一样,留一个给\0)。stream: 从哪里读取,对于标准输入,我们使用stdin。
代码示例 4:使用 fgets 安全读取一行
#include <stdio.h>
int main() {
char name[50];
printf("请输入你的全名: ");
// 从标准输入 stdin 读取最多 49 个字符到 name 数组
fgets(name, sizeof(name), stdin);
printf("你好, %s!\n", name);
return 0;
}
运行结果:
请输入你的全名: John Doe
你好, John Doe!
fgets 的巨大优势:
- 绝对安全:它有一个明确的参数
n来限制读取的字符数,从根本上杜绝了缓冲区溢出的可能。sizeof(name)是一个很好的实践,可以自动获取数组大小。 - 读取整行:它会读取换行符
\n(如果一行内容不超过n-1个字符),这让你可以精确地知道用户输入的完整内容。 - 行为可预测:它不会像
scanf那样留下未处理的字符在缓冲区里(除非你读取的内容超出了限制)。
fgets 的唯一“缺点”和解决方案:
“缺点”:fgets 会把换行符 \n 也读进来。
解决方案:在读取后,手动检查并移除 \n。
#include <stdio.h>
#include <string.h> // 用于 strlen
int main() {
char name[50];
printf("请输入你的全名: ");
fgets(name, sizeof(name), stdin);
// 检查是否读取了换行符
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
// 如果是,则将其替换为字符串结束符 '\0'
name[len - 1] = '\0';
}
printf("你好, %s!\n", name); // 输出时就没有换行了
return 0;
}
总结与最佳实践
| 特性 | scanf("%s", ...) |
scanf("%19s", ...) |
fgets(name, sizeof(name), stdin) |
|---|---|---|---|
| 安全性 | 极低,极易溢出 | 高,有宽度限制 | 极高,内置宽度限制 |
| 遇到空白符停止 | 遇到空白符或达到宽度停止 | 读取一行,包含换行符 | |
| 换行符处理 | 留在缓冲区 | 留在缓冲区 | 被读取到字符串中 |
| 适用场景 | 读取单个单词(已知输入安全) | 读取单个单词(需谨慎) | 读取用户整行输入(推荐) |
| 代码示例 | scanf("%s", buf); |
scanf("%19s", buf); |
fgets(buf, sizeof(buf), stdin); |
给角斗士(程序员)的建议:
- 永远不要使用裸露的
scanf("%s", ...)来读取用户输入的字符串。 这是一条必须遵守的铁律。 - 如果你只需要读取一个单词(没有空格),并且能保证它不会超过数组大小,可以使用带宽度限制的
scanf("%ms", ...)。 - 如果你需要读取一行文本(可能包含空格),或者你追求最高的代码健壮性和安全性,请毫不犹豫地选择
fgets。 fgets会读取换行符,并在使用字符串前检查并移除它。
拿起 fgets 这把更可靠的剑,去征服你的C语言程序吧!
