第2章:字符串输入输出:scanf/gets/fgets/puts/printf 的区别与陷阱
各位同学,咱们今天聊聊字符串的输入输出。说实话,这块儿是C语言里最容易出bug的地方之一。我见过太多新手,甚至工作两三年的同事,在这几个函数上栽跟头。你想想看,一个程序跑起来,数据进进出出,如果连输入输出都搞不定,后面再好的算法也是白搭。
咱们今天就把 scanf、gets、fgets、puts、printf 这几个函数掰开揉碎了讲清楚。我会结合我自己的踩坑经历,告诉你每个函数什么时候该用,什么时候千万别用。
核心要点:字符串输入输出的本质是“缓冲区操作”。理解缓冲区,你就理解了这些函数的一半。
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 知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
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 避坑指南总结
最后,我把这些年踩过的坑总结成几条铁律,你记下来:
- 永远不要用
gets。没有例外,没有借口。 - 用
fgets代替scanf读字符串,除非你确定只读一个单词。 - 如果非要用
scanf,一定要加宽度限定,比如%19s。 - 注意缓冲区残留。混合使用
scanf和fgets时,记得清空缓冲区。 - 输出时,能用
puts就别用printf,除非需要格式化。
我的个人习惯:在项目中,我统一用 fgets 读输入,用 puts 或 printf 输出。这样代码风格一致,不容易出错。你也不妨试试这个方案。
好了,字符串输入输出这块儿就讲到这里。记住,安全第一,功能第二。一个能稳定运行1000次的程序,比一个偶尔崩溃但“看起来更酷”的程序强得多。下次见!