前言
printf 可能是我们在学习 C 语言的过程中最早接触的库函数了. 其基本使用想必我们都已经非常清楚了. 但是下面的这些情况你是否已经清楚地知道了呢?
示例程序
我们来看一个示例程序, 看看你能否对下面的结果输出有非常清晰的认识.
- #include <stdio.h>
- int main(void)
- {
- int a = 4;
- int b = 3;
- int c = a/b;
- float d = *(float*)(&c);
- long long e = 0xffffffffffffffff;
- printf("a/b:%f,a:%d\n",a/b,a,b); // 打印 0
- printf("(float)a/b:%f\n",((float)a)/b); // 打印 1
- printf("(double)a/b:%lf\n",((double)a)/b);// 打印 2
- printf("d:%f\n",d); // 打印 3
- printf("%.*f\n",20,(double)a/b); // 打印 4
- printf("e:%d,a:%d\n",e,a); // 打印 5
- printf("a:%d,++a:%d,a++:%d\n",a,++a,a++); // 打印 6
- return 0;
- }
编译为 32 位程序:
gcc -m32 -o test test.c
在运行之前, 你可以自己先猜想一下打印结果会是什么. 实际运行结果:
- a/b:0.000000,a:3 // 打印 0 的结果
- (float)a/b:1.333333 // 打印 1 的结果
- (double)a/b:1.333333 // 打印 2 的结果
- d:0.000000 // 打印 3 的结果
- 1.33333333333333325932 // 打印 4 的结果
- e:-1,a:-1 // 打印 5 的结果
- a:6,++a:6,a++:4 // 打印 6 的结果
你的猜想是否都正确呢? 如果猜想错误, 那么接下来的内容你就不应该错过了.
你是否会有以下疑问:
0. 打印 0 的 a/b 为什么不是 1,a 为什么不是 4?
1. 打印 1 和打印 2 有什么区别呢?
2. 打印 3 为什么结果会是 0.000000?
3. 打印 4 的结果为什么最后的小数位不对? 其中的 * 是什么意思?
4. 打印 5 中, 为什么 a 的值是 - 1 而不是 4?
5. 打印 6 中, 结果为什么分别是 6,6,4?
在解答这些问题之前, 我们需要先了解一些基本内容.
可变参数中的类型提升
printf 是接受变长参数的函数, 传入 printf 中的参数个数可以不定. 而我们在变长参数探究中说到:
调用者会对每个参数执行 "默认实际参数提升", 提升规则如下:
--float 将提升到 double
--char,short 和相应的 signed,unsigned 类型将提升到 int
也就是说 printf 实际上只会接受到 double,int,long int 等类型的参数. 而从来不会实际接受到 float,char,short 等类型参数.
我们可以通过一个示例程序来检验:
- //bad code
- #include<stdio.h>
- int main(void)
- {
- char *p = NULL;
- printf("%d,%f,%c\n",p,p,p);
- return 0;
- }
编译报错如下:
- printf.c: In function 'main':
- printf.c:5:12: warning: format '%d' expects argument of type 'int', but argument 2 has type 'char *' [-Wformat=]
- printf("%d,%f,%c\n",p,p,p);
- ^
- printf.c:5:12: warning: format '%f' expects argument of type 'double', but argument 3 has type 'char *' [-Wformat=]
- printf.c:5:12: warning: format '%c' expects argument of type 'int', but argument 4 has type 'char *' [-Wformat=]
我们可以从报错信息中看到:
%d 期望的是 int 类型参数
%f 期望的是 double 类型参数
%c 期望的也是 int 类型参数
而编译之所以有警告是因为, char * 类型无法通过默认实际参数提升, 将其提升为 int 或 double.
参数入栈顺序以及计算顺序
在 C 语言中, 参数入栈顺序是确定的, 从右往左. 而参数的计算顺序却是没有规定的. 也就是说, 编译器可以实现从右往左计算, 也可以实现从左往右计算.
浮点数的有效位
对于 double 类型, 其有效位为 15~~16 位(参考: 对浮点数的一些理解).
可变域宽和精度
printf 中,* 的使用可实现可变域宽和精度, 使用时只需要用 * 替换域宽修饰符和精度修饰符即可. 在这样的情况下, printf 会从参数列表中取用实际值作为域宽或者精度. 示例程序如下:
- #include<stdio.h>
- int main(void)
- {
- float a = 1.33333333;
- char *p = "hello";
- printf("%.*f\n",6,a);
- printf("%*s\n",8,p);
- return 0;
- }
运行结果:
- 1.333333
- hello
而这里的 6 或者 8 完全可以是一个宏定义或者变量, 从而做到了动态地格式控制.
格式控制符是如何处理参数的
printf 有很多格式控制符, 例如 %d, 它在处理输入时, 会从堆栈中取其对应大小, 即 4 个字节作为对应的参数值. 也就是说, 当你传入参数和格式控制符匹配或者在经过类型提升后和格式控制符匹配的时候, 参数处理是没有任何问题的. 但是不匹配时, 可能会出现未定义行为 (有两种情况例外, 我们后面再说). 例如,%f 期望一个 double(8 字节) 类型, 但是传入的参数是 int(4 字节), 那么在处理这个 int 参数值, 可能会多处理 4 个字节, 并且也会造成处理数据错误.
真相大白
有了前面这些内容的铺垫, 我们再来解答开始的疑问:
对于问题 0,a/b 的结果显然为 4 字节的 int 类型 1, 而 %f 期望的是 8 字节的 double, 而计算结果只有 4 个字节, 因此会继续格式化后面 4 个字节的 a, 而整型 1 和后面 a 组合成的 8 字节数据, 按照浮点数的方式解释时, 它的值就是 0.000000 了. 由于前面已经读取解释了 a 的内容, 因此第二个 %d 只能继续读取 4 个字节, 也就是 b 的值 3, 最终就会出现打印 a 的值是 3, 而不是 4.
对于问题 1, 实际上在 printf 中, 是不需要 %lf 的,%f 期望的就是 double 类型, 在编译最开始的示例程序其实就可以发现这个事实. 当然了在 scanf 函数中, 这两者是有区别的.
对于问题 2, 也很简单, 2 的二进制存储形式按照浮点数方式解释读取时, 就是该值.
对于问题 3,double 的有效位为 15~16 位, 也就是之外的位数都是不可靠的. printf 中的 * 可用于实现可变域宽和精度, 前面已经解释过了.
对于问题 4, 这里不给出, 留给读者思考, 欢迎大家可留言区给出原因.
对于问题 5, 虽然参数计算顺序没有规定, 但是实际上至少对于 gcc 来说, 它是从右往左计算的. 也就是说, 先计算 a++, 而 a++ 是先用在加, 即压入 a=4, 其后, a 的值变为 5; 再计算 ++a, 先加再用, 即压入 a=5+1=6; 最后 a=6, 压入栈. 最终从左往右压入栈的值就分别为 6,6,4. 也就是最终的打印结果. 但是实际情况中, 这样的代码绝对不该出现!
至此, 真相大白.
总结
虽然我们前面解释了那些难以理解的现象, 同时读者可以参考变长参数探究和对浮点数的一些理解找到更多的信息. 但是我们在实际编程中应该注意以下几点:
格式控制符应该与对应参数类型匹配或者与类型提升后的参数类型匹配.
绝对避免出现计算结果与参数计算顺序有关的代码.
* 在 printf 中实现可变域宽和精度.
printf 不会实际接受到 char,short 和 float 类型参数.
如果 %s 对应的参数可能为 NULL 或者对应整型, 那将是一场灾难.
不要忽略编译器的任何警告, 除非你很清楚你在做什么.
例外情况指的是有符号整型和无符号整型之间, 以及 void * 和 char * 之间.
问题思考
如果编译为 64 位程序运行, 结果还是一样吗? 为什么?
来源: https://www.cnblogs.com/bianchengzhuji/p/10060255.html