C语言scanf输入字符串时如何正确处理空格?

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

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

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

下面我将为你详细解析 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!

看起来很简单,对吗?但这里藏着第一个陷阱!

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

陷阱 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;
}

运行结果:

c语言scanf输入字符串
(图片来源网络,侵删)
请输入一个很长的名字: Alexander the Great
你输入的名字是: Alexander

发生了什么?

  1. 你输入了 Alexander the Great
  2. scanf 遇到第一个空格,停止读取。
  3. 它将 Alexander 这个字符串(包括结尾的 \0)存入 name 数组。
  4. Alexander 有9个字母,加上 \0 共10个字符。
  5. 但你的 name 数组只有5个字节的大小!
  6. 结果就是,Alexander 的前5个字符被硬塞进去,覆盖了数组边界之外的内存,这会导致:
    • 程序崩溃(最幸运的情况)。
    • 数据损坏(其他变量被意外修改)。
    • 安全漏洞(攻击者可以注入恶意代码)。

这是 scanf 读取字符串时最常见、最危险的错误!


进阶用法:带宽度限制的 %s

为了解决缓冲区溢出的问题,%s 可以指定一个最大读取宽度

语法:%msm 是一个整数,表示最多读取 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 就无能为力了,这时,你需要 %[ 格式说明符。

%[...] 表示读取一个字符集合,直到遇到不在集合中的字符为止。

常见用法:

  1. 读取一行(直到换行符 \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
  2. 只读取数字 %[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,这会影响后续的 scanfgetchar 调用。

终极王者之道:抛弃 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 的巨大优势:

  1. 绝对安全:它有一个明确的参数 n 来限制读取的字符数,从根本上杜绝了缓冲区溢出的可能。sizeof(name) 是一个很好的实践,可以自动获取数组大小。
  2. 读取整行:它会读取换行符 \n(如果一行内容不超过 n-1 个字符),这让你可以精确地知道用户输入的完整内容。
  3. 行为可预测:它不会像 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);

给角斗士(程序员)的建议:

  1. 永远不要使用裸露的 scanf("%s", ...) 来读取用户输入的字符串。 这是一条必须遵守的铁律。
  2. 如果你只需要读取一个单词(没有空格),并且能保证它不会超过数组大小,可以使用带宽度限制的 scanf("%ms", ...)
  3. 如果你需要读取一行文本(可能包含空格),或者你追求最高的代码健壮性和安全性,请毫不犹豫地选择 fgets
  4. fgets 会读取换行符,并在使用字符串前检查并移除它。

拿起 fgets 这把更可靠的剑,去征服你的C语言程序吧!

-- 展开阅读全文 --
头像
dede留言转发邮箱如何配置?
« 上一篇 今天
没有更多啦!
下一篇 »

相关文章

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

目录[+]