您好,欢迎访问三七文档
当前位置:首页 > 机械/制造/汽车 > 机械/模具设计 > 详解-C语言可变参数-va-list和-vsnprintf及printf实现
C语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口printf就是使用的变长参数接口,在感受到printf强大的魅力的同时,是否想挖据一下到底printf是如何实现的呢?这里我们一起来挖掘一下C语言变长参数的奥秘。先考虑这样一个问题:如果我们不使用C标准库(libc)中提供的Facilities,我们自己是否可以实现拥有变长参数的函数呢?我们不妨试试。一步一步进入正题,我们先看看固定参数列表函数,voidfixed_args_func(inta,doubleb,char*c){printf(a=0x%p\n,&a);printf(b=0x%p\n,&b);printf(c=0x%p\n,&c);}对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a我们可以得到a的地址,并通过函数原型声明了解到a是int类型的;通过&b我们可以得到b的地址,并通过函数原型声明了解到b是double类型的;通过&c我们可以得到c的地址,并通过函数原型声明了解到c是char*类型的。但是对于变长参数的函数,我们就没有这么顺利了。还好,按照C标准的说明,支持变长参数的函数在原型声明中,必须有至少一个最左固定参数(这一点与传统C有区别,传统C允许不带任何固定参数的纯变长参数函数),这样我们可以得到其中固定参数的地址,但是依然无法从声明中得到其他变长参数的地址,比如:voidvar_args_func(constchar*fmt,...){......}这里我们只能得到fmt这固定参数的地址,仅从函数原型我们是无法确定...中有几个参数、参数都是什么类型的,自然也就无法确定其位置了。那么如何可以做到呢?在大脑中回想一下函数传参的过程,无论...中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,而栈这个东西对我们是开放的。这样一来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置,顺着这个思路,我们继续往下走,通过一个例子来诠释一下:(这里要说明的是:函数参数进栈以及参数空间地址分配都是实现相关的,不同平台、不同编译器都可能不同,所以下面的例子仅在IA-32,WindowsXP,MinGWgccv3.4.2下成立)我们先用上面的那个fixed_args_func函数确定一下这个平台下的入栈顺序。intmain(){fixed_args_func(17,5.40,helloworld);return0;}a=0x0022FF50b=0x0022FF54c=0x0022FF5C从这个结果来看,显然参数是从右到左,逐一压入栈中的(栈的延伸方向是从高地址到低地址,栈底的占领着最高内存地址,先入栈的参数,其地理位置也就最高了)。我们基本可以得出这样一个结论:c.addr=b.addr+x_sizeof(b);/*注意:x_sizeof!=sizeof,后话再说*/b.addr=a.addr+x_sizeof(a);有了以上的等式,我们似乎可以推导出voidvar_args_func(constchar*fmt,...)函数中,可变参数的位置了。起码第一个可变参数的位置应该是:first_vararg.addr=fmt.addr+x_sizeof(fmt);根据这一结论我们试着实现一个支持可变参数的函数:voidvar_args_func(constchar*fmt,...){char*ap;ap=((char*)&fmt)+sizeof(fmt);printf(%d\n,*(int*)ap);ap=ap+sizeof(int);printf(%d\n,*(int*)ap);ap=ap+sizeof(int);printf(%s\n,*((char**)ap));}intmain(){var_args_func(%d%d%s\n,4,5,helloworld);}输出结果:45helloworldvar_args_func只是为了演示,并未根据fmt消息中的格式字符串来判断变参的个数和类型,而是直接在实现中写死了,如果你把这个程序拿到solaris9下,运行后,一定得不到正确的结果,为什么呢,后续再说。先来解释一下这个程序。我们用ap获取第一个变参的地址,我们知道第一个变参是4,一个int型,所以我们用(int*)ap以告诉编译器,以ap为首地址的那块内存我们要将之视为一个整型来使用,*(int*)ap获得该参数的值;接下来的变参是5,又一个int型,其地址是ap+sizeof(第一个变参),也就是ap+sizeof(int),同样我们使用*(int*)ap获得该参数的值;最后的一个参数是一个字符串,也就是char*,与前两个int型参数不同的是,经过ap+sizeof(int)后,ap指向栈上一个char*类型的内存块(我们暂且称之tmp_ptr,char*tmp_ptr)的首地址,即ap-&tmp_ptr,而我们要输出的不是printf(%s\n,ap),而是printf(%s\n,tmp_ptr);printf(%s\n,ap)是意图将ap所指的内存块作为字符串输出了,但是ap-&tmp_ptr,tmp_ptr所占据的4个字节显然不是字符串,而是一个地址。如何让&tmp_ptr是char**类型的,我们将ap进行强制转换(char**)ap=&tmp_ptr,这样我们访问tmp_ptr只需要在(char**)ap前面加上一个*即可,即printf(%s\n,*(char**)ap);前面说过,如果将var_args_func放到solaris上,一定是得不到正确结果的?为什么呢?由于内存对齐。编译器在栈上压入参数时,不是一个紧挨着另一个的,编译器会根据变参的类型将其放到满足类型对齐的地址上的,这样栈上参数之间实际上可能会是有空隙的。上述例子中,我是根据反编译后的汇编码得到的参数间隔,还好都是4,然后在代码中写死了。为了满足代码的可移植性,C标准库在stdarg.h中提供了诸多Facilities以供实现变长长度参数时使用。这里也列出一个简单的例子,看看利用标准库是如何支持变长参数的:#includestdarg.hvoidstd_vararg_func(constchar*fmt,...){va_listap;va_start(ap,fmt);printf(%d\n,va_arg(ap,int));printf(%f\n,va_arg(ap,double));printf(%s\n,va_arg(ap,char*));va_end(ap);}intmain(){std_vararg_func(%d%f%s\n,4,5.4,helloworld);}输出:45.400000helloworld对比一下std_vararg_func和var_args_func的实现,va_list似乎就是char*,va_start似乎就是((char*)&fmt)+sizeof(fmt),va_arg似乎就是得到下一个参数的首地址。没错,多数平台下stdarg.h中va_list,va_start和var_arg的实现就是类似这样的。一般stdarg.h会包含很多宏,看起来比较复杂。在有的系统中stdarg.h的实现依赖somespecialfunctionsbuiltintothethecompilationsystemtohandlevariableargumentlistsandstackallocations,多数其他系统的实现与下面很相似:(VisualC++6.0的实现较为清晰,因为windows上的应用程序只需要在windows平台间做移植即可,没有必要考虑太多的平台情况)。C语言va_list与_vsnprintf的使用先举一个例子:#definebufsize80charbuffer[bufsize];/*这个函数用来格式化带参数的字符串*/intvspf(char*fmt,...){va_listargptr;//声明一个转换参数的变量intcnt;va_start(argptr,fmt);//初始化变量cnt=vsnprintf(buffer,bufsize,fmt,argptr);//将带参数的字符串按照参数列表格式化到buffer中va_end(argptr);//结束变量列表,和va_start成对使用return(cnt);}intmain(intargc,char*argv[]){intinumber=30;floatfnumber=90.0;charstring[4]=abc;vspf(%d%f%s,inumber,fnumber,string);{printf(%s\n,buffer);return0;}下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的C函数要在程序中用到以下这些宏:使用可变参数应该有以下步骤:1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.3)然后用va_arg返回可变的参数,并赋值给整数j.va_arg的第二个参数是你要返回的参数的类型,这里是int型.4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:可变参数在编译器中的处理我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于:1)硬件平台的不同2)编译器的不同MicrosoftVisualStudio\VC98\Include\stdarg.h中,typedefchar*va_list;/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/#define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))/*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/#defineva_start(ap,v)(ap=(va_list)&v+_INTSIZEOF(v))/*va_start的定义为&v+_INTSIZEOF(v),这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap,v)以后,ap指向第一个可变参数在的内存地址*/#defineva_arg(ap,t)(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))/*这个宏做了两个事情,①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/#defineva_end(ap)(ap=(va_list)0)/*x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为
本文标题:详解-C语言可变参数-va-list和-vsnprintf及printf实现
链接地址:https://www.777doc.com/doc-6349330 .html