最近闲来没事,在论坛逛游了几圈,发现很对人对printf按浮点格式输出产生了迷惑,比如:
void main ( ) { int d = 100; printf("%f/n", d); printf("%d/n", d); }
这段代码为啥第一个输出是0.000000,而另一个输出是正确的100。其实这个问题涉及到了c语言标准库里的变长参数的技术。
肯定有人会问啥是变长参数?好,我们慢慢来,馒头要一口一口吃的!
1. 什么是变长参数?
大家可以先看看printf的定义:
int __cdecl printf ( const char *format, ... ) /* * stdout 'PRINT', 'F'ormatted */ { va_list arglist; int buffing; int retval; _VALIDATE_RETURN( (format != NULL), EINVAL, -1); va_start(arglist, format); _lock_str2(1, stdout); __try { buffing = _stbuf(stdout); retval = _output_l(stdout,format,NULL,arglist); _ftbuf(buffing, stdout); } __finally { _unlock_str2(1, stdout); } return(retval); }
其中,函数形参中用到了“...”,表明函数除第一个形参是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位,绝对是有值的!
好,结贴!