在C语言中,通过将函设置为可变参数的形式,可使函数接收多个参数,下面从源码层面对该问题进行简单剖析。
先来看一段代码:
#include<stdio.h>
#include<stdarg.h>
int ave(int n,...)
{
va_list arg;
int i = 0;
int sum = 0;
va_start(arg, n);
for (i = 0; i < n; i++)
{
sum += va_arg(arg, int);
}
va_end(arg);
return sum / n;
}
int main()
{
int ret = ave(4, 1, 3, 5, 7);
printf("ret = %d", ret);
getch();
}
在这段代码中,有一些关键点,例如:va_list arg;
,va_start(arg, n);
等,下面找到它的源码,进行简单剖析。
首先,对va_list
转到定义,发现下图示:
char*
被#typedef
重命名类型为va_list
,所以这里可以认为va_list
是char*
类型,即arg
是一个char*
类型的变量。原来的代码就可以变成char* arg;
接着,对va_start(arg, n);
三次转入定义,发现下图示:
此时,可以对照源码类比写好的代码,得到(arg = (char*)&(n)+4);
,直接将(arg, n)
代入(ap, v)
,char*
代入va_list
替换,可以很容易地得到:
((void)(arg = (char*)_ADDRESSOF(n) + _INTSIZEOF(n))
;
再对_ADDRESSOF(n)
转到定义,发现它的源码为(&(v))
,即对变量做&
操作,而_INTSIZEOF(n))
是对变量测字节的操作,在这段代码中,可以简单地认为完成该操作后它的值为4,原来的这句代码就可以变成:
(arg = (char*)&(n)+4)
。
这句代码的意义在于:初始化arg
为未知参数列表的第一个元素的地址,而之后的+4
代表指针指向的位置跳过4个字节,指向下一个元素。
然后,再对va_arg(arg,int)
两次转入定义,发现:
有了上面对va_start(arg, n);
的分析以后,就能轻易对va_arg(arg,int);
进行类比分析,容易得到 (*(int*)((arg += 4) - 4));
这句代码需要重点分析
在前面的分析中,了解到arg
实际上是一个char*
的指针,对指针+4
指向它的下一个参数,而这时再对指针-4
,再对它强制类型转换为(int*)
后解引用,取到的依然是一开始指向的地址,但指针已经指向了下一个地址,为下次操作做准备。
这句代码实现了两个功能:留下了原来的地址,并使指针指向下一个地址。
最后,对va_end(arg);
转到定义并替换后,得到(arg = (char*)0);
,表明在结束程序后,将arg
置为空指针,保证安全性。
至此,对可变参数列表的简单剖析就告一段落了.
注,可变参数存在一定的限制:
1.可变参数必须从头开始逐个访问。允许在访问了几个可变参数后终止,但不允许从一开始就访问参数裂变中间的参数;
2.参数列表中至少有一个命名参数,否则无法使用va_list
;
3.在转到定义后,发现它们全都是宏,这些宏不能直接判断实际存在的参数的数量,也不能判断参数的类型;
4.如果在va_arg
中指定了错误的类型,后果不堪设想。