2、stdio.h 核心函数(上):printf格式化输出、scanf格式化输入、getchar与putchar、gets与puts的隐患
stdio.h 这个头文件,说白了就是 C 语言和外界打交道的「嘴巴」。你写的程序要跟用户说话,用户要告诉程序点什么,都得靠它。今天咱们就聊聊这张嘴怎么用,以及怎么用好它。
我个人习惯,拿到一个新项目,第一件事就是先把 stdio.h 相关的函数理一遍。为什么?因为 90% 的调试信息、日志输出、用户交互都靠它们。你想想看,要是连输出都搞不定,后面还怎么玩?
printf:格式化输出的「瑞士军刀」
printf 这函数,我估计你刚学 C 语言第一天就见过。但说实话,很多人用了好几年,也就只会个 %d、%s。其实它的能力远不止这些。
先看个最简单的例子:
#include <stdio.h>
int main() {
int age = 25;
float height = 1.78f;
char name[] = "张三";
printf("姓名:%s,年龄:%d,身高:%.2f 米\n", name, age, height);
return 0;
}
输出结果:
姓名:张三,年龄:25,身高:1.78 米
嗯,这里要注意。printf 的格式控制符,说白了就是告诉程序「我要怎么展示这些数据」。%d 表示整数,%f 表示浮点数,%s 表示字符串。%.2f 的意思是保留两位小数。
核心要点:printf 的第一个参数是格式字符串,后面的参数是要输出的值。格式字符串中的 % 占位符必须与后面的参数一一对应,个数、类型都不能错。
我在项目中遇到过一个问题:有个同事把 %d 写成了 %s,结果打印出来的数字变成了乱码。排查了半天才发现是格式符写错了。所以啊,写 printf 的时候一定要仔细检查格式符和参数是否匹配。
printf 的进阶用法
除了基本的格式符,printf 还支持一些高级功能。比如:
- 宽度控制:%10d 表示至少占 10 个字符宽度,右对齐
- 精度控制:%.5f 表示保留 5 位小数
- 左对齐:%-10s 表示左对齐,占 10 个字符宽度
- 补零:%08d 表示用 0 补足 8 位
举个例子:
int num = 42;
printf("默认:%d\n", num);
printf("宽度10:%10d\n", num);
printf("左对齐:%-10d\n", num);
printf("补零:%08d\n", num);
输出:
默认:42
宽度10: 42
左对齐:42
补零:00000042
小技巧:调试时我喜欢用 %p 来打印指针地址,用 %zu 来打印 size_t 类型的值。这些冷门格式符关键时刻能救命。
scanf:格式化输入的「双刃剑」
scanf 和 printf 是兄弟俩,一个负责输出,一个负责输入。但 scanf 比 printf 坑多得多。
int age;
printf("请输入你的年龄:");
scanf("%d", &age); // 注意 & 符号
为什么 scanf 需要 & 符号?因为 scanf 要修改变量的值,必须传入变量的地址。这是 C 语言的「传址调用」。很多新手在这里翻车,忘了加 &,程序直接崩溃。
警告:scanf 的返回值一定要检查!它返回成功读取的项数。如果用户输入了非数字字符,scanf 会返回 0,并且输入缓冲区里还残留着那个非法字符,下次调用 scanf 会直接失败,形成死循环。
我曾经遇到过一个线上 bug,用户输入了一个字母,程序就卡死了。排查后发现是 scanf 没有处理输入失败的情况。从那以后,我写 scanf 必做两件事:检查返回值、清空输入缓冲区。
安全的写法:
int age;
printf("请输入你的年龄:");
if (scanf("%d", &age) != 1) {
printf("输入无效,请重新输入!\n");
// 清空输入缓冲区
while (getchar() != '\n');
}
getchar 与 putchar:字符级 IO 的「老黄牛」
getchar 和 putchar 是 C 语言最原始的字符输入输出函数。它们一次只处理一个字符,但胜在简单、可控。
char ch;
printf("请输入一个字符:");
ch = getchar();
printf("你输入的是:");
putchar(ch);
putchar('\n');
getchar 的返回值是 int 类型,不是 char。为什么?因为 getchar 可能返回 EOF(通常是 -1)表示文件结束或错误。如果用 char 来接收,EOF 可能被截断,导致无法正确判断。
重要:getchar 的返回值一定要用 int 接收!这是 C 语言经典陷阱之一。
正确的写法:
int ch; // 注意是 int,不是 char
while ((ch = getchar()) != EOF) {
putchar(ch);
}
这个模式在文件复制、流处理中非常常见。我写网络协议解析时,经常用这个模式逐字节读取数据流。
gets 与 puts:危险的「老古董」
puts 还算安全,它自动在末尾加换行符,比 printf 简单。但 gets 就是个定时炸弹。
char buffer[10];
puts("请输入一段文字:");
gets(buffer); // 危险!没有长度限制
printf("你输入的是:%s\n", buffer);
这段代码有什么问题?如果用户输入了超过 9 个字符(加上结尾的 '\0' 共 10 个),gets 会继续往 buffer 后面写,覆盖掉栈上的其他数据。这就是经典的「缓冲区溢出」漏洞。
严重警告:gets 函数在 C11 标准中已经被正式移除!永远不要使用 gets。如果你在代码里看到 gets,赶紧换成 fgets。
安全的替代方案:
char buffer[10];
puts("请输入一段文字:");
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
printf("你输入的是:%s\n", buffer);
}
fgets 会限制读取的长度,不会超出缓冲区大小。而且它会把换行符也读进来,需要你自己处理。
我曾经接手过一个遗留项目,里面到处都是 gets。我花了一周时间把所有 gets 替换成 fgets,并修复了相关的缓冲区问题。那段时间我做梦都在改代码……
总结一下
| 函数 | 功能 | 安全性 | 推荐用法 |
|---|---|---|---|
| printf | 格式化输出 | 安全 | 注意格式符与参数匹配 |
| scanf | 格式化输入 | 中等 | 检查返回值,处理输入错误 |
| getchar | 读取单个字符 | 安全 | 用 int 接收返回值 |
| putchar | 输出单个字符 | 安全 | 直接使用 |
| gets | 读取一行字符串 | 极不安全 | 禁止使用,用 fgets 替代 |
| puts | 输出一行字符串 | 安全 | 自动加换行,比 printf 简洁 |
说白了,stdio.h 的这些函数,用好了是利器,用不好就是坑。尤其是 gets 和 scanf,一定要小心再小心。我个人的建议是:能用 fgets 就别用 scanf,能用 fgets 就别用 gets。安全第一,功能第二。
好了,这一章就聊到这儿。记住我今天说的这些坑,写代码的时候多留个心眼,能省不少调试时间。