scanf 是一个强大的格式化输入函数,但它也是导致许多初学者困惑和错误的根源,尤其是在处理字符串时。

使用 %s 格式符读取字符串
这是最基本、最直接的方法。
基本用法
%s 格式符会读取连续的字符,直到遇到空白字符(空格、制表符 \t、换行符 \n)为止,并在末尾自动添加一个字符串结束符 \0。
语法:
scanf("%s", 字符数组名);
注意: scanf 对于 %s 不需要传递变量的地址(即不需要 &),因为数组名本身就是其首元素的地址。

示例代码
#include <stdio.h>
int main() {
char name[20]; // 定义一个可以容纳19个字符+1个'\0'的字符数组
printf("请输入你的名字(不含空格): ");
scanf("%s", name);
printf("你好, %s!\n", name);
return 0;
}
运行示例 1 (输入不含空格):
请输入你的名字(不含空格): Alice
你好, Alice!
运行示例 2 (输入含空格):
请输入你的名字(不含空格): John Doe
你好, John!
你会发现,Doe 被留在了输入缓冲区中,等待下一次读取。
使用 %[^\n] 格式符读取一行(包含空格)
如果你想读取一整行输入,包括其中的空格,直到遇到换行符 \n 为止,可以使用 %[^\n]。
%[...]是一个“扫描集”(scanset)。^表示“非”。[^n]的意思就是“读取所有字符,直到遇到换行符\n为止”。
语法:
scanf("%[^\n]", 字符数组名);
示例代码
#include <stdio.h>
int main() {
char sentence[100];
printf("请输入一句话(可以包含空格): ");
// 注意:在 %[^\n] 前面最好加一个空格,用于跳过前一次输入留下的换行符
scanf(" %[^\n]", sentence);
printf("你输入的是: %s\n", sentence);
return 0;
}
注意: scanf(" %[^\n]", ...); 开头那个空格非常重要!它会消耗掉输入流中残留的换行符(比如用户按回车后产生的 \n),从而避免 scanf 误以为已经读取到了行尾。
运行示例:
请输入一句话(可以包含空格): C language is powerful and flexible.
你输入的是: C language is powerful and flexible.
使用 scanf("%s", ...) 的巨大风险:缓冲区溢出
这是 scanf 在处理字符串时最严重的问题。
问题所在
scanf("%s", ...) 不会检查输入的长度,如果用户输入的字符串长度超过了你定义的字符数组的大小,那么多余的字符就会溢出,覆盖掉数组后面的内存空间,这会导致:
- 程序崩溃。
- 数据被意外修改。
- 产生严重的安全漏洞(例如被恶意利用)。
危险示例
#include <stdio.h>
int main() {
char name[5]; // 只能存4个字符 + '\0'
printf("请输入一个名字: ");
scanf("%s", name); // 危险!没有限制长度
printf("你好, %s!\n", name);
return 0;
}
危险运行示例:
请输入一个名字: ThisIsAVeryLongName
你好, ThisIsAVeryLongName!
虽然程序可能不会立即崩溃,但 name 数组后面的内存已经被破坏了,这是非常危险的。
安全地使用 scanf:限制输入长度
为了防止缓冲区溢出,scanf 提供了一种方法来限制读取的字符数,在 %s 和 %[...] 中,可以指定一个最大宽度。
语法:
scanf("%ms", 字符数组名); // m 是一个长度修饰符
// 或者
scanf("%[^\n]", 字符数组名); // 这种方式不安全,建议用下面的方式
推荐的、安全的方式是:
scanf("%19s", name); // 对于 char name[20]
scanf("%99[^\n]", sentence); // 对于 char sentence[100]
%19s表示最多读取 19 个字符。scanf会自动在第 19 个字符后停止,并添加\0,总共占用 20 个字节空间,非常安全。%99[^\n]表示最多读取 99 个字符(直到遇到\n),为\0预留空间。
安全示例
#include <stdio.h>
int main() {
char name[5]; // 只能存4个字符 + '\0'
printf("请输入一个名字: ");
// 安全地读取,最多读取 4 个字符
scanf("%4s", name);
printf("你好, %s!\n", name);
return 0;
}
安全运行示例:
请输入一个名字: ThisIsAVeryLongName
你好, This!
这次,scanf 只读取了前 4 个字符 This,后面的内容留在了输入缓冲区中,程序没有崩溃,name 数组也得到了保护。
scanf 输入字符串后的遗留问题
每次你使用 scanf 读取输入时,它都会读取输入缓冲区中的数据,但不会消耗掉最后的换行符 \n,这个 \n 会留在缓冲区中,成为下一次输入的“幽灵”。
问题示例
#include <stdio.h>
int main() {
int age;
char name[20];
printf("请输入你的年龄: ");
scanf("%d", &age); // 读取年龄,留下 '\n' 在缓冲区
printf("请输入你的名字: ");
scanf("%s", name); // %s 遇到 '\n' 就认为输入结束了!
printf("年龄: %d, 名字: %s\n", age, name);
return 0;
}
运行示例:
请输入你的年龄: 25
请输入你的名字: // 程序直接跳过,等待下一次输入
年龄: 25, 名字: (空字符串或未定义值)
当程序执行到第二个 scanf 时,它发现缓冲区开头就是 \n,于是立即认为读取结束,name 没有被正确赋值。
解决方案
有几种方法可以解决这个问题:
清空输入缓冲区(推荐)
定义一个函数来清除缓冲区中的所有剩余字符,直到遇到换行符或文件结尾。
void clearInputBuffer() {
int c;
// 读取并丢弃字符,直到遇到换行符或EOF
while ((c = getchar()) != '\n' && c != EOF);
}
然后在每次 scanf 后调用它(除了你明确想利用 \n 的情况,如 scanf(" %[^\n]"))。
修正后的代码:
#include <stdio.h>
void clearInputBuffer() {
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
int main() {
int age;
char name[20];
printf("请输入你的年龄: ");
scanf("%d", &age);
clearInputBuffer(); // 清除缓冲区中的 '\n'
printf("请输入你的名字: ");
scanf("%s", name);
printf("年龄: %d, 名字: %s\n", age, name);
return 0;
}
在格式字符串中消耗掉换行符
对于数字后面紧跟字符串的情况,可以在 %d 后面加一个空格,这个空格会告诉 scanf 跳过接下来的所有空白字符(包括 \n)。
#include <stdio.h>
int main() {
int age;
char name[20];
printf("请输入你的年龄: ");
scanf("%d ", &age); // 注意 %d 后面的空格!
printf("请输入你的名字: ");
scanf("%s", name);
printf("年龄: %d, 名字: %s\n", age, name);
return 0;
}
这个方法很巧妙,但有时会让人困惑,不如 clearInputBuffer 函数清晰。
更好的替代方案:fgets
对于C语言初学者和大多数应用场景来说,fgets 是比 scanf 更安全、更可靠的字符串输入函数。
fgets 专门用于从流(如 stdin,标准输入)中读取一行。
语法:
fgets(字符数组名, 数组大小, 输入流);
- 字符数组名:存储读取内容的数组。
- 数组大小:最多读取
n-1个字符,并在末尾自动添加\0,这从根本上杜绝了缓冲区溢出的风险。 - 输入流:通常是
stdin,代表键盘输入。
fgets 的优点
- 绝对安全:因为它需要你提供缓冲区的大小,所以永远不会发生溢出。
- 行为一致:总能读取一行,包括换行符
\n(如果该行长度不超过n-1)。 - 简单直观。
fgets 的缺点
- 它会保留换行符
\n,如果不需要这个换行符,你需要手动将它去掉。
示例代码
#include <stdio.h>
#include <string.h> // 用于 strlen
int main() {
char name[20];
printf("请输入你的名字: ");
// sizeof(name) 会计算出数组的大小,这里是20
fgets(name, sizeof(name), stdin);
// 去掉末尾的换行符(如果存在)
size_t len = strlen(name);
if (len > 0 && name[len - 1] == '\n') {
name[len - 1] = '\0'; // 将换行符替换为字符串结束符
}
printf("你好, %s!\n", name);
return 0;
}
运行示例:
请输入你的名字: Bob Smith
你好, Bob Smith!
总结与最佳实践
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
scanf("%s", ...) |
简单 | 不安全(易溢出)、不能处理空格、遗留 \n 问题 |
强烈不推荐用于直接输入字符串。 |
scanf("%[^\n]", ...) |
可以读取含空格的行 | 不安全(易溢出)、遗留 \n 问题 |
不推荐。 |
scanf("%ms", ...) |
安全(带长度限制) | 语法不直观、遗留 \n 问题 |
可以,但不如 fgets 优雅。 |
scanf("%19s", ...) |
安全(带长度限制) | 语法直观、遗留 \n 问题 |
可以接受,但必须记得处理 \n。 |
fgets(...) |
绝对安全、行为一致、简单 | 会保留 \n(需要手动处理) |
强烈推荐,是C语言中读取字符串输入的最佳实践。 |
核心建议:
- 永远不要使用
scanf("%s", ...)来读取用户输入的字符串。 它太危险了。 - 首选
fgets函数。 它是现代C语言中处理字符串输入最安全、最可靠的方式。 - 如果你因为某些原因必须使用
scanf,请务必使用宽度限制(如scanf("%19s", name)),并且记得处理输入缓冲区中遗留的换行符。
