词条 | vadefs |
释义 | 一、 在谈论这问题前我们先要了解什么是可变参数: int printf( const char* format, ...); printf知道为什么叫这个名字么?原来f就是format。所以命名也是很讲究的。 然后Win下有有好几种不同的调用约定,比如__stdcall,__pascal,__cdecl。 在Windows下__stdcall,__pascal是一样的,一般用于固定参数的函数, 表示调用端负责被调用函数引数的入栈和出栈; __cdecl一般用于可变参数的函数,表示被调用函数自身负责函数引数的入栈和出栈, 不过效率不高,所以除非一定要用,一般都采用上一种。 这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示).而我们又可以用各种方式来调用printf,如: printf("%d",value); printf("%s",str); printf("the number is %d ,string is:%s", value,str); 二.实现原理 C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义: VC2005在这些东西的定义前有这个东西“#elif defined(_M_IX86) //。。。” 觉得M代表Machine,IX86大概是Intel X86的意思吧。 然后这些东西被放到了vadefs.h中,名字前都加了个_crt_,比如va_strat就变成了_crt_va_start 最后在stdarg.h中加入了下面的东西来满足标准 #include<vadefs.h> #define va_start_crt_va_start //...... typedef char *va_list; /*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/ #define _INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))//简单的位操作,应该可以理解 /*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统(转载者注:X86MS就是这样的系统),从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/ #define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v)) /*va_start的定义为 &v+_INTSIZEOF(v),这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们写va_start(ap,v)以后,ap指向第一个可变参数在的内存地址*/ #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) -_INTSIZEOF(t)) ) /*这个宏做了两个事情, ①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值 ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/ #define va_end(ap) ( ap = (va_list)0 ) /*x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.*/ 以下再用图来表示: 在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的(不同的编译器的实现可能将参数从右向左压栈,也可能从左向右压栈,这个顺序我们是不能加以利用的,应该考虑到代码的移植性。所以应该用标准化的方式,在第二个例子中会给出。尽管如此,了解下VC的模式对于我们的理解也是有好处的),因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。 |——————————————————————————| |最后一个可变参数 | ->高内存地址处 |——————————————————————————| ................... |——————————————————————————| |第N个可变参数 |->va_arg(arg_ptr,int)后arg_ptr所指的地方, | | 即第N个可变参数的地址。 |——————————————— | …………………………. |——————————————————————————| |第一个可变参数 |->va_start(arg_ptr,start)后arg_ptr所指的地方 | | 即第一个可变参数的地址 |——————————————— | |——————————————————————————| | | |最后一个固定参数 | -> start的起始地址 |—————————————— —|................. |——————————————————————————| | | |——————————————— |->低内存地址处 三.printf研究 下面是一个简单的printf函数的实现,参考了书中的156页的例子,读者可以结合书上的代码与本文参照。 最好不要这样写,还是移植性的问题,但作者是全靠自己的理解写的,所以还是放着,加深印象 #include <stdio.h> #include<stdlib.h> void myprintf(char* fmt,...){ //一个简单的类似于printf的实现,//参数必须都是int类型 char* pArg=NULL; //等价于原来的va_list char c; pArg = (char*) &fmt; //注意不要写成p = fmt!!因为这里要对//参数取址,而不是取值 pArg += sizeof(fmt); //等价于原来的va_start do{ c =*fmt; if (c != '%'){ putchar(c); //照原样输出字符 }else{ //按格式字符输出数据 switch(*++fmt){ case 'd':printf("%d",*((int*)pArg));break; case 'x':printf("%#x",*((int*)pArg));break; default:break; } pArg += sizeof(int); //等价于原来的va_arg } ++fmt; }while (*fmt != '\\0'); pArg = NULL;//等价于va_end //其实这里是不必要这么写的,反正调用好后都会失效,但是作为一个好习惯应该保留 } int main(int argc, char* argv[]){ int i = 1234, j = 5678; myprintf("the first test:i=%d\",i,j); myprintf("the secend test:i=%d; %x;j=%d;\",i,0xABCD,j); myprintf("OK\"); system("pause"); return 0; } 在intel+win2k+vc6的机器执行结果如下: the first test:i=1234 the secend test:i=1234; 0xabcd; j=5678; OK VC2005也能得到同样结果。 四.应用 求最大值: //这个程序原作者挂得很惨,所以自己重新写了一个 #include <stdarg.h> #include <stdio.h> int mymax(int n,...){ int m=0,i,y; va_list x; //说明变量x 要尽量这么写,尽量用宏名 va_start(x,n); //x被初始化为指向n后的第一个可变参数 for(i=1;i<=n;++i){ //将变量x所指向的int类型的值赋给y,同时使x指向下一个参数 y=va_arg(x,int); if(y>m) m=y; } va_end(x); //清除变量x return m; } int main(){ printf("max1=%d,max2=%d\",mymax(3,5,56,50),mymax(6,0,4,32,45,12,500)); /* *注意调用的时候千万要搞清参数的个数! *原作者犯了个很搞笑的错误 *前面明明写了3,结果后面只跟了两个参数 *就像mymax(3,5,56) *最后,gcc自带一个max函数,所以换个名字叫mymax */ return 0; } VC2005,DevCpp编译通过,运行结果: max1=56,max2=500 |
随便看 |
百科全书收录4421916条中文百科知识,基本涵盖了大多数领域的百科知识,是一部内容开放、自由的电子版百科全书。