第2章:字符串输入输出:scanf/gets/fgets/puts/printf 的区别与陷阱

各位同学,咱们今天聊聊字符串的输入输出。说实话,这块儿是C语言里最容易出bug的地方之一。我见过太多新手,甚至工作两三年的同事,在这几个函数上栽跟头。你想想看,一个程序跑起来,数据进进出出,如果连输入输出都搞不定,后面再好的算法也是白搭。

咱们今天就把 scanfgetsfgetsputsprintf 这几个函数掰开揉碎了讲清楚。我会结合我自己的踩坑经历,告诉你每个函数什么时候该用,什么时候千万别用。

核心要点:字符串输入输出的本质是“缓冲区操作”。理解缓冲区,你就理解了这些函数的一半。

2.1 scanf 读字符串:看似简单,实则暗藏杀机

scanf 大家最熟悉了。用 %s 格式说明符读字符串,代码写起来很顺手:

char name[20];
scanf("%s", name);  // 注意:name 前面不需要 & 符号

但是,这里有个大坑。scanf 遇到空白字符(空格、制表符、换行符)就会停止读取。换句话说,你输入 "Zhang San",它只读到 "Zhang","San" 还留在缓冲区里。我当年做学生管理系统时,就因为这个bug,查了整整一个下午。

警告:scanf 不会检查缓冲区大小。如果你输入超过19个字符(留一个给'\0'),就会发生缓冲区溢出。这是安全漏洞的常见来源。

那怎么解决?可以用宽度限定:

char name[20];
scanf("%19s", name);  // 最多读19个字符,留一个给'\0'

嗯,这样安全多了。但空格问题还是没解决。如果你非要读带空格的字符串,可以用 %[^\n]

char name[50];
scanf("%49[^\n]", name);  // 读取直到遇到换行符

我个人习惯,在需要读简单单词时用 scanf,但一旦涉及整行文本,我会换别的函数。

2.2 gets:这个函数已经被“判死刑”了

gets 函数,我建议你直接忘掉它。为什么?因为它不检查缓冲区大小,是缓冲区溢出的“重灾区”。

char buf[10];
gets(buf);  // 危险!如果你输入20个字符,程序直接崩溃

我记得在C11标准里,gets 已经被正式移除了。但有些老编译器还支持。如果你在维护老代码,看到 gets,请立刻替换掉。我曾经接手过一个遗留项目,里面用了大量 gets,每次跑测试我都提心吊胆。

避坑指南:永远不要使用 gets()。如果你在面试中写出 gets,面试官可能会直接让你走人。

2.3 fgets:我心目中的“最佳输入函数”

fgets 是我最推荐的字符串输入函数。它安全、可控,而且行为明确。

char buf[100];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
    // 成功读取
}

fgets 有三个参数:目标缓冲区、缓冲区大小、输入流。它会自动在末尾加 '\0',并且最多读取 size-1 个字符。如果一行太长,剩下的字符会留在缓冲区里,下次读取时继续。

fgets 有个小特点:它会保留换行符。也就是说,你输入 "hello\n",buf 里存的是 "hello\n\0"。有时候你需要手动去掉这个换行符:

char buf[100];
if (fgets(buf, sizeof(buf), stdin)) {
    size_t len = strlen(buf);
    if (len > 0 && buf[len-1] == '\n') {
        buf[len-1] = '\0';  // 去掉换行符
    }
}

我在项目中几乎只用 fgets 来读字符串。它虽然比 scanf 多写几行代码,但换来的是安全和稳定。你想想看,一个稳定的程序比少写几行代码重要得多,对吧?

2.4 puts 和 printf:输出也有讲究

输出相对简单一些,但也有细节要注意。

puts 专门用来输出字符串,它会自动在末尾加一个换行符:

char str[] = "Hello, World!";
puts(str);  // 输出 "Hello, World!" 并换行

printf 更灵活,但不会自动换行:

printf("%s\n", str);  // 需要手动加 \n

那什么时候用 puts,什么时候用 printf?我的经验是:

  • 如果只是输出一个字符串,不需要格式化,用 puts。它更快,代码也更简洁。
  • 如果需要输出多个变量、数字、或者需要控制格式,用 printf

注意:puts 只能输出字符串,不能输出数字。如果你写 puts(123),编译器会报错。

还有一个容易被忽略的点:printf%s 要求参数必须是 '\0' 结尾的字符串。如果你传一个没有 '\0' 的字符数组,printf 会一直读下去,直到遇到内存中的 '\0',这可能导致输出乱码甚至程序崩溃。

2.5 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

字符串输入输出函数对比 输入函数 scanf("%s", buf) ✅ 适合读单词 ❌ 遇空格停止,不检查大小 gets(buf) — 已废弃 ❌ 不检查缓冲区大小 ❌ 安全漏洞,永远不要用 fgets(buf, size, stdin) ✅ 安全,可控制大小 ⚠ 保留换行符,需手动去除 输出函数 puts(str) ✅ 自动加换行,简洁 ⚠ 只能输出字符串 printf("%s", str) ✅ 灵活,可格式化 ⚠ 需手动加 \n 最佳实践 输入用 fgets,输出用 puts/printf 永远不要用 gets,scanf 要加宽度

2.6 实战对比:一个完整的例子

咱们来看一个完整的例子,对比这几个函数的行为:

#include <stdio.h>
#include <string.h>

int main() {
    char buf1[20], buf2[20], buf3[20];
    
    // 1. scanf 示例
    printf("请输入一个单词(用scanf):");
    scanf("%19s", buf1);
    printf("scanf 读到:%s\n\n", buf1);
    
    // 清空缓冲区
    while(getchar() != '\n');
    
    // 2. fgets 示例
    printf("请输入一行文本(用fgets):");
    fgets(buf2, sizeof(buf2), stdin);
    // 去掉换行符
    size_t len = strlen(buf2);
    if (len > 0 && buf2[len-1] == '\n') {
        buf2[len-1] = '\0';
    }
    printf("fgets 读到:%s\n\n", buf2);
    
    // 3. puts 和 printf 对比
    puts("这是 puts 输出的内容(自动换行)");
    printf("这是 printf 输出的内容(需手动换行)\n");
    
    return 0;
}

运行这个程序,你会发现:

  • scanf 只读了第一个单词,后面的内容被丢弃了。
  • fgets 读了整行,包括空格。
  • puts 自动加了换行,printf 没有。

2.7 避坑指南总结

最后,我把这些年踩过的坑总结成几条铁律,你记下来:

  1. 永远不要用 gets。没有例外,没有借口。
  2. fgets 代替 scanf 读字符串,除非你确定只读一个单词。
  3. 如果非要用 scanf,一定要加宽度限定,比如 %19s
  4. 注意缓冲区残留。混合使用 scanffgets 时,记得清空缓冲区。
  5. 输出时,能用 puts 就别用 printf,除非需要格式化。

我的个人习惯:在项目中,我统一用 fgets 读输入,用 putsprintf 输出。这样代码风格一致,不容易出错。你也不妨试试这个方案。

好了,字符串输入输出这块儿就讲到这里。记住,安全第一,功能第二。一个能稳定运行1000次的程序,比一个偶尔崩溃但“看起来更酷”的程序强得多。下次见!


公众号:蓝海资料掘金营,微信deep3321