6.C变长参数函数
本来准备讨论下对C++的认识的,看了一下昨天写的东西,发现前一篇讲到了函数的参数传递问题,突然想到了C的一个特殊的函数——变长参数函数。其实这也不是个新东西,估计学过C的都用过,只不过没实现过这种函数而已。最常见的就是printf函数:
在stdio.h头文件中它是这样定义的:
printf(const char *format, ...);
使用方法:
int a=1;
float b=1.0;
printf(“a=%d”,a);
printf(“a=%d,b=%f”,a,b);
两次对printf的调用使用了不同数量的参数,但是函数却只有一个定义。那么,编译器是怎样约定来使得函数内部正确得到它所需参数的呢?我们晓得,当编译器看到printf(“a=%d,b=%f”,a,b);后,会发现这个函数有三个参数,他们分别是:字符串“a=%d,b=%f”(char *类型)、a(int类型)、b(float类型)。前一篇中提到过,C语言是通过栈来传递参数的,X86中stack是向下增长的,就是说每当push的时候sp的值是递减的,而且C语言规定把参数按照从右向左的顺序push进入stack。在调用printf的地方,编译器会产生如下一些代码(伪代码):
push b;
push a;
push 字符串地址;
call printf
pop字符串地址;
pop a;
pop b;
call printf使得IP指向printf入口,但是在编写printf时,我们不晓得有多少个参数传进来,真么获取这些参数呢?其实printf知道第一个参数一定是一个字符串,它分析该字符串,看有多少个%,就知道有几个参数。并根据%号后面的字符得到各个参数的类型,当然也就知道其大小。然后就可以算出它们在stack中的位置,进而取得它们。下面看看,如何取得各个参数在stack中的地址:
首先我们可以得到第一个参数的地址,然后根据其类型T,把sp加上sizeof(T)得到第二个参数的地址,并根据%给出的类型获取其值,然后再增加sp,依此类推。
在stdarg.h中定义了宏va_start,va_arg,va_end。下面用一个简单的函数来展示它的使用方法:
#include <stdarg.h>
void var_list_fun(int i, ...)
{
va_list arg_ptr;//定义一个指针来指向栈
int j=0;
va_start(arg_ptr, i);//初始化指针指向第一个可变参数
j=va_arg(arg_ptr, int);/*指针加上sizeof(int),arg_ptr指向下一个参数地址,并返回上一个地址中的参数值,这
里就是可变列表第一个参数值。*/
va_end(arg_ptr);//这句无关紧要,表示结束
printf("%d %d\n", i, j);
return;
}
如果调用:var_list_fun(1,2);
就会输出:1 2回车
不同的硬件平台和编译器宏的具体定义有所不同,下面是VC++里 x86平台的宏定义:
typedef char * va_list;
#define _INTSIZEOF(n) \
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) \
( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
注:
其中_INTSIZEOF(n)用来完成32位对齐。
va_start:定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址.
va_arg:取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:j= (*(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址。
va_end:x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的
本来准备讨论下对C++的认识的,看了一下昨天写的东西,发现前一篇讲到了函数的参数传递问题,突然想到了C的一个特殊的函数——变长参数函数。其实这也不是个新东西,估计学过C的都用过,只不过没实现过这种函数而已。最常见的就是printf函数:
在stdio.h头文件中它是这样定义的:
printf(const char *format, ...);
使用方法:
int a=1;
float b=1.0;
printf(“a=%d”,a);
printf(“a=%d,b=%f”,a,b);
两次对printf的调用使用了不同数量的参数,但是函数却只有一个定义。那么,编译器是怎样约定来使得函数内部正确得到它所需参数的呢?我们晓得,当编译器看到printf(“a=%d,b=%f”,a,b);后,会发现这个函数有三个参数,他们分别是:字符串“a=%d,b=%f”(char *类型)、a(int类型)、b(float类型)。前一篇中提到过,C语言是通过栈来传递参数的,X86中stack是向下增长的,就是说每当push的时候sp的值是递减的,而且C语言规定把参数按照从右向左的顺序push进入stack。在调用printf的地方,编译器会产生如下一些代码(伪代码):
push b;
push a;
push 字符串地址;
call printf
pop字符串地址;
pop a;
pop b;
call printf使得IP指向printf入口,但是在编写printf时,我们不晓得有多少个参数传进来,真么获取这些参数呢?其实printf知道第一个参数一定是一个字符串,它分析该字符串,看有多少个%,就知道有几个参数。并根据%号后面的字符得到各个参数的类型,当然也就知道其大小。然后就可以算出它们在stack中的位置,进而取得它们。下面看看,如何取得各个参数在stack中的地址:
首先我们可以得到第一个参数的地址,然后根据其类型T,把sp加上sizeof(T)得到第二个参数的地址,并根据%给出的类型获取其值,然后再增加sp,依此类推。
在stdarg.h中定义了宏va_start,va_arg,va_end。下面用一个简单的函数来展示它的使用方法:
#include <stdarg.h>
void var_list_fun(int i, ...)
{
va_list arg_ptr;//定义一个指针来指向栈
int j=0;
va_start(arg_ptr, i);//初始化指针指向第一个可变参数
j=va_arg(arg_ptr, int);/*指针加上sizeof(int),arg_ptr指向下一个参数地址,并返回上一个地址中的参数值,这
里就是可变列表第一个参数值。*/
va_end(arg_ptr);//这句无关紧要,表示结束
printf("%d %d\n", i, j);
return;
}
如果调用:var_list_fun(1,2);
就会输出:1 2回车
不同的硬件平台和编译器宏的具体定义有所不同,下面是VC++里 x86平台的宏定义:
typedef char * va_list;
#define _INTSIZEOF(n) \
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) \
( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
注:
其中_INTSIZEOF(n)用来完成32位对齐。
va_start:定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址.
va_arg:取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:j= (*(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址。
va_end:x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的