你可能不知道的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*之间。
问题思考
如果编译为64位程序运行,结果还是一样吗?为什么?
你可能不知道的printf的更多相关文章
- 你所不知道的printf函数
#include <stdio.h> int main(void) { int a = 4; int b = 3; int c = a / b; float d = *(float *)( ...
- 10686 DeathGod不知道的事情
Description 蚂蚁是很强大的动物,除了DeathGod知道的事情外还有很多不知道的!例如… 根据某种理论,时间方向上有无数个平行世界,有的世界蚂蚁很多,有的世界蚂蚁很少,有的世界蚂蚁会繁殖, ...
- 你所不知道的C++
C++与C的不同 C++从诞生之初就号称和C是兼容的,正是这种兼容,使C++得以迅猛发展,然而也正是这种兼容,让C++背上了沉重的历史包袱.且不论其利弊,让我们来看看C++在兼容C的那部分中,与C语言 ...
- [转帖]你所不知道的C和C++运行库
[C-C++]你所不知道的C和C++运行库 https://blog.csdn.net/humanking7/article/details/85887884 原作者也是转的blog 最近一个物理机上 ...
- 你所不知道的setInterval
在你所不知道的setTimeout记载了下setTimeout相关,此篇则整理了下setInterval:作为拥有广泛应用场景(定时器,轮播图,动画效果,自动滚动等等),而又充满各种不确定性的这set ...
- 你所不知道的setTimeout
JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成.它们向任务队列添加定时任务.初始接触它的人都觉得好简单 ...
- 你可能不知道的陷阱, IEnumerable接口
1. IEnumerable 与 IEnumerator IEnumerable枚举器接口的重要性,说一万句话都不过分.几乎所有集合都实现了这个接口,Linq的核心也依赖于这个万能的接口.C语言的 ...
- 你真的会玩SQL吗?你所不知道的 数据聚合
你真的会玩SQL吗?系列目录 你真的会玩SQL吗?之逻辑查询处理阶段 你真的会玩SQL吗?和平大使 内连接.外连接 你真的会玩SQL吗?三范式.数据完整性 你真的会玩SQL吗?查询指定节点及其所有父节 ...
- 你所不知道的linq(二)
上一篇说了from in select的本质,具体参见你所不知道的linq.本篇说下from...in... from... in... select 首先上一段代码,猜猜结果是什么? class P ...
随机推荐
- java 开发 websocket 网页端聊天室
博客地址:https://ainyi.com/67 WebSocket协议是基于TCP的一种新的网络协议.它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端. ...
- 南大算法设计与分析课程OJ答案代码(1)中位数附近2k+1个数、任意两数之和是否等于给定数
问题1 用来测试的,就不说了 问题2:中位数附近2k+1个数 给出一串整型数 a1,a2,...,an 以及一个较小的常数 k,找出这串数的中位数 m 和最接近 m 的小于等于 m 的 k 个数,以及 ...
- 第一册:lesson fifty one.
原文: A pleasant climate. A:Where do you come frome? B:I come from Greece. A:What's the climate like i ...
- Python网络编程Socket之协程
一.服务端 __author__ = "Jent Zhang" import socket import gevent from gevent import monkey monk ...
- 【转载】.NET开源快速开发框架Colder(NET452+AdminLTE版)
.NET开源快速开发框架Colder(NET452+AdminLTE版) 简介 本框架旨在为.NET开发人员提供一个Web后台快速开发框架,采用本框架,能够极大的提高项目开发效率. 本版本框架采后端采 ...
- Heaps(Contest2080 - 湖南多校对抗赛(2015.05.10)(国防科大学校赛决赛-Semilive)+scu1616)
Problem H: Heaps Time Limit: 2 Sec Memory Limit: 128 MBSubmit: 48 Solved: 9[Submit][Status][Web Bo ...
- confidence interval
95%置信区间.置信区间的两端被称为置信极限.对一个给定情形的估计来说,置信水平越高,所对应的置信区间就会越大. 对置信区间的计算通常要求对估计过程的假设(因此属于参数统计),比如说假设估计的误差是成 ...
- JSP使用过滤器防止SQL注入
什么是SQL注入攻击?引用百度百科的解释: sql注入_百度百科: 所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令.具 ...
- vue(四)-vuex与组件联合使用
官方定义: Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.我的理解就是,vuex就是前端的数据库. 1.首先核心是store,是个仓库,包含着state,因此第一步需要新建一个sto ...
- 微信小程序 JS 获取View 和 屏幕相关属性(高度、宽度等等)
wx.getSystemInfo({success: function (res) {thisWidth = res.windowWidth;}}); that.setData({view_Width ...