可变参数定义
我们学习C语言时最经常使用printf()函数,但我们很少了解其原型。其实printf()的参数就是可变参数,想想看,我们可以利用它打印出各种类型的数据。下面我们来看看它的原型:
intprintf( const char* format, ...);
它的第一个参数是format,属于固定参数,后面跟的参数的个数和类型都是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式:
printf("%d",i);
printf("%s",s);
printf("thenumber is %d ,string is:%s", i, s);
说明例子
①一个简单的可变参数的C函数
在函数simple_va_fun参数列表中至少有一个整数参数,其后是占位符…表示后面参数的个数不定.。在这个例子里,所有输入参数必须都是整数,函数的功能只是打印所有参数的值。
#include
#include
voidsimple_va_fun(int start, ...)
{
va_listarg_ptr;
intnArgValue =start;
intnArgCout=0; //可变参数的数目
va_start(arg_ptr,start);//以固定参数的地址为起点确定变参的内存起始地址。
do
{
++nArgCout;
printf("the%d th arg: %d\n",nArgCout,nArgValue); //输出各参数的值
nArgValue= va_arg(arg_ptr,int); //得到下一个可变参数的值
}while(nArgValue != -1);
return;
}
intmain(int argc, char* argv[])
{
simple_va_fun(100,-1);
simple_va_fun(100,200,-1);
return0;
}
②格式化到一个文件流,可用于日志文件
FILE*logfile;
intWriteLog(const char * format, ...)
{
va_listarg_ptr;
va_start(arg_ptr,format);
intnWrittenBytes = vfprintf(logfile, format, arg_ptr);
va_end(arg_ptr);
returnnWrittenBytes;
}
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
⑴在程序中用到了以下这些宏:
voidva_start( va_list arg_ptr, prev_param );
typeva_arg( va_list arg_ptr, type );
voidva_end( va_list arg_ptr );
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.
⑵. 函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
⑷. 然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。
⑸. 设定结束条件
① 是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。
② 是调用宏va_end。
剖析
va_*宏定义
我们已经知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于硬件平台的不同和编译器的不同,所以定义的宏也有所不同。
以下VC++6.0中stdarg.h里的代码
(文件的路径为VC安装目录下的\
vc98\include\stdarg.h)
typedefchar * va_list;
#define_INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#defineva_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#defineva_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#defineva_end(ap) ( ap = (va_list)0 )
linux中的定义
typedefchar *va_list;
#define__va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) *sizeof (int))
#defineva_start(AP, LASTARG) (AP=((char*)&(LASTARG) + __va_rounded_size (LASTARG))
voidva_end (va_list);
#defineva_end(AP) (AP=(char *)0)
#defineva_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),\
*((TYPE*)(AP - __va_rounded_size (TYPE))))
要理解上面这些宏定义的意思,需要首先了解:
1. 栈的方向和参数的入栈顺序
2. CPU的对齐方式
3. 内存地址的表达方式。
1. 栈
以Intel 32位的CPU为分析基础
在Intel CPU中,栈的生长方向是向下的,即栈底在高地址,而栈顶在低地址;从栈底向栈顶看过去,地址是从高地址走向低地址的,因为称它为向下生长,如图。
从上面压栈前后的两个图可明显看到栈的生长方向,在Intel32位的CPU中,windown或linux都使用了它的保护模式,ss指定栈所有在的段,ebp指向栈基址,esp指向栈顶。显然执行push指令后,esp的值会减4,而pop后,esp值增加4。 栈中每个元素存放空间的大小决定push或pop指令后esp值增减和幅度。Intel32位CPU中的栈元素大小为16位或32位,由定义堆栈段时定义。在Window和Linux
系统中,内核代码已定义好栈元素的大小为32位,即一个字长(sizeof(int))。因此用户空间程栈元素的大小肯定为32位,这样每个栈元素的地址向4字节对齐。
C语言的函数调用约定对编写可变参数函数是非常重要的,只有清楚了,才更欲心所欲地控制程序。在高级程序设计语言中,函数调用约定有如下几种,stdcal