一、输入输出基础:printf格式化输出、scanf格式化输入、格式说明符详解
说实话,C语言的输入输出,是每个初学者最先接触,也最容易踩坑的地方。我见过不少工作两三年的工程师,还在为printf的格式控制符翻手册。今天咱们就把这块彻底讲透。
1.1 printf格式化输出
printf说白了就是“按指定格式往屏幕上打印东西”。它的原型在stdio.h里:
int printf(const char *format, ...);
返回值是实际打印的字符数,这个细节很多人忽略。我在项目中就遇到过,用printf返回值做调试计数,结果发现换行符也算一个字符。
1.1.1 基本用法
#include <stdio.h>
int main() {
int a = 42;
float b = 3.14159;
char c = 'A';
char str[] = "Hello C";
printf("整数:%d\n", a);
printf("浮点数:%f\n", b);
printf("字符:%c\n", c);
printf("字符串:%s\n", str);
return 0;
}
1.1.2 格式控制符详解
| 格式符 | 说明 | 示例 | 输出 |
|---|---|---|---|
| %d | 有符号十进制整数 | printf("%d", 42) | 42 |
| %f | 浮点数(默认6位小数) | printf("%f", 3.14) | 3.140000 |
| %c | 单个字符 | printf("%c", 'A') | A |
| %s | 字符串 | printf("%s", "Hi") | Hi |
| %x | 十六进制(小写) | printf("%x", 255) | ff |
| %p | 指针地址 | printf("%p", &a) | 0x7ff...(地址值) |
核心要点:格式符必须与变量类型匹配。%d对应int,%f对应double(float会被自动提升为double),%c对应char,%s对应char*。
1.1.3 格式修饰符
光有基本格式符还不够,实际项目中经常需要控制宽度、精度、对齐方式。我个人习惯把这些修饰符记成“三件套”:
- 宽度修饰:%5d 表示至少占5个字符宽度,右对齐
- 精度修饰:%.2f 表示保留2位小数
- 对齐修饰:%-5d 表示左对齐
int num = 42;
float pi = 3.1415926;
printf("右对齐:[%5d]\n", num); // 输出:[ 42]
printf("左对齐:[%-5d]\n", num); // 输出:[42 ]
printf("补零:[%05d]\n", num); // 输出:[00042]
printf("两位小数:[%.2f]\n", pi); // 输出:[3.14]
printf("宽度+精度:[%8.3f]\n", pi);// 输出:[ 3.142]
我的小技巧:调试时我喜欢用 %p 打印指针地址,配合 %d 看偏移量,排查内存问题特别管用。
1.2 scanf格式化输入
scanf和printf是“镜像关系”——printf是输出,scanf是输入。但有个关键区别:scanf需要传变量的地址。这个坑我当年栽过不止一次。
int age;
float salary;
char name[50];
printf("请输入年龄:");
scanf("%d", &age); // 注意 & 符号
printf("请输入薪资:");
scanf("%f", &salary); // 浮点数也要 &
printf("请输入姓名:");
scanf("%s", name); // 数组名本身就是地址,不用 &
警告:scanf的%s读取字符串时,遇到空格、制表符、换行符就会停止。如果你想读取带空格的字符串,用 gets() 或 fgets() 更安全。
1.2.1 scanf的返回值
scanf返回成功匹配并赋值的参数个数。这个返回值在健壮性编程中非常有用。我曾经写过一个数据采集程序,就用这个返回值做输入校验:
int a, b;
int result = scanf("%d %d", &a, &b);
if (result != 2) {
printf("输入格式错误!期望两个整数\n");
// 清空输入缓冲区
while (getchar() != '\n');
}
1.2.2 格式控制符的微妙之处
嗯,这里要注意:scanf的格式控制符和printf虽然长得一样,但行为有差异。比如 %f 在scanf中对应float*,而 %lf 才对应double*。printf里 %f 和 %lf 都可以打印double,但scanf必须严格区分。
| 格式符 | printf对应类型 | scanf对应类型 |
|---|---|---|
| %d | int | int* |
| %f | double(float自动提升) | float* |
| %lf | double | double* |
| %c | int(char自动提升) | char* |
| %s | char* | char*(数组名) |
1.3 常见陷阱与避坑指南
做嵌入式这些年,我总结出几个输入输出的“经典翻车现场”:
- 缓冲区残留问题:scanf读取后,换行符会留在缓冲区,影响后续的%c读取。解决方案是在%c前加空格:scanf(" %c", &ch);
- 浮点数精度丢失:printf默认6位小数,但float的有效数字只有6-7位,double有15-16位。别指望%.10f能打印出精确的10位小数。
- 字符串溢出:scanf("%s", buf) 不检查缓冲区长度。安全做法是用 scanf("%49s", buf) 限制最大读取长度。
我曾经踩过的坑:有一次调试串口通信,发现printf打印的十六进制数总是少一位。排查了半天,原来是 %02x 写成了 %2x——少了前导零。从那以后,我打印十六进制数据一律用 %02x 或 %04x,确保对齐。
1.4 知识体系总览
下面这张图,把本章的核心知识点串起来了。你想想看,输入输出其实就三件事:格式符、修饰符、类型匹配。搞懂这三样,基本就掌握了八成。
1.5 实战建议
最后,给你几个我实际项目中的建议:
- 调试时多用printf:在关键变量处加printf,打印格式统一用 %d 或 %f,别偷懒。
- 生产代码慎用scanf:嵌入式产品中,scanf容易导致输入阻塞。建议用 fgets + sscanf 组合,更可控。
- 养成检查返回值的习惯:不管是printf还是scanf,返回值里藏着很多调试信息。
我的习惯:每个printf后面加个 fflush(stdout),确保数据立即输出。特别是在嵌入式裸机开发中,没有操作系统帮你缓冲,不fflush的话,数据可能一直憋在缓冲区里。
好了,输入输出基础就讲到这里。记住:格式符是“语法”,修饰符是“修辞”,类型匹配是“逻辑”。三样都拿捏住,你就能写出既漂亮又健壮的输入输出代码。
公众号:蓝海资料掘金营,微信deep3321