2、stdio.h 核心函数(上):printf格式化输出、scanf格式化输入、getchar与putchar、gets与puts的隐患

stdio.h 这个头文件,说白了就是 C 语言和外界打交道的「嘴巴」。你写的程序要跟用户说话,用户要告诉程序点什么,都得靠它。今天咱们就聊聊这张嘴怎么用,以及怎么用好它。

我个人习惯,拿到一个新项目,第一件事就是先把 stdio.h 相关的函数理一遍。为什么?因为 90% 的调试信息、日志输出、用户交互都靠它们。你想想看,要是连输出都搞不定,后面还怎么玩?

stdio.h 核心函数知识体系 stdio.h 核心函数 输出函数 printf putchar puts(有隐患) 输入函数 scanf getchar gets(有隐患) 红色标注的函数存在安全隐患,建议避免使用

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。安全第一,功能第二。

好了,这一章就聊到这儿。记住我今天说的这些坑,写代码的时候多留个心眼,能省不少调试时间。

专注资料整理