scanf与gets输入有何区别?

99ANYc3cd6
预计阅读时长 18 分钟
位置: 首页 C语言 正文
特性 scanf gets (已废弃)
功能 格式化输入,可以读取多种类型的数据(整数、浮点数、字符、字符串)。 专门读取一行字符串,直到遇到换行符 \n 或文件结尾。
输入缓冲区 会留下换行符 \n 在缓冲区中,容易导致后续输入函数(如 gets, getchar)出错。 会消耗掉换行符 \n,读取后缓冲区是干净的。
安全性 不安全,容易因格式不匹配导致意外行为,且无法限制输入长度,极易导致缓冲区溢出。 极度不安全完全无法限制输入长度,是缓冲区溢出的重灾区,已被 C11 标准从标准库中移除
现代替代方案 使用时注意处理缓冲区残留的 \n,并用 %ns (如 %10s) 限制输入长度。 绝对不要使用 gets!使用 fgets 替代。

scanf 函数详解

scanf 是 "scan formatted" 的缩写,意为“扫描格式化输入”,它是一个非常强大但也非常“狡猾”的函数。

c语言scanf gets
(图片来源网络,侵删)

函数原型

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

执行流程分析:

c语言scanf gets
(图片来源网络,侵删)
  1. 程序运行,打印 "请输入你的年龄: "。
  2. 用户输入 20 并按下回车
    • scanf("%d", &age) 成功读取了数字 20
    • 回车符 \n 被留在了输入缓冲区里
  3. 程序打印 "请输入你的名字: "。
  4. scanf("%s", name) 开始读取,它遇到缓冲区里残留的 \n,而 %s 的规则是“读取非空白字符”,\n 对它来说是“空白符”。
  5. %s 认为输入已经结束,它没有读取到任何字符,name 数组没有被赋值(或者被赋值为空字符串 )。
  6. 程序继续执行,打印出 "年龄: 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)。

c语言scanf gets
(图片来源网络,侵删)
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 函数没有提供任何方式来限制它要读取的字符数,它只会傻傻地读取,直到遇到 \nEOF,如果用户输入的字符超过了目标缓冲区的大小,就会发生灾难性的缓冲区溢出。

示例代码(极度危险,切勿运行!):

#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

fgetsgets 的安全替代品,它限制了读取的字符数。

fgets 函数原型

char *fgets(char *str, int n, FILE *stream);
  • str:存储读取内容的字符数组。
  • n:要读取的最大字符数(包括结尾的 \0),这是安全的关键!
  • stream:输入流,通常使用 stdin 表示标准输入(键盘)。

fgetsgets 的区别

  1. 安全性fgets 通过参数 n 限制了读取的最大长度,从根本上杜绝了缓冲区溢出的风险。
  2. 换行符的处理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;
}

总结与最佳实践

  1. gets

    • 彻底忘记 gets 的存在,它是一个历史遗留的、有严重安全漏洞的函数。
    • 替代:统一使用 fgets 来读取一行字符串。
  2. scanf

    • scanf 仍然有其用途,尤其是在读取格式化数据(如数字)时,但必须小心谨慎。
    • 何时用
      • 读取整数、浮点数等基本类型时。
      • 当输入格式非常严格且可控时。
    • 如何安全地用
      • 读取数字后要读取字符串:务必使用 while(getchar() != '\n'); 来清空输入缓冲区。
      • 使用 %s:一定要指定宽度,如 scanf("%19s", str);,防止溢出。
      • 读取一行:如果只是想读取一行任意内容(包括空格),scanf 不适合,应该用 fgets
  3. 现代 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 直接与输入流交互带来的各种问题。

-- 展开阅读全文 --
头像
织梦搜索关键词维护怎么做?
« 上一篇 03-14
dede全自动采集插件真的能全自动吗?
下一篇 » 03-14

相关文章

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

目录[+]