目录
printf 到底做了什么
原型
c
#include <stdio.h>
int printf(const char *format, ...);
※const char *format —— 格式字符串。
const表示 printf 承诺不改它。为什么加?因为你可以传字符串字面量"hello",字面量存在只读区,改了会崩。- 它是唯一一个位置固定的参数。所有可变参数都排在它后面。
※... —— 省略号,C 语言里叫可变参数(variadic)。
printf 不知道你会传几个参数、什么类型,它连"有多少个参数"都不知道。它只能靠你写在 format 里的 % 数量来推断。
这就埋下了所有安全问题的种子:说明符和实参对不上时,printf 没有任何办法发现。它不会报错,只会继续往后读内存。
※返回 int
工作流程
plaintext
读取 format 字符串,从左往右走
│
├─ 普通字符(如 'h' 'e' 'l')→ 直接输出
│
└─ 遇到 '%'
├─ 解析后面的格式说明符(标志/宽度/精度/长度/转换符)
├─ 到可变参数列表里【按顺序】取下一个参数
├─ 按说明符的规则把这个参数转成文本
└─ 输出
"按顺序取"这四个字是整个 printf 的核心机制。 它没有名字、没有键,全靠位置对应。
所以:
c
#include <cstido>
#include <iostream>
using namespce std
int main
{
printf("%d %s\n", 42, "hello"); /* ✓ 顺序对上 */
printf("%d %s\n", "hello", 42); /* ✗ 顺序反了,输出是乱码/崩溃 */
}
第二个不会编译报错(因为都是可变参数,类型检查宽松),
但运行时会按 %d 去读一个字符串指针的地址值——输出一个巨大的数字,然后按 %s 去读一个整数当地址 → 崩溃。
内部实现:四个宏
printf 靠 <stdarg.h> 提供的机制"往后取参数":
c
#include <stdarg.h>
void my_printf(const char *fmt, ...)
{
va_list args; /* 参数游标 */
va_start(args, fmt); /* 定位到第一个可变参数 */
int x = va_arg(args, int); /* 取一个,游标后移 */
char *s = va_arg(args, char *); /* 再取一个,游标再后移 */
va_end(args); /* 收尾 */
}
va_arg(args, int) 的意思是:
“我认为下一个参数是 int,按 int 的大小读出来,然后把游标往后挪 sizeof(int) 个字节。”
如果它以为的类型和实际类型不一致——比如它按 4 字节读,实际那 8 字节是 double——游标偏移就错了,后面所有参数全部错位。
这就是"说明符必须和实参类型匹配"的根本原因。
为什么有"默认实参提升"
在参数交给 printf 之前,C 语言会先做一次自动转换:
| 原始类型 | 提升成 | 原因 |
|---|---|---|
char / signed char / unsigned char | int | CPU 寄存器最小操作单位通常是 int |
short / unsigned short | int | 同上 |
float | double | 精度换一致性 |
规则名字:默认实参提升(default argument promotions)。
这条规则解释了三件事:
① 为什么 printf("%f", 3.14f) 不报错
3.14f 是 float,传进去之前已经变成 double。
%f 期待 double,正好对上。
② 为什么 %c 对应的是 int 而不是 char
看第三节那张表——%c 那一行写的是"int(提升后)"。因为任何 char 传进来时都已经是 int 了。%c 做的事是"取这个 int 的低 8 位,当字符输出"。
所以这个能跑:
c
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
printf("%c\n", 65); /* 输出 'A' —— 65 是 int,%c 取低 8 位当字符 */
}
③ 为什么 %02X 印 char 会多出 6 个 F
char c = 0xC8; → 提升成 int 时,因为 char 有符号、值是负数,做的是符号扩展(高位补 1)→ 位模式变成 0xFFFFFFC8。
这不是 printf 的错,是提升这一步干的。
可变参数与提升机制
刚刚已经讲了机制,这里补充几个能实际验证的实验。
代码观察
c
#include <stdio.h>
int main(void)
{
char c1 = 1, c2 = 2;
short s1 = 1, s2 = 2;
float f1 = 1.0f, f2 = 2.0f;
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(c1 + c2) = %zu\n", sizeof(c1 + c2));
printf("sizeof(s1 + s2) = %zu\n", sizeof(s1 + s2));
printf("sizeof(f1 + f2) = %zu\n", sizeof(f1 + f2));
return 0;
}
在 MSVC 上(x64):
plaintext
sizeof(char) = 1
sizeof(c1 + c2) = 4 ← 提升成 int 了
sizeof(s1 + s2) = 4 ← 提升成 int 了
sizeof(f1 + f2) = 4 ← 注意这个!float + float 还是 float
前三个是提升,第四个不是。
为什么 float + float 还是 4 字节?
升到 int"这条规则只对"比 int 小的整数类型"生效。 float 不在这个范围里——它比 int 大,不在被提升的行列。
运算符会做"寻常算术转换"(usual arithmetic conversions),规则是"float 不提升成 double"——只有传参时才提升。
但这里并没有经过float的传参:
提升发生在"把一个 float 值交给 printf"的那一刻。sizeof(f1+f2) 交给 printf 的是个 size_t 整数——那个 float 从来没有活着走到 printf 面前。
所以:
c
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
printf("%zu\n", sizeof(float)); /* 4 */
printf("%zu\n", sizeof(3.14f + 3.14f)); /* 4,运算后还是 float */
}
但传进 printf 时它会变成 double。
sizeof 是编译期常量
上面 sizeof(char) 能写成编译期常量,这是它的特性——sizeof 在编译期算完,运行时没有开销。
编译期就能算出结果、不需要等程序跑起来的值。
c
sizeof(char) /* 4 —— 编译器数一下就知道 */
#define N 10 /* 10 —— 预处理时直接替换 */
3 + 5 /* 8 —— 常量折叠,编译时算好 */
程序运行时这些不需要再做任何计算——编译器已经把结果写死在生成的代码里了。
一个验证:
c
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int arr[10];
printf("%zu\n", sizeof(arr)); /* 40 —— 整个数组 40 字节 */
}
在函数里就不一样了:
c
#include <iostream>
#include <cstdio>
using namespace std;
void f(int arr[10])
{ /* 这个 [10] 是骗人的 */
printf("%zu\n", sizeof(arr)); /* 8 —— 退化成指针了! */
}
int main()
{
int arr[10];
f(arr);
return 0;
}
数组作为参数传递时会退化成指针,sizeof 就量不出数组长度了。
提升带来的实际体现
c
#include <stdio.h>
int main()
{
char c = (char)0xC8; /* 位模式 1100 1000 */
printf("原值(提升后)= %d\n", c); /* -56 */
printf("十六进制 = %X\n", c); /* FFFFFFC8 */
printf("转无符号后 = %X\n", (unsigned char)c); /* C8 */
return 0;
}
注意第三个:(unsigned char)c 先做强制转换,把它变回无符号字节(值 200),然后再提升——这次是零扩展,高位补 0,所以印出来干净。
这一行的差别,就是"先转再传"和"直接传"的差别。
printf用法总览
#include <stdio.h>
int main(void)
{
int i = -42;
unsigned int u = 3000000000u;
double d = 3.14159;
char c = 'A';
const char *s = "hello"; /* ← 这里有 const */
int p = 42;
printf("========== 整数类 ==========\n");
printf("%%d = [%d]\n", i);
printf("%%u = [%u]\n", u);
printf("%%o = [%o]\n", p);
printf("%%x = [%x]\n", p);
printf("%%X = [%X]\n", p);
printf("%%#x = [%#x]\n", p);
printf("%%#o = [%#o]\n", p);
printf("\n========== 浮点类 ==========\n");
printf("%%f = [%f]\n", d);
printf("%%.2f = [%.2f]\n", d);
printf("%%.0f = [%.0f]\n", d);
printf("%%10.3f = [%10.3f]\n", d);
printf("%%-10.3f = [%-10.3f]\n", d);
printf("%%e = [%e]\n", d);
printf("%%E = [%E]\n", d);
printf("%%g = [%g]\n", d);
printf("\n========== 字符与字符串 ==========\n");
printf("%%c = [%c]\n", c);
printf("%%s = [%s]\n", s);
printf("%%10s = [%10s]\n", s);
printf("%%-10s = [%-10s]\n", s);
printf("%%.3s = [%.3s]\n", s);
printf("%%10.3s = [%10.3s]\n", s);
printf("\n========== 指针与百分号 ==========\n");
printf("%%p = [%p]\n", (void *)&i);
printf("%%%% = [%%]\n");
printf("\n========== 宽度的各种写法 ==========\n");
printf("%%5d = [%5d]\n", p);
printf("%%.5d = [%.5d]\n", p);
printf("%%05d = [%05d]\n", p);
printf("%%-5d = [%-5d]\n", p);
printf("%%+d = [%+d]\n", p);
printf("%% d = [% d]\n", p);
printf("\n========== 宽度用 * 动态指定 ==========\n");
printf("%%*d = [%*d]\n", 8, p);
printf("%%-*d = [%-*d]\n", 8, p);
return 0;
}
格式说明符全表
完整语法
一个格式说明符最多六个部分,顺序不能乱:
plaintext
% [标志] [宽度] [.精度] [长度修饰符] 转换符
例子:
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
printf("[%#010.5llx]\n", 42LL);
}
中间的[]可加可不加,只是为了更好的区分边界
这次每一部分都起作用了:
plaintext
% # 0 10 .5 ll x
│ │ │ │ │ │
│ │ │ │ │ └─ 十六进制小写
│ │ │ │ └────── long long
│ │ │ └─────────── 精度 5(不足 5 位补 0)
│ │ └──────────────── 宽度 10
│ └──────────────────── 0:空位补 0
└───────────────────────── #:加 0x 前缀
只有 % 和转换符是必须的,其余都是可选。
逐段看:
| 部分 | 取值范围 | 说明 |
|---|---|---|
| 标志 | - + 空格 # 0 | 可多个,顺序任意 |
| 宽度 | 数字,或 * | 最小输出宽度 |
| 精度 | . 后跟数字,或 .* | 含义随转换符变 |
| 长度修饰符 | hh h l ll L z j t | 实参的真实宽度 |
| 转换符 | 见下文表 | 怎么把值转成文本 |
标志
| 标志 | 名 | 作用 | 例子 |
|---|---|---|---|
- | 左对齐 | 内容靠左,右侧补 | [%-5d] → [42 ] |
+ | 强制符号 | 正数也显示 + | [%+d] → [+42] |
| 空格 | 符号占位 | 正数前留一个空格 | [% d] → [ 42] |
# | 替代形式 | 加进制前缀/强制小数点 | [%#x] → [0x2a] |
0 | 零填充 | 用 0 代替空格填充 | [%05d] → [00042] |
示例
-
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%-5d%d\n",a,b);
printf("%-4d%d\n",a,b);
printf("%-3d%d\n",a,b);
printf("%-2d%d\n",a,b); //没效果
printf("%-1d%d\n",a,b); //没效果
printf("%-d%d\n", a,b); //没效果
return 0;
}
-是在扣除原数位数之后,再进行的补位
10 -> 两位 -> -2/-1 -> 没有效果
+
#include <stdio.h>
int main()
{
int a = 10;
int b = -20;
char c = 'a';
printf("%+d %+d\n",a,b);//对负数也没用
printf("%+c", c);//对字符没用
return 0;
}
只会对正数前添加+
(空格)
#include <stdio.h>
int main()
{
int a = 10;
int b = -20;
char c = 'a';
printf("%d %d\n",a,b);
printf("%d %d\n",a,b);
printf("%d %d\n",a,b);
printf("%d %d\n",a,b);
return 0;
}
#
#include <stdio.h>
int main()
{
int a = 10;
int b = -20;
printf("[%.0f] [%#.0f]\n", 3.0, 3.0); /* [3] [3.] 小数点被强行留住 */
printf("[%g] [%#g]\n", 3.0, 3.0); /* [3] [3.00000] 尾随的 0 不再被砍掉 */
//%g 平时会删掉尾随的 0,# 让它保留——保证输出能被"读回"成原始精度。
printf("[%x] [%#x]\n", 255, 255); /* [ff] [0xff] */
printf("[%X] [%#X]\n", 255, 255); /* [FF] [0XFF] */
printf("[%o] [%#o]\n", 42, 42); /* [52] [052] */
//特殊情况:
//值为 0 时不加前缀
printf("[%#x]\n", 0); /* [0] 不是 [0x0] */
printf("[%#o]\n", 0); /* [0] 不是 [00] */
//对 %d %u %s %c 完全无效
printf("[%#d]\n", 42); /* [42] */
//十进制没有前缀可加
}
# 是"把默认省略的信息补回来"——十六进制的 0x、八进制的 0、浮点的小数点。
0
通常和其他用法搭配使用
#include <stdio.h>
int main()
{
int a = 10;
int b = -20;
printf("%05d\n", a);
printf("%04d\n", a);
printf("%03d\n", a); //无效果
printf("%02d\n", a); //无效果
printf("%01d\n", a); //无效果
printf("%0d\n", a); //无效果
printf("%-05d\n", a); //无效果
}
同理,“0”也是扣除原数位数后再填0的,但与“-搭配”又不一样
- 和 0 同时用会怎样?
c
#include <stdio.h>
int main()
{
printf("[%-05d]\n", 42); /* [42 ] ← 0 被忽略 */
return 0;
}
左对齐时右边补的是空格,0 标志失效。
因为"左对齐 + 补 0"会让数字看起来像 42000,语义混乱。标准规定这种情况下忽略 0。
+ 和空格标志的区别:
c
#include <stdio.h>
int main()
{
printf("% d % d\n", 42, -42); /* 42 -42 ← 正数前有个空格 + 优先 */
return 0;
}
两者互斥,同时出现时 + 优先。
宽度
含义:输出的最小字符数。不够就补,超了就照印。
其用法常和“0” “-”相搭配
c
#include <stdio.h>
int main()
{
printf("[%5d]\n", 42); /* [ 42] 补空格到 5 格 */
printf("[%5d]\n", -42); /* [ -42] ← 注意负号占一格 */
printf("[%5d]\n", 1234567); /* [1234567] 超了,不减 */
printf("[%-5d]\n", 42); /* [42 ] 左对齐 */
printf("[%05d]\n", 42); /* [00042] 补 0 */
printf("[%05d]\n", -42); /* [-0042] ← 负号在前,0 补在后面 */
return 0;
}
最后一行是个细节:%05d 印负数时,0 补在负号后面,不是前面。
因为 -0042 比 000-42 更符合"数值表示"的直觉。
宽度可以是 *,从参数里取:
c
#include <stdio.h>
int main()
{
int w = 8;
printf("[%*d]\n", w, 42); /* [ 42] 宽度由参数给 */
return 0;
}
* 机制在写动态对齐时有用——比如根据内容长度动态决定列宽。
精度:三种含义
同一个 .N,对三种类型是三种完全不同的意思。 这是 printf 里最容易搞混的一点。
① 对浮点数:小数点后保留几位
c
printf("%.0f\n", 3.14159); /* 3 */
printf("%.2f\n", 3.14159); /* 3.14 */
printf("%.10f\n", 3.14159); /* 3.1415901184 ← 注意!后面是误差 */
最后一行值得停一下。
在有的编译器中:
3.14159 存在 double 里,实际是 3.14158999999999988262...。
.10f 把它"真实的二进制值"印出来了。
为什么?详见浮点数的存储:IEEE‑754 _csdn中对ieee754浮点的定义-CSDN博客
② 对整数:最少几位,不够补 0
c
#include <stdio.h>
int main()
{
printf("%5d\n", 42); /* 42 ← 宽度:补空格 */
printf("%.5d\n", 42); /* 00042 ← 精度:补 0 */
return 0;
}
这两个写法长得很像,效果完全不同。
%5d是宽度,补的是空格%.5d是精度,补的是 0
为什么整数需要"精度"这个概念?典型场景是日期和时间:
c
#include <stdio.h>
int main()
{
printf("%04d-%02d-%02d\n", 2026, 9, 15); /* 2026-09-15 */
printf("%02d:%02d:%02d\n", 9, 5, 3); /* 09:05:03 */
return 0;
}
注意这里用的是 %02d(宽度补 0),不是 %.2d。
对于数字补 0,习惯上写宽度形式 %02d 更常见。两者在这里效果相同(都是补 0 到 2 位),但 %02d 更通用——因为当数值超过 2 位时,%02d 能正常印出 3 位数,
2d 也一样(精度是下限不是截断)。
③ 对字符串:最多输出几个字符
c
#include <stdio.h>
int main()
{
printf("%.3s\n", "abcdef"); /* abc 截断到 3 个 */
printf("%5.3s\n", "abcdef"); /* [ abc] 先截断成 abc,再补宽到 5 */
return 0;
}
精度对字符串是"上限"(截断)
对整数和浮点是"下限/位数"。这是精度唯一表现出"限制"作用的场景。
记忆:
plaintext
float : .N = 小数点后 N 位
int : .N = 至少 N 位,补 0
string : .N = 最多 N 个字符(截断)
长度修饰符
为什么需要:long 和 int 大小可能不同,size_t 和 int 也可能不同(因为不同的编译器不一样)
%d 只保证按 int 去读,如果实参是 long long(8 字节),格式符却按 4 字节读,就会错位。
| 修饰符 | 用在哪 | 实参类型 | MSVC 上字节数 |
|---|---|---|---|
hh | %hhd %hhu %hhx | char 类 | 1 |
h | %hd %hu %hx | short 类 | 2 |
| (无) | %d %u %x | int 类 | 4 |
l | %ld %lu %lx | long 类 | Windows: 4 |
ll | %lld %llu | long long 类 | 8 |
z | %zu %zd | size_t / ssize_t | 8 |
j | %jd | intmax_t | 8 |
t | %td | ptrdiff_t | 8 |
L | %Lf %Lg | long double | 8(MSVC 上等于 double) |
注意 l 那一行的加粗部分。 这是 Windows 特有的坑:
plaintext
数据类型大小对比(x64)
Windows (LLP64) Linux (LP64)
int 4 int 4
long 4 ← 注意 long 8
long long 8 long long 8
size_t 8 size_t 8
指针 8 指针 8
LLP64 的意思是:long 和指针都是 64 位?不——是 Long 和 Pointer 都是 64 位,但 Long 和 int 都是 32 位。
准确说 LLP64 = "long long 和 pointer 是 64 位,long 仍然是 32 位"。
实际后果:
c
#include <stdio.h>
int main()
{
long x = 1234567890123; /* ✗ 在 Windows 上溢出,long 只有 4 字节 */
printf("%ld\n", x); /* 输出的值是错的 */
long long y = 1234567890123LL; /* ✓ */
printf("%lld\n", y);
return 0;
}
跨平台写代码时,如果需要 64 位整数,用 long long 或 int64_t,别用 long。
转换符说"怎么读",长度修饰符说"读多少"。少了后者,printf 就用默认宽度(
int)去猜——猜错的时候,它不会报错,只会念出一串别的数字。
转换符全表
| 说明符 | 实参类型 | 输出形式 | 例子 |
|---|---|---|---|
%d %i | int | 十进制有符号 | 42 / -42 |
%u | unsigned int | 十进制无符号 | 4294967295 |
%o | unsigned int | 八进制 | 52 |
%x | unsigned int | 十六进制小写 | 2a |
%X | unsigned int | 十六进制大写 | 2A |
%f | double | 定点小数 | 3.140000 |
%e | double | 科学计数法小写 | 3.140000e+00 |
%E | double | 科学计数法大写 | 3.140000E+00 |
%g | double | 自动选 %f 或 %e | 3.14 |
%a | double | 十六进制浮点(C99) | 0x1.91eb851eb851fp+1 |
%c | int(提升后) | 单字符 | A |
%s | char * | 字符串 | hello |
%p | void * | 指针 | 000000AB1234 |
%n | int * | 不输出,把已输出字符数写进指针 | ⚠️ 危险 |
%% | 无 | 一个字面 % | % |
%n :它不输出任何东西,而是把"到目前为止输出了多少字符"写进你给的 int*。
因为它需要一个可写的指针,在格式化字符串漏洞里常被用来改写内存。除非有极特殊理由,不要用 %n。
正当用途:想知道"已经输出了多少字符",可以这么写:
c
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
int len;
printf("%d-%02d-%02d%n", 2026, 9, 15, &len);
printf("\n总共输出了 %d 个字符\n", len);
return 0;
}
%g
会根据数值大小,自动在 %f 和 %e 之间选一个"更紧凑"的形式:
c
#include <stdio.h>
int main()
{
printf("%g\n", 0.0001); /* 0.0001 */
printf("%g\n", 0.00001); /* 1e-05 ← 太小了,转科学计数法 */
printf("%g\n", 100000.0); /* 100000 */
printf("%g\n", 1000000.0); /* 1e+06 ← 太大了,转科学计数法 */
return 0;
}
规则是:当指数小于 −4 或大于等于精度(默认 6)时,用 %e;否则用 %f。
而且 %g 会去掉尾随的 0:
c
printf("%f\n", 3.140000); /* 3.140000 */
printf("%g\n", 3.14); /* 3.14 ← 尾巴的 0 没了 */
什么时候用 %g:想让输出"看起来干净"、又不想手动算位数的时候。科学计算和数据展示常用它。
%i 和 %d 的区别
在 printf 里完全一样。
在 scanf 里不一样:%d 只读十进制,%i 会根据前缀自动判断进制
(0x 开头当十六进制,0 开头当八进制)。
c
int x ;
scanf("%i", &x); /* 输入 0x1F → x = 31 */
scanf("%d", &x); /* 输入 0x1F → x = 0(读到 0 就停) */
%zu 的重要性
sizeof 返回的类型是 size_t,它不是 unsigned int。在 64 位系统上 size_t 是 8 字节,而 %u 只读 4 字节。
c
#include <stdio.h>
int main()
{
printf("%u\n", sizeof(int)); /* ✗ 在 x64 上可能出错 */
printf("%zu\n", sizeof(size_t)); /* ✓ 正确 */
return 0;
}
规则:凡是 sizeof 的结果,一律用 %zu。
printf的返回值
返回实际输出的字符数(int 类型)。
c
#include <stdio.h>
int main(void) {
int n1 = printf("hello\n");
printf("-> %d\n", n1);
int n2 = printf("%d\n", 42);
printf("-> %d\n", n2);
int n3 = printf("%.5d\n", 42);
printf("-> %d\n", n3);
int n4 = printf("%s\n", "abc");
printf("-> %d\n", n4);
return 0;
}
输出(已核对):
plaintext
hello
-> 6
42
-> 3
00042
-> 6
abc
-> 4
逐个对照:
| 语句 | 实际输出 | 返回 |
|---|---|---|
printf("hello\n") | hello + 换行 | 6 |
printf("%d\n", 42) | 42 + 换行 | 3 |
printf("%.5d\n", 42) | 00042 + 换行 | 6 |
printf("%s\n", "abc") | abc + 换行 | 4 |
三个关键认知:
① 换行符算 1 个字符。 "hello\n" 是 6 个字符,不是 5 个。这一点在算输出长度时经常漏。
② 返回的是"格式化之后"的长度,不是参数原样。 %.5d 传的是 42(2 个字符),但输出 00042(5 个字符),所以返回 6(+换行)。返回值反映的是实际写出去的东西。
③ 出错返回负数。 比如往一个已关闭的流里写:
c
fclose(stdout);
int n = printf("test");
printf 返回值 n 是负数 /* 具体值由实现决定 */
实际用法:一般代码里不检查 printf 的返回值,但在两种场景必须检查:
- 往文件/网络写:可能失败(磁盘满、连接断),要用
fprintf的返回值判断 - 需要知道写出去多长时:比如累加计算总列宽
常见易错点
说明符与实参类型不匹配
c
double d = 3.14;
printf("%d\n", d); /* ✗ */
为什么危险:
printf 按 %d 去读 4 字节。但 double 是 8 字节——它读到的是 double 位模式的前半段,输出一个毫无意义的整数。
这类错误编译器通常能查出来:
plaintext
warning C4477: 'printf' : format string '%d' requires an argument of type 'int',
but variadic argument 1 has type 'double'
务必开警告:MSVC 用 /W4,GCC/Clang 用 -Wall -Wextra。
注意:编译器能查,是因为现代编译器认得 printf,做了特殊处理。你自己的可变参数函数,编译器查不出来。
%s 传了非字符串(会崩)
c
int x = 65;
printf("%s\n", x); /* ✗ 崩溃 */
%s 期待一个指针。它把 x 的值 65(即 0x41)当地址,去读内存 0x00000041 处的字符——段错误。
如果传的是 NULL:
c
char *p = NULL;
printf("%s\n", p); /* ✗ Windows 上输出 (null),Linux 上崩溃 */
MSVC 会友好地输出 (null),但这是实现行为,不是标准保证。别依赖它——Linux 上就是直接崩。
顺序错位
c
printf("%d %s\n", "hello", 42); /* ✗ */
这类错误最阴险:编译器可能不警告(因为都是可变参数),运行时输出一串看似正常的数字和乱码,甚至可能不崩。
%f 与 %lf 的不对称(必记)
float | double | |
|---|---|---|
| printf | %f | %f(%lf 也行,无区别) |
| scanf | %f | %lf |
原因:
- printf 时 float 被提升成 double,所以
%f期待 double 完全正确 - scanf 拿的是指针,没有提升。
%f期待float*,%lf期待double*——这是两个不同的指针类型,不能混用
背这一句:
printf 里
%f通吃,scanf 里%lf专给 double。
格式化字符串漏洞(最严重)
c
char msg[100];
fgets(msg, sizeof(msg), stdin);
printf(msg); /* ✗✗✗ 极度危险 */
如果输入是 %x %x %x %x,printf 会去读参数列表里根本不存在的参数——把栈上的内存当参数打印出来(信息泄露)。
更狠的是 %n:它会往你给的地址写入已输出的字符数。攻击者可以用它改写内存。
正确写法:
c
printf("%s", msg); /* ✓ */
守住这条铁律:
printf 的第一个参数,永远应该是你自己的字符串常量。任何来自外部的字符串,都要用
printf("%s", x)的形式。
%02X 印出 FFFFFFC8
c
char c = (char)0xC8;
printf("%02X\n", c); /* FFFFFFC8 */
printf("%02X\n", (unsigned char)c); /* C8 ← 正确 */
链条:char 有符号 → 0xC8 读作 −56 → 提升成 int 时符号扩展 → 0xFFFFFFC8 → %X 印 8 位。
顺带记住:%02X 的 2 是最少位数,不是最多。想真的截断到 2 位,要自己 & 0xFF。
转载自 CSDN-专业IT技术社区




