第1章:输入输出与格式化——printf、scanf、getchar、putchar、字符串输入输出与缓冲区问题

各位同学,咱们今天聊聊C语言里最基础、也最容易被忽视的一块——输入输出。说白了,就是程序怎么跟人“说话”,人怎么给程序“下指令”。我做了十几年嵌入式开发,见过太多因为输入输出没处理好导致线上翻车的案例。嗯,这一章咱们就把这些坑一个个填上。

1.1 printf格式化输出——不只是打印

printf,C语言里最常用的输出函数。但你真的会用吗?

我个人习惯把printf当成调试利器。在嵌入式开发中,串口输出基本靠它。但要注意,printf不是“无脑打印”,它背后有一套格式化规则。

核心格式:

printf("格式控制字符串", 参数列表);

常用的格式占位符:

占位符含义示例
%d有符号十进制整数printf("%d", 10);
%u无符号十进制整数printf("%u", 10);
%x / %X十六进制(小写/大写)printf("%x", 255); // ff
%f浮点数printf("%.2f", 3.1415); // 3.14
%c单个字符printf("%c", 'A');
%s字符串printf("%s", "hello");
%p指针地址printf("%p", &a);

避坑指南:我曾经在项目中用%d打印size_t类型的变量,结果在64位系统上直接打印出乱码。size_t要用%zu,记住了。

格式化输出还有几个修饰符:

  • 宽度控制:printf("%5d", 10); 输出" 10"(右对齐,占5位)
  • 精度控制:printf("%.3f", 3.14159); 输出"3.142"(四舍五入)
  • 左对齐:printf("%-5d", 10); 输出"10 "(左对齐)
  • 补零:printf("%05d", 10); 输出"00010"

你想想看,如果嵌入式设备里要打印一个时间戳,用%05d是不是比手动补零优雅多了?

1.2 scanf格式化输入——小心这个“坑王”

scanf,C语言里最让人头疼的函数之一。为什么?因为它跟缓冲区关系太密切了。

int num;
scanf("%d", &num);  // 注意:必须加&

scanf的常见问题:

  • 忘记取地址符&:初学者常犯,编译不报错,运行直接崩溃
  • 输入类型不匹配:你输入了字母,它等着数字,结果就是死循环
  • 缓冲区残留:输入完数字后按回车,回车符留在缓冲区,下一个scanf直接“吃掉”

我曾经踩过的坑:写一个菜单程序,先scanf读整数,再gets读字符串。结果gets直接读到空行,程序跳过了输入。原因就是scanf留下的换行符被gets吃掉了。解决方案:在scanf后面加一个getchar()吃掉换行符。

我个人建议:在嵌入式开发中,尽量少用scanf,改用fgets+sscanf的组合,可控性更强。

1.3 getchar与putchar——字符级输入输出

getchar和putchar,简单但实用。它们每次只处理一个字符。

char ch;
ch = getchar();   // 从键盘读一个字符
putchar(ch);      // 输出这个字符

这两个函数在嵌入式调试中很有用。我记得有一次调试串口协议,就是用getchar逐字节读取数据帧,再用putchar回显验证。

注意:getchar返回的是int类型,不是char。为什么?因为要返回EOF(-1)表示文件结束或出错。如果你用char接收,EOF可能被截断。

小技巧:用getchar实现“按任意键继续”:

printf("按任意键继续...");
getchar();  // 等待用户按键

1.4 字符串输入输出——gets与puts

gets和puts,一对老搭档。但gets已经被C11标准废弃了,为什么?

危险警告:gets不检查缓冲区长度,输入超长直接溢出,覆盖内存。黑客最喜欢这种漏洞。

char buf[10];
gets(buf);  // 如果输入超过10个字符,直接溢出!

替代方案:

  • fgets:可以指定最大读取长度,安全
  • scanf("%s", buf):但遇到空格就停止
char buf[100];
fgets(buf, sizeof(buf), stdin);  // 安全!最多读99个字符+'\0'

puts相对安全,它自动在末尾加换行符:

puts("Hello World");  // 等价于 printf("Hello World\n");

1.5 缓冲区问题——输入输出的“隐形杀手”

缓冲区,说白了就是一块临时存储区域。标准输入输出默认是行缓冲的——遇到换行符才真正执行I/O操作。

为什么会出问题?我给你画个图:

键盘输入 用户输入数据 输入缓冲区 暂存未处理的数据 scanf/gets 常见问题: 1. scanf后残留换行符 2. gets不检查长度导致溢出 3. 混合使用scanf和getchar混乱

缓冲区问题的典型场景:

int num;
char str[100];

printf("请输入数字:");
scanf("%d", &num);  // 输入 123 然后回车
// 缓冲区里现在有:'1','2','3','\n'

printf("请输入字符串:");
gets(str);  // 直接读到 '\n',str为空字符串!
// 问题:换行符被gets吃掉了,程序跳过了输入

解决方案:

  • 清空缓冲区:在scanf后加getchar()吃掉换行符
  • 统一使用fgets:所有输入都用fgets,再用sscanf解析
  • 使用fflush(stdin):但注意,fflush(stdin)是未定义行为,不同编译器表现不同

我的经验:在嵌入式项目中,我一般这样处理:

char buf[100];
fgets(buf, sizeof(buf), stdin);  // 统一用fgets
int num;
sscanf(buf, "%d", &num);  // 再从缓冲区解析

这样既安全又可控,不会出现缓冲区残留问题。

1.6 本章小结

输入输出看似简单,但缓冲区问题、格式化匹配问题,都是实际开发中的高频坑。我个人建议:

  • 输出用printf,掌握格式化控制
  • 输入尽量用fgets+sscanf组合
  • 字符级操作用getchar/putchar
  • 永远不要用gets
  • 时刻关注缓冲区状态

嗯,这一章就到这里。记住一句话:输入输出是程序的门面,也是bug的重灾区。把基础打牢,后面才能走得更远。


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