C语言可变参数列表源码级深度解析

在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中指定 的类型要慎重,不然其后果是不可预知的。

猜你喜欢

转载自blog.csdn.net/aixintianshideshouhu/article/details/81176631