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');  // 清空缓冲区
}
避坑指南:我建议能用fgets+sscanf组合,就别直接用scanf。fgets先读一行字符串,再用sscanf解析,可控性高得多。
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);
注意:getchar返回的是int,千万别用char接收。否则在有些平台上,EOF(-1)会被截断成0xFF,导致永远检测不到文件结束。

5.4 行缓冲与无缓冲——理解背后的机制

为什么有时候printf的内容不会立刻显示?为什么getchar要等按了回车才返回?这背后是缓冲机制在起作用。

C语言的缓冲模式有三种:

缓冲模式 触发条件 典型场景
行缓冲 遇到换行符或缓冲区满 标准输出(stdout)
全缓冲 缓冲区满才刷新 文件输出
无缓冲 立即输出 标准错误(stderr)

我画了一张图,帮你理解这三种模式的区别:

C语言三种缓冲模式对比 行缓冲 数据先存缓冲区 遇到 \n 或满时刷新 stdout 默认模式 终端交互常用 全缓冲 缓冲区满才刷新 默认大小 4096 字节 文件操作默认模式 性能最优 无缓冲 立即输出 不缓存 stderr 默认模式 错误信息即时显示 实际应用场景: • 调试时用 fprintf(stderr, ...) 确保信息即时显示 • 写日志文件时用全缓冲,提高写入性能 • 交互式程序用行缓冲,用户输入一行处理一行

行缓冲是什么意思?说白了就是:数据先攒着,遇到换行符才真正输出。你想想看,如果每打一个字符就输出一次,那效率得多低?

但有时候行缓冲会带来麻烦。比如你想打印一个进度条:

for (int i = 0; i < 100; i++) {
    printf("\r进度:%d%%", i);
    // 没有换行符,不会立即显示
    // 需要用 fflush(stdout) 强制刷新
    fflush(stdout);
    usleep(100000);
}

这里如果不加fflush,你什么都看不到,直到循环结束才一次性输出。我刚开始写这类工具时就吃过这个亏。

我的习惯:在写命令行工具时,所有需要实时显示的信息,我都用fprintf(stderr, ...)输出。stderr默认无缓冲,省去了手动刷新的麻烦。

你可以用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。这两个原则能帮你省下不少调试时间。


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