在C语言中,我们可以通过函数实现可变参数的形式,来使函数接受多个参数,接下我就深度刨析一下C语言中的可变参数的源码及其是如何实现的。
当让在这之前我们先要进行一下知识铺垫了,先要了解一下函数栈帧的部分知识,当然,有想要详细了解的朋友可以看我之前的一篇博客,地址在此奉上。https://blog.csdn.net/aixintianshideshouhu/article/details/81157657
首先,我们要知道根据函数调用约定,在函数调用过程中,函数形参的在是从右向左在内存中创建的,而栈的开辟又是从高地址向低地址的,所以我们可以通过对维护栈顶的寄存器esp减去相应的字节,来拿到对应的形参的地址,好了,知识补充完了,接下来就要放大招了。
先上我们们的演示代码。
#include<stdio.h>
#include<stdarg.h>
int SUM(int n, ...)
{
int i = 0;
int sum = 0;
va_list arg;
va_start(arg, n);
for (i = 0; i < n; i++)
{
sum += va_arg(arg, int);
}
return sum/n;
va_end(arg);
}
int main()
{
int ret = SUM(3, 4, 5, 6);
printf("%d",ret);
return 0;
}
这个程序呢,可以求出三个数的平均值,在函数的传参部分,我可以看到只传了参数个数,而具体参数的类型用…代替了。
接下来,我们就详细剖析一下这段代码。
首先,我们看va_list arg;是什么作用呢,在编译器中点击转到定义,我们会看到这样的。
typedef char * va_list;
//定义了一个字符指针变量arg
接下来,再来看看va_start(arg,n)又发生了什么呢,同样的做法.
#define va_start _crt_va_start
//首先看到是一个va_start又被定义了一次,我们再点击转到定义。
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)
//在点击_ADDRESSOF(V)转到定义,我们看到只是一个取地址的符号。
#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
//我们发现va_start是被个宏定义的,我们把宏的定义翻译成预编译的代码来理解一下
arg = &n + sizeof(n)
//首先,取出n的地址,又给它加了4个字节,让他跳过了参数个数n的地址,此时根据函数栈帧的形参创建相关知识,我们知道,这是,它指向的是下一个参数的地址,让后把他存到char *类型的指针中去。
//接下来再来看va_arg(arg,)
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//可以看到,也是定义了一个宏,相同的做法
(*(int *)((arg += sizeof(int)) - sizepf(int))
//我们发现这段代码写的是真的好难,一次完成了两件事,首先,把arg的地址加上四个字节,让它指向下一个参数的地址,让后又减去四个字节又跳回到原本指向的参数的的地址,而此时arg已经被赋值为下一个参数的地址了,然后又转换为整形类型指针,在解引用,就可以访问到这个参数了。
这样循环三次,就可以访问到,我们参数个数值之后的那三个参数
//最后来观察一下va_end(0)
define _crt_va_end(ap) ( ap = (va_list)0 )
//可以看到又将arg的赋值为零,也就是空指针了。
看懂可变参数列表的定义,我们也可以自己动手实现一个可变参数列表,来解析多个参数的问题。
#include<stdio.h>//由于是自己实现的,所以不用包含<stdarg.h>的头文件
int SUM(int n,...)
{
int i = 0;
int sum = 0;
char *arg;
arg = (char*)(&n);//得到参数列表起始参数的地址
arg = arg + 4;//跳过起始参数的地址
for(i = 0;i<n;i++)
{
sum += (*(int *)arg);//对参数解引用,得到参数的值,加到sum上
arg += 4;//跳到下一个参数的地址
}
return sum/n;
}
#include<stdio.h>
int main()
{
int ret = SUM(3,4,5,6);
printf("%d\n",ret);
return 0;
}
看完上面的代码演示,我们发现,第一个参数都表示的是可变参数的个数,其实也觉得不过如此吗,接下来我们就来演示一个高级版 的,用参数列表的最后一个参数作为程序结束的条件。
#include<stdio.h>
#include<stdarg.h>
int SUM(int first,...)
{
int i = 0;
int sum = 0;
va_list arg;
va_start(arg,first);
while((first = va_arg(arg,int)) >=0)
{
sum += first;
}
return sum;
va_end(arg);
}
int main()
{
int ret = SUM(100,4,5,6,-1);
printf("%d",ret);
return 0;
}
这段代码和上面一样,不过第一个参数不是作为参数的个数存在,而是用最后一个参数作为程序结束的条件。
###接下来我们在介绍一下可变参数的一些限制。
1,可变参数必须从头到尾访问,向要直接从中间的参数开始访问时不可以的。
2,参数列表至少要有一个命名参数,不然无法使用va_start
3,这些宏是无法判断参数的数量和类型的
4,在va_arg中指定 的类型要慎重,不然其后果是不可预知的。