本节摘要:printf 与 scanf 是 C 里最常用也最容易被误用的两个函数。它们的机器真相是:调用时参数按调用约定依次入栈或装进寄存器,函数内部按格式串的声明去"取"参数——取多少字节、按什么类型解读,全由格式占位符说了算,运行时没有任何类型核对。理解这一点,格式错配造成的乱码与崩溃就全部可以解释。
上一章说过:内存里只有字节,类型是滤镜。printf 的格式串就是一份滤镜清单——%d 声明"取 4 字节按 int 解读",%f 声明"取 8 字节按 double 解读"(float 参数会自动提升为 double,这是可变参数的规矩)。它与你实际传了什么类型毫无沟通渠道:
#include <stdio.h> int main(void) { long long big = 4294967296LL; /* 2 的 32 次方 */ printf("%d\n", big); /* 按 int 只取低 32 位:输出 0 */ printf("%lld\n", big); /* 正确:输出 4294967296 */ int n = 65; printf("%c\n", n); /* 按 char 解读:输出 A */ printf("%s\n", n); /* 灾难:把 65 当地址去取字符串 */ return 0; }
%s 那行在多数机器上是段错误——把整数 65 当成地址去解引用。printf 没有"防呆":它信任你。这也解释了那条著名的安全防线——永远不要把用户输入直接当格式串:
printf(user_input); /* 危险:输入里的 %s %n 会被 printf 认真执行 */ printf("%s", user_input); /* 正确写法 */
恶意输入里的 %n(把已输出字符数写回参数指向的地址)能把数据写进内存,这是格式串漏洞的基本原理。历史上大量 C 程序因此被攻破。
函数调用时,前几个参数走寄存器(x86-64 下整数用 rdi、rsi 等,浮点用 xmm 系列),多余参数与可变参数部分按平台约定处理。printf 拿到第一个参数(格式串地址),逐字符扫描:
%:解析占位符,按声明从参数区取下一份字节并转换成文本。参数个数与占位符个数不匹配时,多取的部分读的是调用现场附近的随机字节——输出乱码甚至崩溃,且不同优化等级下症状不同。编译器会尽力救你:开 -Wall 编译,占位符与实参类型不匹配基本都会被警告。把警告当错误对待(-Werror)是 C 工程师的自我修养。
scanf 按格式串读输入、把结果写回你给的地址。所以参数必须是地址:
#include <stdio.h> int main(void) { int age; if (scanf("%d", &age) != 1) { printf("输入不是整数\n"); return 1; } printf("明年你 %d 岁\n", age + 1); return 0; }
三条铁律:
%s 没有长度限制,输入超长就冲爆缓冲区——这是蠕虫级漏洞的经典来源。用长度限制版 %19s(配 20 字节缓冲区,留 1 字节给结尾零)。%d 与 %f 的地址类型不同。int 用 &n,double 用 %lf(可变参数里 float 提升为 double,scanf 家族用 l 前缀声明"这是 double 的地址")。写成 %f 配 double 是常见错配。printf 的输出常常不是立刻到屏幕,而是先进一块用户态缓冲区,满了或遇到冲刷条件才真正写设备。冲刷时机三规则:
fflush(stdout) 或程序正常退出时。一段能亲眼看到缓冲存在的程序:
#include <stdio.h> int main(void) { printf("还没换行,先睡 2 秒"); fflush(stdout); /* 注释掉这行再试:输出会迟到 2 秒 */ sleep(2); printf("醒了\n"); return 0; }
把这个程序重定向到文件运行(./a.out > out.txt),行缓冲退化为全缓冲,不冲刷的话输出会更晚才落盘。崩溃时最后几行 printf 输出失踪也是同一原因:段错误直接杀死进程,缓冲区没机会冲刷。调试时怀疑"到底执行到哪了",先 fflush 或者用 stderr(默认不缓冲)打点。

| 占位符 | 解读对象 | 典型用途 |
|---|---|---|
| %d / %i | int | 整数 |
| %u | unsigned int | 无符号整数 |
| %lld | long long | 64 位整数 |
| %f | double(printf) | 浮点输出 |
| %lf | double 的地址(scanf) | 浮点读入 |
| %c | int 按 char | 单字符 |
| %s | char 指针 | 字符串(scanf 必限长) |
| %p | void 指针 | 打印地址 |
| %zu | size_t | sizeof 结果 |
| %x | unsigned int | 十六进制视图 |
| %g | double | 自动取舍的有效数字显示 |
⚠️ 常见坑三连:格式串当用户输入直接用(安全漏洞);
%s读输入不限长(缓冲区溢出);崩溃前 printf 的输出没出现就以为"没执行到"(其实是缓冲没冲刷)。
💡 关键直觉:printf 与 scanf 是"无类型核对的可变参数机器",格式串是唯一的真相声明。开
-Wall让编译器替你核对声明与实参,是成本最低的保险。
%n 可写内存,格式串漏洞真实存在。%s 限长、浮点读入用 %lf。第 2 章结束。接下来进入控制流:if、switch、while 在 CPU 眼里都是"跳转指令",编译器如何安排这些跳转,直接决定了程序的性能特征。