生命之风的低语
Whispers in the Wind of Life.

C语言printf函数详解..

2025-10-11 19:05:01

1.printf函数解析

前面我们有讲过printf函数的格式为:

printf(“占位1 占位2 占位3……”, 替代1, 替代2, 替代3……);

今天我们进一步深入的解析一下这个函数

2.printf函数的特点

1.printf函数是一个变参函数(即参数的数量和类型都不确定)

2.printf函数的第一个参数是字符串

3.printf函数的第一个字符串参数中的具体内容就是需要打印的字符以及需要被替代的占位符

4.printf函数第一个参数之后的参数依次替代占位符

5.printf函数中占位和替代的数量和类型对应

3.变参函数

int main(){

printf("%c\n", 'a');// a

printf("%c %d\n", 'b', 10);// b 10

return 0;

}

4.第一个参数必须是字符串

int main(){

printf("the first argument must be string\n");

return 0;

}

5.第一个参数包含输出字符以及占位符

int main(){

printf("the first argument includes %s and %s\n", "character", "placeholder");

return 0;

}

后续的两个特点我就不一一阐述了

2.整型占位符

1.有符号整型的类型提升

在C语言中 有符号整型遵循一元数字提升的原则 即比int类型小的类型诸如char和short类型的一元数字都会自动提升为int类型

下面这段代码中 char类型的字节原本为1 但是由于进行了一元数字提升 导致变成了int类型 所以打印的结果是为4

int main(){

char a = 'a';

printf("%zu\n", sizeof(+a));// 4

return 0;

}

但是还有一个疑问 就是在visual studio中 long和int类型占用的字节数是一致的 既然这些小于等于int类型的类型都使用%d作为占位符的话 那么long这种等于int的类型适合使用%d作为占位符吗 其实是不妥当的

因为前面讲过C标准没有明确规定类型占用的字节数 所以不同的平台和编译器有着自己的标准 long类型占用的字节数在不同平台和编译器下是不尽相同的

为了体现程序的可移植性 防止代码打印结果产生偏差 我们对于long类型的占位符尽量使用%ld

int main(){

long l = 11;

printf("%ld\n", l);

return 0;

}

2.无符号整型的类型提升

其实他也同有符号整型的类型提升一样遵循一元数字提升 只不过区别在于说无符号整型涵盖的范围就是无符号的整数 而有符号整型涵盖的范围就是有符号的整数

说白了就是 比unsigned int小的无符号整型会自动提升为unsigned int类型 诸如unsigned char、unsigned short在内的一元数字会自动提升为unsigned int类型

下面这段代码可以体现无符号整型的一元数字提升

int main(){

unsigned char ch = 'a';

printf("%zu\n", sizeof(+ch));// 4

return 0;

}

下面这段代码则体现了无符号整型的占位符

int main(){

unsigned char ch = 'a';

unsigned int i = 11;

unsigned long l = 11;

unsigned long long ll = 11;

printf("%u %u %lu %llu\n", ch, i, l, ll);

return 0;

}

3.printf函数中涉及到的整型类型提升

在printf函数中 char、short、int这些小于等于int类型的类型会自动提升为int类型

4.printf函数中涉及到的浮点型类型提升

在printf函数中 float、double这些小于等于double类型的类型会自动提升为double类型

3.转换规范

其实占位符这种说法并不准确 较为准确的说法是转换规范 我们称%d这种形式的叫做转换规范

我可以就此举一个例子:% -#0 12 .4 l d

我们可以发现真正的转换规范包含了很多东西 不仅仅是一个字母

1.转换规范的组成

转换规范以%开头 然后依次跟着以下这些元素:

1.零个或者多个标志字符 比如:- # 0

2.一个可选的十进制整型常量表示的最小字符宽度

3.一个可选的.开头的精度 后跟十进制整形常量

4.一个可选的长度指示符

5.一个字符包含的转换操作 诸如:d u f这些字符

2.转换操作

转换操作会根据不同的转换方式 截取n个字节的二进制数据 转换成不同类型的字符或者字符串

我们从中就可以解释前面的一个疑惑:

double和float类型都可以使用占位符%f 原因在于%f是截取sizeof(double)字节的二进制数据 然后转换为双精度浮点型的字符

1.转换操作d

