你可能不知道的printf

前言

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*之间。

本文分享自微信公众号 - 编程珠玑(shouwangxiansheng)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-04-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android必知必会

Android 必知必会 - 获取手机系统的构建模式

版权声明:本文为[他叫自己Mr.张]的原创文章,转载请...

10520
来自专栏凯哥Java

Java中注解学习系列教程-2

JDK自带的三个注解(@Override(重写、覆盖)、@Deprecated(废弃的,过时的)、@Suppvisewarnings(压缩警告))都是编译期的注...

6820
来自专栏MasiMaro 的技术博文

Java 学习笔记(11)——异常处理

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因...

10840
来自专栏FinGet前端之路

JavaScript从初级往高级走系列————ES6

在nodejs,exports 是 module.exports的引用,初始化时,它们都指向同一个{}对象。

8110
来自专栏诸葛青云的专栏

Java和C语言到底有什么区别?

Java和C语音的区别可以说是我们许多同学来咨询的重点困惑了,U妹找来了优就业研究院的老师来深入浅出地讲解一下,Java和C到底哪儿不一样!

90400
来自专栏宜信技术实践

宜信开源|一个实例解析PaaS平台LAIN的9大杀手级功能

在金融的场景下,LAIN 是为解放各个团队和业务线的生产力而设计的一个云平台。LAIN 正式上线已经大约两年,基本已经成熟,为宜信大数据创新中心各个团队提供了统...

13940
来自专栏h5

react实战开发|react+web版聊天室

基于react+react-dom+react-router-dom+redux+react-redux+webpack2.0+react-photoswipe...

21410
来自专栏开发架构二三事

实战之mapstruct的妙用

lombok是一款插件,在常用的开发工具eclipse和idea中都很好进行安装,具体安装方式请自行网上寻找。lombok提供了一些的的注解,会在编译期帮你自动...

80730
来自专栏凯哥Java

Java中反射学习系列教程之二

在上一篇文章《Java中反射学习系列教程之一》中我们学了什么是反射;反射中的反字怎么理解;以及反射的特点。今天我们主要讲的内容:

9420
来自专栏攻城狮的那点事

Java项目开发中你都踩过那些坑?

在项目开发中,我们经常被一些不清楚的问题搞得一脸懵逼,甚至几天都解决不了。今天总结总结近期踩过的坑吧!

12720

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励