#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 float aa = 1.33333333;
char *pa = "hello";
printf("%.*f\n",9,aa);
printf("%*s\n",10,pa);
return 0; return 0;
}

可变参数中的类型提升

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函数的更多相关文章

  1. 【javascript杂谈】你所不知道的replace函数

    前言 最近在做面试题的时候总会用到这个函数,这个函数总是和正则表达式联系到一起,并且效果很是不错,总能很简单出色的完成字符串的实际问题,大家肯定都会使用这个函数,像我一样的初学者可能对这个函数的了解还 ...

  2. 你所不知道的C++

    C++与C的不同 C++从诞生之初就号称和C是兼容的,正是这种兼容,使C++得以迅猛发展,然而也正是这种兼容,让C++背上了沉重的历史包袱.且不论其利弊,让我们来看看C++在兼容C的那部分中,与C语言 ...

  3. [转帖]你所不知道的C和C++运行库

    [C-C++]你所不知道的C和C++运行库 https://blog.csdn.net/humanking7/article/details/85887884 原作者也是转的blog 最近一个物理机上 ...

  4. 你所不知道的setInterval

    在你所不知道的setTimeout记载了下setTimeout相关,此篇则整理了下setInterval:作为拥有广泛应用场景(定时器,轮播图,动画效果,自动滚动等等),而又充满各种不确定性的这set ...

  5. 你所不知道的setTimeout

    JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成.它们向任务队列添加定时任务.初始接触它的人都觉得好简单 ...

  6. 你真的会玩SQL吗?你所不知道的 数据聚合

    你真的会玩SQL吗?系列目录 你真的会玩SQL吗?之逻辑查询处理阶段 你真的会玩SQL吗?和平大使 内连接.外连接 你真的会玩SQL吗?三范式.数据完整性 你真的会玩SQL吗?查询指定节点及其所有父节 ...

  7. 你所不知道的SQL Server数据库启动过程,以及启动不起来的各种问题的分析及解决技巧

    目前SQL Server数据库作为微软一款优秀的RDBMS,其本身启动的时候是很少出问题的,我们在平时用的时候,很少关注起启动过程,或者很少了解其底层运行过程,大部分的过程只关注其内部的表.存储过程. ...

  8. Android中Context详解 ---- 你所不知道的Context

    转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好,  ...

  9. JavaScript中你所不知道的Object(二)--Function篇

    上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关.最后留了个悬念,就是Boolean.Date.Number.Str ...

随机推荐

  1. 谈谈redis的热key问题如何解决

    引言 讲了几天的数据库系列的文章,大家一定看烦了,其实还没讲完...(以下省略一万字).今天我们换换口味,来写redis方面的内容,谈谈热key问题如何解决.其实热key问题说来也很简单,就是瞬间有几 ...

  2. [转帖]InnoDB与MyISAM等存储引擎对比

    InnoDB与MyISAM等存储引擎对比 https://blog.ouyangsihai.cn/innodb-yu-myisam-deng-cun-chu-yin-qing-dui-bi.html ...

  3. python gdal安装与简单使用

    原文链接:python gdal安装与简单使用 gdal安装方式一:在网址 https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal 下载对应python版本的 ...

  4. 数组模拟循环队列(java实现)

    1.front变量的含义:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素front的初始值=0. 2.rear变量的含义:rear指向队列的最后一个元素的后一个位置 ...

  5. 分布式系统领域的 CAP 定理

    C 为数据一致性: A 为服务可用性: P 为服务对网络分区故障的容错性. 这三个特性在任何分布式系统中都不能同时满足,最多同时满足两个.

  6. Vue.js源码全方位深入解析--学习笔记

    模板中的插入变量是如何渲染到DOM上的? initMixin(Vue)->_init->$options-> $mount()当执行该挂载方法时DOM变化 为什么可以通过this访问 ...

  7. java EE学习之数据库操作

    jdbc开发流程 注册驱动 建立连接(Connection) 创建运行SQL的语句(Statement) 运行语句 处理运行结果(ResultSet) 释放资源 注冊驱动有三种方式: Class.fo ...

  8. Java源码阅读之ArrayList

    基于jdk1.8的ArrayList源码分析. 实现List接口最常见的大概就四种,ArrayList, LinkedList, Vector, Stack实现,今天就着重看一下ArrayList的源 ...

  9. 【转载】 C#中PadRight函数以特定字符在字符串结尾补足位数

    在C#开发过程中字符串String类处理过程中,有时字符串长度不够时,需要在右侧侧指定特定的字符来补足字符串长度,此时可以使用String类下的PadRight方法对字符串结尾按特定的字符补足位数.M ...

  10. stm32 FSMC-TFTLCD显示

    TFTLCD TFT液晶屏常用的通信模式主要有6800模式和8080模式,对于TFT彩屏通常都使用8080并口(简称80并口)模式 8080模式的读写时序其实跟LCD1602或者LCD12864的读写 ...