第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操作。
为什么会出问题?我给你画个图:
缓冲区问题的典型场景:
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的重灾区。把基础打牢,后面才能走得更远。