5、标准输入输出:printf格式化、scanf使用陷阱、getchar/putchar、行缓冲与无缓冲
标准输入输出,说白了就是C语言跟用户打交道的窗口。printf往屏幕上吐东西,scanf从键盘收东西。听起来简单?我当年刚入行时也这么想,结果被坑得够呛。今天咱们就把这块掰开揉碎了讲清楚。
5.1 printf格式化——不只是打印字符串
printf的格式化,远比你想象的要强大。我见过不少同事,用了好几年C语言,还在用%d打天下。其实printf的格式控制符,每个字段都有讲究。
先看一个完整的格式说明:
%[标志][宽度][.精度][长度]类型
举个例子:
printf("%-10.3f\n", 3.14159);
// 输出:"3.142 " 左对齐,宽度10,保留3位小数
这里每个部分的作用,我整理了一张表:
| 字段 | 可选值 | 作用 |
|---|---|---|
| 标志 | - + 空格 # 0 | 左对齐、显示正号、前导空格、备用格式、补零 |
| 宽度 | 数字 或 * | 最小输出宽度,不够则补空格 |
| 精度 | .数字 或 .* | 浮点数小数位数,字符串最大长度 |
| 长度 | h l ll L z t | 修饰数据类型长度 |
| 类型 | d i u f e g s c p x | 指定输出格式 |
%p 打印指针地址,用 %zu 打印 size_t 类型。这两个组合能帮你快速定位内存问题。
宽度和精度可以用*动态指定,这在写通用函数时特别有用:
int width = 10;
int precision = 3;
printf("%*.*f\n", width, precision, 3.14159);
// 等价于 printf("%10.3f\n", 3.14159);
5.2 scanf使用陷阱——我踩过的坑
scanf这个函数,说它是C语言里最容易出bug的函数之一,一点不过分。我在项目中至少见过十几种由scanf引发的诡异bug。
这是最常见的坑。当你用scanf读入一个整数后,换行符还留在缓冲区里。紧接着用getchar或gets,就会直接读到这个换行符。
看个例子:
int age;
char name[50];
printf("请输入年龄:");
scanf("%d", &age); // 输入 25 回车,缓冲区里还留着 '\n'
printf("请输入姓名:");
gets(name); // 直接读到 '\n',name变成空字符串
解决办法?我一般会在scanf后面加一个getchar吃掉多余的换行符:
scanf("%d", &age);
getchar(); // 吃掉换行符
用户输入了字母,你却用%d读整数。scanf会返回0,但输入内容还留在缓冲区。下次循环又读到同样的内容,无限循环就来了。
我曾经在一个生产工具里遇到过这个问题。用户不小心按错了键,程序直接卡死。后来我改成这样:
int num;
while (scanf("%d", &num) != 1) {
printf("输入无效,请重新输入:");
while (getchar() != '\n'); // 清空缓冲区
}
char line[100];
int num;
fgets(line, sizeof(line), stdin);
if (sscanf(line, "%d", &num) == 1) {
// 成功解析
} else {
// 处理错误
}
5.3 getchar与putchar——字符级IO
getchar和putchar是C语言里最基础的字符IO函数。别看它们简单,用好了能写出非常高效的代码。
getchar从stdin读一个字符,返回int类型。为什么是int不是char?因为要返回EOF(通常是-1)。
int ch;
while ((ch = getchar()) != EOF) {
putchar(ch);
}
// 这个循环会把输入原样输出,直到遇到文件结束符
我写过一个简单的文本统计工具,就用到了getchar:
int ch;
long char_count = 0, line_count = 0;
while ((ch = getchar()) != EOF) {
char_count++;
if (ch == '\n') {
line_count++;
}
}
printf("字符数:%ld,行数:%ld\n", char_count, line_count);
5.4 行缓冲与无缓冲——理解背后的机制
为什么有时候printf的内容不会立刻显示?为什么getchar要等按了回车才返回?这背后是缓冲机制在起作用。
C语言的缓冲模式有三种:
| 缓冲模式 | 触发条件 | 典型场景 |
|---|---|---|
| 行缓冲 | 遇到换行符或缓冲区满 | 标准输出(stdout) |
| 全缓冲 | 缓冲区满才刷新 | 文件输出 |
| 无缓冲 | 立即输出 | 标准错误(stderr) |
我画了一张图,帮你理解这三种模式的区别:
行缓冲是什么意思?说白了就是:数据先攒着,遇到换行符才真正输出。你想想看,如果每打一个字符就输出一次,那效率得多低?
但有时候行缓冲会带来麻烦。比如你想打印一个进度条:
for (int i = 0; i < 100; i++) {
printf("\r进度:%d%%", i);
// 没有换行符,不会立即显示
// 需要用 fflush(stdout) 强制刷新
fflush(stdout);
usleep(100000);
}
这里如果不加fflush,你什么都看不到,直到循环结束才一次性输出。我刚开始写这类工具时就吃过这个亏。
你可以用setvbuf函数改变缓冲模式:
// 将stdout改为无缓冲
setvbuf(stdout, NULL, _IONBF, 0);
// 将stdout改为行缓冲
setvbuf(stdout, NULL, _IOLBF, 0);
// 将stdout改为全缓冲,缓冲区大小8192
char buf[8192];
setvbuf(stdout, buf, _IOFBF, sizeof(buf));
不过说实话,我很少主动改缓冲模式。默认的行缓冲对大多数场景都够用。只有在写一些特殊工具时,比如实时数据采集、进度显示,才会考虑调整。
嗯,标准输入输出这块,说难不难,说简单也不简单。关键是要理解背后的缓冲机制,以及scanf那些容易踩的坑。记住一句话:能用fgets就别用scanf,能用stderr就别用stdout。这两个原则能帮你省下不少调试时间。