UIU114头像
关注
彻底搞明白printf:原理与用法封面图

彻底搞明白printf:原理与用法

目录

printf 到底做了什么

原型

工作流程

内部实现:四个宏

为什么有"默认实参提升"

可变参数与提升机制

代码观察

sizeof 是编译期常量

提升带来的实际体现

printf用法总览

​格式说明符全表

完整语法

标志

示例

- 和 0 同时用会怎样?

宽度

精度:三种含义

① 对浮点数:小数点后保留几位

② 对整数:最少几位,不够补 0

③ 对字符串:最多输出几个字符

长度修饰符

转换符全表

%i 和 %d 的区别

%zu 的重要性

printf的返回值

常见易错点

说明符与实参类型不匹配

%s 传了非字符串(会崩)

顺序错位

%f 与 %lf 的不对称(必记)

​格式化字符串漏洞(最严重)

%02X 印出 FFFFFFC8


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 charintCPU 寄存器最小操作单位通常是 int
short / unsigned shortint同上
floatdouble精度换一致性

规则名字:默认实参提升(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 %hhxchar 类1
h%hd %hu %hxshort 类2
(无)%d %u %xint 类4
l%ld %lu %lxlong 类Windows: 4
ll%lld %llulong long 类8
z%zu %zdsize_t / ssize_t8
j%jdintmax_t8
t%tdptrdiff_t8
L%Lf %Lglong double8(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 %iint十进制有符号42 / -42
%uunsigned int十进制无符号4294967295
%ounsigned int八进制52
%xunsigned int十六进制小写2a
%Xunsigned int十六进制大写2A
%fdouble定点小数3.140000
%edouble科学计数法小写3.140000e+00
%Edouble科学计数法大写3.140000E+00
%gdouble自动选 %f 或 %e3.14
%adouble十六进制浮点(C99)0x1.91eb851eb851fp+1
%cint(提升后)单字符A
%schar *字符串hello
%pvoid *指针000000AB1234
%nint *不输出,把已输出字符数写进指针⚠️ 危险
%%无一个字面 %%

%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 的不对称(必记)

floatdouble
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技术社区

原文链接:https://blog.csdn.net/UIU114/article/details/165446353

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--