对于char、short类型的整型来说 通过转换操作d是可以被正确打印的 但是对于long或者long long类型来说 由于他们可能存在int类型范围之外的数据 所以通过这个转换操作显然是不能够被正确打印的 所以对于这种比int大的整型来说 采用他们各自相应的转换操作才是最合适的做法

int main(){

char c1 = 127;// char类型的最大数据

short s1 = 32767;// short类型的最大数据

char c2 = -128;// char类型的最小数据

short s2 = -32768;// short类型的最小数据

long long ll1 = 2200000000;// 超出int类型范围的数据

long long ll2 = 2100000000;// 包含在int类型范围的数据

printf("%d\n", c1);// 127

printf("%d\n", s1);// 32767

printf("%d\n", c2);// -128

printf("%d\n", s2);// -32767

printf("%d\n", ll1);// -2094967296

printf("%d\n", ll2);// 2100000000

return 0;

}

2.转换操作u

int main(){

unsigned char c1 = 255;// unsigned char的最大数据

unsigned short s1 = 65535;// unsigned short的最大数据

unsigned char c2 = 0;// unsigned char的最小数据

unsigned short s2 = 0;// unsigned short的最小数据

unsigned long long ll1 = 4294967295;// unsigned long long类型中包含在unsigned int类型中的数据

unsigned long long ll2 = 4300000000;// unsigned long long类型中不包含在unsigned int类型中的数据

printf("%u\n", c1);// 255

printf("%u\n", s1);// 65536

printf("%u\n", c2);// 0

printf("%u\n", s2);// 0

printf("%u\n", ll1);// 4294967295

printf("%u\n", ll2);// 5032704

return 0;

}

3.转换操作d和u的错误搭配

由于有符号int和无符号int的取值范围不一致 导致当我们错误搭配他们的时候 可能会产生不符合预期的结果

如果当错误搭配时 数据处于两者的共同范围之内 那么打印结果符合预期

但是如果不处于两者的共同范围之内的话 那么打印的结果就不符合预期

int main(){

unsigned int u1 = 2100000000;// 该数据处于有符号int和无符号int的共同范围之内

unsigned int u2 = 2200000000;// 该数据并不处于两者的共同范围之内

printf("%d\n", u1);// 2100000000

printf("%d\n", u2);// -2094967296

return 0;

}

4.注意数据类型的取值范围

如果数据超出了类型本身的取值范围 那么无论如何打印 都无法获取正确结果

int main(){

char ch = 200;// 200超出了char类型本身的数据范围 -128~127

printf("%d\n", ch);// -56

return 0;

}

5.转换操作c

打印ASCII码表中数值对应的字符

int main(){

char ch = 65;

short s = 66;

int i = 67;

long l = 68;

long long ll = 69;

printf("%c %c %c %c %c\n", ch, s, i, l, ll);

return 0;

}

6.转换操作f、e、E

截取sizeof(double)个字节的二进制数据 然后转换为双精度浮点型的字符

int main(){

float f = 12.34;

double d = 12.34;

printf("%f\n", f);// 12.34

printf("%f\n", d);// 12.34

printf("%e\n", f);// 1.234000e+01

printf("%E\n", f);// 1.234000e+01

return 0;

}

7.转换操作o、x、X

截取sizeof(int)个字节的二进制数据 然后将其转换为无符号八进制、十六进制形式整数的字符

int main(){

unsigned int i = 20;

printf("%u %o %x %X", i, i, i, i);// 20 24 14 14

return 0;

}

8.转换操作s

截取sizeof(char*)个字节的二进制数据 即地址值占用的字节数 接着以这个地址开始输出字符串

int main(){

printf("%s\n", "Hello World!");

return 0;

}

3.长度指示符

int main(){

long l = 2100000000;// 该数据处于long类型的范围之内

long long ll = 2200000000;// 该数据处于long long类型范围之内

printf("%d\n", l);// 2100000000 由于long类型和int类型占有的字节数在该编译器和平台上是一样的 所以转换操作d所截取的字节数也是一样的 所以就可以正确表示long类型数据

printf("%d\n", ll);// -2094967296 但是由于long long类型比int类型大得多 所以截取的时候只能够截取long long类型数据的一部分 打印的结果不能正确显示long long类型数据

return 0;

}

由上面的案例 我们可以知道 既然用%d这个转换规范无法正确long long类型的数据的话 那么就要引入长度指示符才能解决这个问题了

