C语言可变参数详解.docx_第1页
C语言可变参数详解.docx_第2页
C语言可变参数详解.docx_第3页
C语言可变参数详解.docx_第4页
C语言可变参数详解.docx_第5页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1,首先,怎么得到参数的值。对于一般的函数,我们可以通过参数对应在参数列表里的标识符来得到。但是参数可变函数那些可变的参数是没有参数标识符的,它只有“”,所以通过标识符来得到是不可能的,我们只有另辟途径。我们知道函数调用时都会分配栈空间,而函数调用机制中的栈结构如下图所示: | . | - | 参数2 | - | 参数1 | - | 返回地址 | - |调用函数运行状态| -可见,参数是连续存储在栈里面的,那么也就是说,我们只要得到可变参数的前一个参数的地址,就可以通过指针访问到那些可变参数。但是怎么样得到可变参数的前一个参数的地址呢?不知道你注意到没有,参数可变函数在可变参数之前必有一个参数是固定的,并使用标识符,而且通常被声明为char*类型,printf函数也不例外。这样的话,我们就可以通过这个参数对应的标识符来得到地址,从而访问其他参数变得可能。我们可以写一个测试程序来试一下:#include void va_test(char* fmt,.);/参数可变的函数声明void main() int a=1,c=55; char b=b; va_test(,a,b,c);/用四个参数做测试void va_test(char* fmt,.) /参数可变的函数定义,注意第一个参数为char* fmt char *p=NULL; p=(char *)&fmt;/注意不是指向fmt,而是指向&fmt,并且强制转化为char *,以便一个一个字节访问 for(int i = 0;i16;i+)/16是通过计算的值(参数个数*4个字节),只是为了测试,暂且将就一下 printf(%.4d ,*p);/输出p指针指向地址的值 p+; 编译运行的结果为0056 0000 0066 0000 | 0001 0000 0000 0000 | 0098 0000 0000 0000 | 0055 0000 0000 0000由运行结果可见,通过这样方式可以逐一获得可变参数的值。至于为什么通常被声明为char*类型,我们慢慢看来。2,怎样确定参数类型和数量通过上述的方式,我们首先解决了取得可变参数值的问题,但是对于一个参数,值很重要,其类型同样举足轻重,而对于一个函数来讲参数个数也非常重要,否则就会产生了一系列的麻烦来。通过访问存储参数的栈空间,我们并不能得到关于类型的任何信息和参数个数的任何信息。我想你应该想到了使用char *参数。Printf函数就是这样实现的,它把后面的可变参数类型都放到了char *指向的字符数组里,并通过%来标识以便与其它的字符相区别,从而确定了参数类型也确定了参数个数。其实,用何种方式来到达这样的效果取决于函数的实现。比如说,定义一个函数,预知它的可变参数类型都是int,那么固定参数完全可以用int类型来替换char*类型,因为只要得到参数个数就可以了。3,言归正传 我想到了这里,大概的轮廓已经呈现出来了。本来想就此作罢的(我的惰性使然),但是一想到如果不具实用性便可能是一堆废物,枉费我打了这么些字,决定还是继续下去。 我是比较抵制用那些不明所以的宏定义的,所以在上面的阐述里一点都没有涉及定义在的va(variable-argument)宏。事实上,当时让我产生极大疑惑和好奇的正是这几个宏定义。但是现在我们不得不要去和这些宏定义打打交道,毕竟我们在讨生计的时候还得用上他们,这也是我曰之为“言归正传”的理由。 好了,我们来看一下那些宏定义。 打开文件,找一下va_*的宏定义,发现不单单只有一组,但是在各组定义前都会有宏编译。宏编译指示的是不同硬件平台和编译器下用怎样的va宏定义。比较一下,不同之处主要在偏移量的计算上。我们还是拿个典型又熟悉的X86的相关宏定义:1)typedef char * va_list;2)#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & (sizeof(int) - 1) )3)#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )4)#define va_arg(ap,t) ( *(t *)(ap += _INTSIZEOF(t) - _INTSIZEOF(t) )5)#define va_end(ap) ( ap = (va_list)0 )我们逐一看来:第一个我想不必说了,类型定义罢了。第二个是颇有些来头的,我们也不得不搞懂它,因为后面的两个关键的宏定义都用到了。不知道你够不够细心,有没有发现在上面的测试程序中,第二个可变参数明明是char类型,可是在输出结果中占了4个byte。难道所有的参数都会占4个byte的空间?那如果是double类型的参数,且不是会丢失数据!如果你不嫌麻烦的话,再去做个测试吧,在上面的测试程序中用一个double类型(长度为8byte)和一个long double类型(长度为10byte)做可变参数。发现什么?double类型占了8byte,而long double占了12byte。好像都是4的整数倍哦。不得不引出另一个概念了“对齐(alignment)”,所谓对齐,对Intel80x86 机器来说就是要求每个变量的地址都是sizeof(int)的倍数。原来我们搞错了,char类型的参数只占了1byte,但是它后面的参数因为对齐的关系只能跳过3byte存储,而那3byte也就浪费掉了。那为什么要对齐?因为在对齐方式下,CPU 的运行效率要快得多(举个例子吧,要说明的是下面的例子是我从网上摘录下来的,不记得出处了。示例:如下图,当一个long 型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU 存取这个数只需访问一次内存,而当一个long 型数(如图中的long2)在内存中的位置跨越了字边界时,CPU 存取这个数就需要多次访问内存,如i960cx 访问这样的数需读内存三次(一个BYTE、一个SHORT、一个BYTE,由CPU 的微代码执行,对软件透明),所以对齐方式下CPU 的运行效率明显快多了。1 8 16 24 32- - - -| long1 | long1 | long1 | long1 |- - - -| | | | long2 |- - - -| long2 | long2 | long2 | |- - - -| .)。好像扯得有点远来,但是有助于对_INTSIZEOF(n)的理解。位操作对于我来说是玄的东东。单个位运算还应付得来,而这样一个表达式摆在面前就晕了。怎么办?菜鸟自有菜的办法。(待续)Trackback: /TrackBack.aspx?PostId=14721-C语言中的可变参数函数 CSDN Blog推出文章指数概念,文章指数是对Blog文章综合评分后推算出的,综合评分项分别是该文章的点击量,回复次数,被网摘收录数量,文章长度和文章类型;满分100,每月更新一次。第一篇C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为:int printf( const char* format, .);它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“”做参数占位符),实际调用时可以有以下的形式:printf(%d,i);printf(%s,s);printf(the number is %d ,string is:%s, i, s);一个简单的可变参数的C函数 先看例子程序。该函数至少有一个整数参数,其后占位符,表示后面参数的个数不定。在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值。函数代码如下:/示例代码1:可变参数函数的使用#include stdio.h#include stdarg.hvoid simple_va_fun(int start, .) va_list arg_ptr; int nArgValue =start; int nArgCout=0; /可变参数的数目 va_start(arg_ptr,start); /以固定参数的地址为起点确定变参的内存起始地址。 do +nArgCout; printf(the %d th arg: %d,nArgCout,nArgValue); /输出各参数的值 nArgValue = va_arg(arg_ptr,int); /得到下一个可变参数的值 while(nArgValue != -1); return;int main(int argc, char* argv) simple_va_fun(100,-1); simple_va_fun(100,200,-1); return 0;下面解释一下这些代码。从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:由于在程序中将用到以下这些宏:void va_start( va_list arg_ptr, prev_param );type va_arg( va_list arg_ptr, type );void va_end( va_list arg_ptr );va在这里是variable-argument(可变参数)的意思。这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件。函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。然后用va_start宏初始化中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数。然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,在看完这几个宏的内部实现机制后,自然就会明白。第二篇C语言之可变参数问题C语言中有一种长度不确定的参数,形如:,它主要用在参数个数不确定的函数中,我们最容易想到的例子是printf函数。原型:int printf( const char *format , argument. );使用例:printf(Enjoy yourself everyday!n);printf(The value is %d!n, value);这种可变参数可以说是C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。注意:在C+中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。问题:printf的实现请问,如何自己实现printf函数,如何处理其中的可变参数问题? 答案与分析:在标准C语言中定义了一个头文件专门用来对付可变参数列表,它包含了一组宏,和一个va_list的typedef声明。一个典型实现如下:typedef char* va_list;#define va_start(list) list = (char*)&va_alist#define va_end(list)#define va_arg(list, mode)(mode*) (list += sizeof(mode)-1自己实现printf:#includeint printf(char* format, )va_list ap;va_start(ap, format);int n = vprintf(format, ap);va_end(ap);return n;问题:运行时才确定的参数有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?答案与分析:目前没有正规的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是main(),它的原型是:int main(int argc,char *argv);函数的参数是argc和argv。深入想一下,只能在运行时确定参数形式,也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个void *类型的参数,用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是main函数中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。问题:可变长参数的传递有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现?答案与分析:目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数的参数为va_list类型,同时在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。看如下所示:void subfunc (char *fmt, va_list argp).arg = va_arg (fmt, argp); /* 从argp中逐一取出所要的参数 */.void mainfunc (char *fmt, .)va_list argp;va_start (argp, fmt); /* 将可变长参数转换为va_list */subfunc (fmt, argp); /* 将va_list传递给子函数 */va_end (argp);.问题:可变长参数中类型为函数指针我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?答案与分析:这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下:#define va_arg(argp, type) (*(type *)(argp) += sizeof(type) - sizeof(type)其中,argp的类型是char *。如果你想用va_arg从可变参数列表中提取出函数指针类型的参数,例如int (*)(),则va_arg(argp, int (*)()被扩展为:(*(int (*)() *)(argp) += sizeof (int (*)() -sizeof (int (*)()显然,(int (*)() *)是无意义的。解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如:typedef int (*funcptr)();这时候再调用va_arg(argp, funcptr)将被扩展为:(* (funcptr *)(argp) += sizeof (funcptr) - sizeof (funcptr)这样就可以通过编译检查了。问题:可变长参数的获取有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:va_arg (argp, float);这样做可以吗?答案与分析:不可以。在可变长参数中,应用的是加宽原则。也就是float类型被扩展成double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。问题:定义可变长参数的一个限制为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?int f (.).答案与分析:不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。-如何判别可变参数函数的参数类型?函数形式如下:void fun(char* str,.) .若传的参数个数大于1,如何判别第2个以后传参的参数类型?最好有源码说明!没办法判断的如楼上所说,例如printf( %d%c%s , .)是通过格式串中的%d, %c, %s来确定后面参数的类型,其实你也可以参考这种方法来判断不定参数的类型.无法判断。可变参数实现主要通过三个宏实现:va_start, va_arg, va_end。六、 扩展与思考个数可变参数在声明时只需.即可;但是,我们在接受这些参数时不能.。va函数实现的关键就是如何得到参数列表中可选参数,包括参数的值和类型。以上的所有实现都是基于来自stdarg.h的va_xxx的宏定义。 能不能不借助于va_xxx,自己实现VA呢?,我想到的方法是汇编。在C中,我们当然就用C的嵌入汇编来实现,这应该是可以做得到的。至于能做到什么程度,稳定性和效率怎么样,主要要看你对内存和指针的控制了。参考资料1.IEEE和OpenGroup联合开发的Single Unix specification Ver3;BR2.Linux man手册;3.x86汇编,还有一些安全编码方面的资料。-转帖对C/C+可变参数表的深层探索C/C+语言有一个不同于其它语言的特性,即其支持可变参数,典型的函数如printf、scanf等可以接受数量不定的参数。如:printf ( I love you );printf ( %d, a );printf ( %d,%d, a, b );第一、二、三个printf分别接受1、2、3个参数,让我们看看printf函数的原型:int printf ( const char *format, . );从函数原型可以看出,其除了接收一个固定的参数format以外,后面的参数用表示。在C/C+语言中,表示可以接受不定数量的参数,理论上来讲,可以是0或0以上的n个参数。本文将对C/C+可变参数表的使用方法及C/C+支持可变参数表的深层机理进行探索。一. 可变参数表的用法1、相关宏标准C/C+包含头文件stdarg.h,该头文件中定义了如下三个宏:void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */type va_arg ( va_list arg_ptr, type );void va_end ( va_list arg_ptr );在这些宏中,va就是variable argument(可变参数)的意思;arg_ptr是指向可变参数表的指针;prev_param则指可变参数表的前一个固定参数;type为可变参数的类型。va_list也是一个宏,其定义为typedef char * va_list,实质上是一 char型指针。char型指针的特点是+、-操作对其作用的结果是增1和减1(因为sizeof(char)为1),与之不同的是int等其它类型指针的+、-操作对其作用的结果是增sizeof(type)或减sizeof(type),而且sizeof (type)大于1。通过va_start宏我们可以取得可变参数表的首指针,这个宏的定义为:#define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) )显而易见,其含义为将最后那个固定参数的地址加上可变参数对其的偏移后赋值给ap,这样ap就是可变参数表的首地址。其中的_INTSIZEOF宏定义为:#define _INTSIZEOF(n) (sizeof ( n ) + sizeof ( int ) - 1 ) & ( sizeof( int ) - 1 ) )va_arg宏的意思则指取出当前arg_ptr所指的可变参数并将ap指针指向下一可变参数,其原型为:#define va_arg(list, mode) (mode *)(list =(char *) (int)list + (_builtin_alignof(mode)=4?3:7) &(_builtin_alignof(mode)=4?-4:-8)+sizeof(mode)-1对这个宏的具体含义我们将在后面深入讨论。而va_end宏被用来结束可变参数的获取,其定义为:#define va_end ( list )可以看出,va_end ( list )实际上被定义为空,没有任何真实对应的代码,用于代码对称,与va_start对应;另外,它还可能发挥代码的自注释作用。所谓代码的自注释,指的是代码能自己注释自己。下面我们以具体的例子来说明以上三个宏的使用方法。2、一个简单的例子#include /*函数名:max*功能:返回n个整数中的最大值* 参数:num:整数的个数 .:num个输入的整数* 返回值:求得的最大整数*/int max ( int num, . )int m = -0x7FFFFFFF; /* 32系统中最小的整数 */va_list ap;va_start ( ap, num );for ( int i= 0; i m )m = t;va_end (ap);return m;/* 主函数调用max */int main ( int argc, char* argv )int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求5个整数中的最大值 */cout n;return 0;函数max中首先定义了可变参数表指针ap,而后通过va_start ( ap, num )取得了参数表首地址(赋给了ap),其后的for循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到的遍历方式是类似的。函数max看起来简洁明了,但是实际上printf的实现却远比这复杂。max函数之所以看起来简单,是因为:(1) max函数可变参数表的长度是已知的,通过num参数传入;(2) max函数可变参数表中参数的类型是已知的,都为int型。而printf函数则没有这么幸运。首先,printf函数可变参数的个数不能轻易的得到,而可变参数的类型也不是固定的,需由格式字符串进行识别(由%f、%d、%s等确定),因此则涉及到可变参数表的更复杂应用。下面我们以实例来分析可变参数表的高级应用。二. 高级应用下面这个程序是我们为某嵌入式系统(该系统中CPU的字长为16位)编写的在屏幕上显示格式字符串的函数DrawText,它的用法类似于 int printf ( const char *format, . )函数,但其输出的目标为嵌入式系统的液晶显示屏幕(LED)。/ 函数名称: DrawText/ 功能说明: 在显示屏上绘制文字/ 参数说明: xPos -横坐标的位置 0 . 30/ yPos -纵坐标的位置 0 . 64/ . 可以同数字一起显示,需设置标志(%d、%l、%x、%s)/extern void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, . )BYTE lpData100; /缓冲区BYTE byIndex;BYTE byLen;DWORD dwTemp;WORD wTemp;int i;va_list lpParam;memset( lpData, 0, 100);byLen = strlen( lpStr );byIndex = 0;va_start ( lpParam, lpStr );for ( i = 0; i byLen; i+ )if( lpStri != % ) /不是格式符开始lpDatabyIndex+ = lpStri;elseswitch (lpStri+1)/整型case d:case D:wTemp = va_arg ( lpParam, int );byIndex += IntToStr( lpData+byIndex, (DWORD)wTemp );i+;break;/长整型case l:case L:dwTemp = va_arg ( lpParam, long );byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp );i+;break;/16进制(长整型)case x:case X:dwTemp = va_arg ( lpParam, long );byIndex += HexToStr ( lpData+byIndex, (DWORD)dwTemp );i+;break;default:lpDatabyIndex+ = lpStri;break;va_end ( lpParam );lpDatabyIndex = 0;DisplayString ( xPos, yPos, lpData, TRUE);/在屏幕上显示字符串lpData在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在for循环中。譬如,在识别为%d后,做的是va_arg ( lpParam, int ),而获知为%l和%x后则进行的是va_arg ( lpParam, long )。格式字符串识别完成后,可变参数也就处理完了。在项目的最初,我们一直苦于不能找到一个好的办法来混合输出字符串和数字,我们采用了分别显示数字和字符串的方法,并分别指定坐标,程序条理被破坏。而且,在混合显示的时候,要给各类数据分别人工计算坐标,我们感觉头疼不已。以前的函数为:/显示字符串showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr )/显示数字showNum ( BYTE xPos, BYTE yPos, int num )/以16进制方式显示数字showHexNum ( BYTE xPos, BYTE yPos, int num )最终,我们用DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, . )函数代替了原先所有的输出函数,程序得到了简化。就这样,兄弟们用得爽翻了。三. 运行机制探索通过第2节我们学会了可变参数表的使用方法,相信喜欢抛根问底的读者还不甘心,必然想知道如下问题:(1)为什么按照第2节的做法就可以获得可变参数并对其进行操作?(2)C/C+在底层究竟是依靠什么来对这一语法进行支持的,为什么其它语言就不能提供可变参数表呢?我们带着这些疑问来一步步进行摸索。3.1 调用机制反汇编反汇编是研究语法深层特性的终极良策,先来看看2.2节例子中主函数进行max ( 5, 5, 6 ,3 ,8 ,5)调用时的反汇编:1. 004010C8 push 52. 004010CA push 83. 004010CC push 34. 004010CE push 65. 004010D0 push 56. 004010D2 push 57. 004010D4 call ILT+5(max) (0040100a)从上述反汇编代码中我们可以看出,C/C+函数调用的过程中:第一步:将参数从右向左入栈(第16行);第二步:调用call指令进行跳转(第7行)。这两步包含了深刻的含义,它说明C/C+默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为_cdecl调用。x86系统的入栈方向为从高地址到低地址,故第1至n个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到max函数的内部:int max ( int num, .)1. 00401020 push ebp2. 00401021 mov ebp,esp3. 00401023 sub esp,50h4. 00401026 push ebx5. 00401027 push esi6. 00401028 push edi7. 00401029 lea edi,ebp-50h8. 0040102C mov ecx,14h9. 00401031 mov eax,0CCCCCCCCh10. 00401036 rep stos dword ptr ediva_list ap;int m = -0x7FFFFFFF; /* 32系统中最小的整数 */11. 00401038 mov dword ptr ebp-8,80000001hva_start ( ap, num );12. 0040103F lea eax,ebp+0Ch13. 00401042 mov dword ptr ebp-4,eaxfor ( in

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论