变长参数函数的实现及原理

来源:互联网 发布:黑客技术入门教程软件 编辑:程序博客网 时间:2024/05/17 09:08

变长参数其实是C语言的特殊参数形式
例如printf()函数和scanf()函数等:

int printf(const char * format,...);

如此声明,printf函数除了第一个函数类型为const char 之外,其后可以追加任意数量、任意类型的参数。
如何实现呢?在函数的实现部分,可以使用stdarg.h里的多个宏来访问各个额外的参数:
假设lastarg是变长参数函数的最后一个具名参数(如printf里面的format)

在函数内部定义类型为va_list的变量:

va_list ap;

该变量以后会一次指向各个可变参数。ap首先必须用宏 va_start 初始化一次,其中lastarg必须是函数的最后一个具名的参数。

va_start( ap , lastarg );

此后可以用 va_arg宏获得下一个不定参数(假设已知其类型为type):

type next = va_arg( ap, type ) ;

在函数结束之前,还必须用宏va_end来收拾清理现场。
在这里我们可以讨论一下几个宏的实现细节。
变长函数的实现得益于C默认的cdecl调用惯例的自右向左压栈的参数传递方式(不明白的可以先去了解一下函数调用时,栈的现场保护以及参数压栈的实现等基础)
设想如下的函数:

int sun(unsigned num , ...);

其语意如下:
第一个参数传递一个整数num,紧接着后面会传递num个整数,返回num个整数的和。
当我们调用:

int n = sum(3, 16 , 15 , 26);

参数会在栈上形成如下布局:

TOP

26

15

16

3

在函数的内部,函数可以使用num来访问数字3,但没法使用名称去访问其他的几个不定参数。但从以上栈的排列可看出,其他的几个参数恰好在参数num的高地址方向(栈向高地址伸展),因此可以简单的通过num的地址计算出其他参数的地址。
sum 函数实现如下:

int sum(unsigned num){    int *p = &num + 1;    int ret = 0;    while(num--){        ret += *p++;    }    retur nret;}

从以上我们可以观察到两个事实:

1. sum函数获取参数的量取决与num函数的值,因此,如果num参数的值不等于实际传递的不定参数的数量,那么sum函数可能获取到错误的或者不足的参数。可能会访问到栈上的其他局部变量,引起程序的崩溃。

2. cdecl调用惯例保证了可变参数的正确取用。

各个宏:

//访问可变参数流程   va_list args; //定义一个可变参数列表   va_start(args,arg);//初始化args指向强制参数arg的下一个参数;   va_arg(args,type);//获取当前参数内容并将args指向下一个参数   ...//循环获取所有可变参数内容   va_end(args);//释放args  

所以以上的sum函数可如下定义:

int sum( unsigned int n,...)  {     int sum = 0 ;     va_list args ;     va_start(args , n);     while(n > 0)     {      //通过va_arg(args,int)依次获取参数的值       sum += va_arg(args,int);       n --;     }     va_end(args);     return sum;  }  

其实只要了解了 cdecl调用惯例 和函数调用的具体到汇编代码的实现,我们就可以很好地理解可变参数函数的实现原理。
我们在编写可变参数函数的时候,一定要注意输入参数和你写的可变参数函数的获取参数相同,不然也会引起以上所说的越界情况。

1 0