printf带来的思考

来源:互联网 发布:阿里云服务器价格 编辑:程序博客网 时间:2024/05/05 09:13

最近闲来没事,在论坛逛游了几圈,发现很对人对printf按浮点格式输出产生了迷惑,比如:

这段代码为啥第一个输出是0.000000,而另一个输出是正确的100。其实这个问题涉及到了c语言标准库里的变长参数的技术。

肯定有人会问啥是变长参数?好,我们慢慢来,馒头要一口一口吃的!

 

1. 什么是变长参数?

大家可以先看看printf的定义:

其中,函数形参中用到了“...”,表明函数除第一个形参是const char*类型的,可以拥有任意数量和任意类型的参数,也就是说:我们可以随意的输入不定个任意类型的参数,可能现在有人会疑惑:函数内部怎么知道我们输入了什么参数,输入多少参数呢?好问题!我们继续往下看!

 

2. 变长参数的技术实现

我们可以看到printf函数内部用到了一个变量va_list arglist; 我们跟进,可以找到va_list类型的定义如下,其实就是char类型的指针

typedef char *  va_list;

我们往下看,在15行对arglist做了操作 va_start(arglist, format); 并且将最后一个参数传入,同样我们看看和va_start相关的定义:

 

#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end

 

#define _ADDRESSOF(v)   ( &(v) )

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap)      ( ap = (va_list)0 )

其中,_INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍,至于为啥化成4个字节的整数倍,这是与编译器相关的问题,我们暂时不讨论,以sizeof代替即可,整理下可得:

typedef char *  va_list;

#define va_start(ap,v)  ( ap = (va_list)&v+ sizeof(v) )
#define va_arg(ap,t)    ( *(t *)((ap += sizeof(t)) - sizeof(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

由上面的分析可知,va_start(arglist, format);就是将arglist指向printf函数的第一个形参所存储位置的后一个地址,为什么是这样呢?这个地址又是什么呢?printf的声明隐含天机,大家可以看到

int __cdecl printf (
        const char *format,
        ...
        )

c语言标准库里将printf的调用惯例声明成__cdecl,也就是说printf函数形参入栈的方式是从右至左,“哇”你是否恍然大悟!如果还没有,不要紧,我们徐徐道来:经过编译连接后的程序,装载到内存中后,程序栈在虚拟内存中的形式是栈顶在上,位于高地址,栈是向低地址扩展。那printf的形参是从右至左的顺序入栈,则最左边的形参最后入栈,位于低地址!这回你应该有点想法了吧!O(∩_∩)O哈哈~

经过va_start(arglist, format)处理的arglist此时所指向的就是format右边的形参,此时你就可以通过type nxt = va_arg(arglist,type)来获取当前参数,并将arglist移动到下一个参数的位置,不断循环的取值,直到最后一个参数。这时,你可能有点疑惑:我怎么知道要循环几次,也就是说还是没有说明函数内部是怎么知道有多少参数,以及参数的类型的呢?好,我们就继续讲讲这两个问题。这两个问题涉及到变长参数的使用规范,就是如果要用变长参数,函数内部必须要分清形参的个数和类型。那printf是怎么实现的呢?它不是还有第一个参数format吗!当用户使用该函数时,必须指定输出的类型规格,然后函数内部解析format字串,来获知是否还有参数,以及该参数的类型的!恍然大悟了吧!

 

最后,va_end(arglist)的功能就是将arglist指针赋空,防止野指针。

 

3. 该到结贴的时候了

对于文章开始提出的printf按浮点格式输出为0的问题,你是否有点想法了!也就是说,明明是int类型的数据,你非要以float类型去读,那肯定是会出错的,因为int和float在内存中存储的方式是不同的。那你可能有会说:为啥很多情况都是为0呢?要说明这个问题就先要讲解下float的内存存储的的方式:

在c++中float是用四个字节三十二位二进制位来存储,第一位是符号位,后8位是指数位,剩下的是23位有效位。8为指数位有一个是符号位,1表示正,0表示负。

我们看看100的int类型的内存中的值是:0000 0000 0000 0000 0000 0000 0110 0100 这个数按照float类型计算方式得到的值非常小,默认情况下“%f”保留六位有效数字,所以输出会为0.000000,如果不信你可以输出小数点后100000位,绝对是有值的!

 

好,结贴!

 

 

 

原创粉丝点击