scanf输入字符串,缓冲区溢出如何防?

99ANYc3cd6
预计阅读时长 22 分钟
位置: 首页 C语言 正文

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

c语言scanf 输入字符串
(图片来源网络,侵删)

使用 %s 格式符读取字符串

这是最基本、最直接的方法。

基本用法

%s 格式符会读取连续的字符,直到遇到空白字符(空格、制表符 \t、换行符 \n)为止,并在末尾自动添加一个字符串结束符 \0

语法:

scanf("%s", 字符数组名);

注意: scanf 对于 %s 不需要传递变量的地址(即不需要 &),因为数组名本身就是其首元素的地址。

c语言scanf 输入字符串
(图片来源网络,侵删)

示例代码

#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 的优点

  1. 绝对安全:因为它需要你提供缓冲区的大小,所以永远不会发生溢出。
  2. 行为一致:总能读取一行,包括换行符 \n(如果该行长度不超过 n-1)。
  3. 简单直观

fgets 的缺点

  1. 它会保留换行符 \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语言中读取字符串输入的最佳实践

核心建议:

  1. 永远不要使用 scanf("%s", ...) 来读取用户输入的字符串。 它太危险了。
  2. 首选 fgets 函数。 它是现代C语言中处理字符串输入最安全、最可靠的方式。
  3. 如果你因为某些原因必须使用 scanf,请务必使用宽度限制(如 scanf("%19s", name)),并且记得处理输入缓冲区中遗留的换行符
-- 展开阅读全文 --
头像
织梦上传图片为何不显示?
« 上一篇 02-16
printf在C语言中是什么意思?
下一篇 » 02-16

相关文章

取消
微信二维码
支付宝二维码

目录[+]