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 != ' ');
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