何为长度指示符 前面有讲过 他是一个可选的字符 但是具体的作用是什么

其实正如这个问题所指出的那样 既然我们无法用截取字节数较小的转换操作来正确表示一个占用字节数较大的类型的话 那么就需要依靠长度指示符来进行截取字节数的扩宽或者缩小了

所谓长度指示符 正是为了扩宽和缩小转换操作所截取的字节数:

在转换操作之前加上一个l 表示将转换操作所截取的字节数上升为更高一级的类型长度

在转换操作之前加上一个h 表示将转换操作所截取的字节数下降为更低一级的类型长度

以下列举了一些长度指示符和转换操作搭配的组合

通过上述这个理论 我们就可以解决刚才这个问题了

我们可以使用%lld来表示long long类型的数据

int main(){

long long ll = 2200000000;

printf("%lld\n", ll);// 2200000000 ok

printf("%ld\n", ll);// -2094967296 error

return 0;

}

再举个例子

int main(){

long long ll = 2200000000;

unsigned long long ull = 0xffffffffffffffff;

printf("%lld\n", ll);// 2200000000 %lld将原本的sizeof(int)扩宽为了sizeof(long long)

printf("%llx\n", ull);// ffffffffffffffff %llx将原本的sizeof(int)扩宽为了sizeof(unsigned long long)

return 0;

}

再举一个例子

int main(){

unsigned int i = 0x12345678;

printf("%hx\n", i);// 0x5678

printf("%hhx\n", i);// 78

return 0;

}

另外前面还讲到一个特殊的长度指示符z 这是用于指示size_t类型的 而size_t类型是sizeof()函数的返回值类型 这个size_t可不是具体的类型 他只不过是具体类型的别名罢了 至于是什么具体类型的别名 这个是由编译器的具体实现所决定的

但是当你想要表示sizeof()的占位符时 显然使用%zu是比%d更为妥当的

int main(){

printf("%zu\n", sizeof(int));// 4

return 0;

}

4.精度

所谓精度 其实就是一个.开头 后接一个可选的十进制整型

对于不同的转换操作 精度控制的对象是不一样的:

针对d、i、u、o、x、X这些转换操作而言 精度所控制的是数字的最小位数 不足将补齐

而针对e、E、f这些转换操作而言 精度控制的则是小数位数 可能会涉及四舍五入

int main(){

int i = 123;

double df = 123.456789;

printf("%.6d\n", i);// 000123 限制数字的位数为6位 由于位数不够 所以在前面进行0的补齐

printf("%f\n", df);// 123.456789 没有限制 想几位就几位

printf("%.0f\n", df);// 123 限制小数位数为0位

printf("%.4f\n", df);// 123.4568 限制小数位数为4位

return 0;

}

5.最小字段宽度

所谓最小字段宽度 指的就是一个可选的十进制整型常量

他控制的是最小的字符长度 不足用空格补齐

int main(){

int i = 123;

double df = 123.456789;

printf("%2d\n", i);// 123 限制的最小长度为2位 超出了 不做任何处理

printf("%6d\n", i);// 123 限制的最小长度为6位 没有超出 在其前面补3个空格

printf("%12f\n", df);// 123.456789 限制的最小长度为12位 没有超出 在其前面补2个空格

return 0;

}

6.标志

标志0控制补齐方式为0 具体控制的对象是最小字段宽度 因为精度的默认补齐方式就是0

int main(){

int i = 123;

printf("%6d\n", i);// 123 最小字段宽度的默认补齐方式是为空格

printf("%06d\n", i);// 000123 但是添加了标志0以后 默认补齐方式变成了0

return 0;

}

标志-控制对齐方式为左对齐

int main(){

int i = 123;

printf("%6d\n", i);// 123

printf("%-6d\n", i);// 123

printf("%-06d\n", i);// 123

return 0;

}

标志+控制符号的产生

int main(){

int i = 123;

printf("%6d\n", i);// 123

printf("%+6d\n", i);// +123

printf("%-+6d\n", i);// +123

return 0;

}

标志#控制八进制数据前加0 十六进制数据前加0x

int main(){

int i = 20;

printf("%o\n", i);// 24

printf("%#o\n", i);// 024

printf("%x\n", i);// 14

printf("%#x\n", i);// 0x14

return 0;

}