对可变参数列表源码的浅剖析

什么是可变参数?我们通过下面这个例子来看看。
我现在来写一段简单的程序:

#include<stdio.h>
int Add(int a,int b)
{
    return a+b;
}
int main()
{
    int a=10;
    int b=20;
    int ret=0;
    ret=Add(a,b);
    printf("ret= %d";ret);
    return 0;
}

这段简单的代码大家肯定都能看懂,程序执行后毫无疑问会打印ret=30。
上面这个 Add()函数只能接受2个参数,那现在我们要求三个数 Add(a,b,c) 、四个数 Add(a,b,c,d) 、五个数 Add(a,b,c,d,e) 等等的和,我们是要再写出这些函数吗?这时我们就要考虑怎么样让一个函数在不同的时候接受不同数目的参数,这就要用到可变参数列表。
接下来我就用可变参数列表来实现上述问题:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdarg.h>//可变参数列表的头文件
#include<stdlib.h>
int Add(int n, ...)
{
    va_list arg;
    int i = 0;
    int sum = 0;
    va_start(arg, n);
    for (i = 0; i < n; i++)
    {
        sum = sum + va_arg(arg, int);
    }
    va_end(arg);
    return sum;
}
int main()
{
    int ret1 = 0;
    int ret2 = 0;
    int ret3 = 0;
    ret1 = Add(2, 4, 7);//第一个参数是是后面所需要相加数的个数
    ret2 = Add(3, 2, 7, 3);
    ret3 = Add(4, 2, 66, 3, 2);
    printf(" ret1=%d\n ret2=%d\n ret3=%d\n", ret1, ret2, ret3);
    system("pause");//在运行程序查看输出效果时,会出现窗口闪一下就关闭的情况。使用这句语句后,我们就能清楚的看到结果。它的头文件是stdlib.h
    return 0;

}

执行结果:
这里写图片描述
接下来我们就来通过剖析va_list, va_start, va_arg, va_\end 它们的源码来理解它们是干什么的。可以看出va_list是一种类型,其它几个都是宏,不是函数。给他们分别转到定义:
va_list:

typedef char *  va_list;

va_list就相当于char
va_start():

#define va_start _crt_va_start

va_start就是_crt_va_start,va_start(ap,v)和 _crt_va_start(ap,v)一样
对_crt_va_start转到定义:

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

_ADDRESSOF(v)转到定义:

#define _ADDRESSOF(v)   ( &(v) )

就是对 v 取地址
_INTSIZEOF(v)转到定义:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

n是int型,那sizeof(n)= sizeof (int) = 4,n是char型,那sizeof(n)= sizeof (char) = 1,其他类型类似。
n如果是int型,那sizeof(n) + sizeof(int) - 1=4+4-1=7,sizeof(int) - 1=4-1=3
7:00000000 00000000 00000000 00000111————————————1
3:00000000 00000000 00000000 00000011————————————2
3取反:11111111 11111111 11111111 11111100——————————3
1式这个二进制与3式这个相与:00000000 00000000 00000000 00000100 = 4
也就说_INTSIZEOF(int) = 0
n如果是char型:sizeof(n) + sizeof(int) - 1=1+4-1=4,sizeof(int) - 1=4-1=3
4:00000000 00000000 00000000 00000100————————————1
3:00000000 00000000 00000000 00000011————————————2
3取反:11111111 11111111 11111111 11111100——————————3
1式这个二进制与3式这个相与:00000000 00000000 00000000 00000100 = 4
_INTSIZEOF(char) = 4
这两个类型算出来都是4,那为什么要搞这么复杂的表达式呢?用sizeof关键字就好了。一般数据类型确实看上去求算有点复杂了,但是如果是结构体呢?结构体内有多种类型变量,如果继续使用简单的sizeof关键字求解的话,未免有些不太适合了。使用 ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)),传入char,float等小于等于4字节的类型,都会返回4。相应的传入如果类型大小是5,6,7,8的话,则返回8,传入如果类型大小是9,10,11,12的话,则返回12。即返回当前一组数中靠近4的倍数的值。

说了这么多,其实也就是这个意思:
va_start(arg,int)就相当于arg = (char*)&int + 4
va_start(arg,char)就相当于arg = (char*)&char + 4

va_arg( ):

#define va_arg _crt_va_arg

va_arg就是_crt_va_arg,va_arg(ap,t)和 _crt_va_arg(ap,t)一样
对_crt_va_start转到定义:

#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

如果传的是int型,那_INTSIZEOF(t) = 4,( (t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )就相当于 *(int*)(ap+=4) - 4),
也就是ap先自增4,然后ap - 4,对(ap - 4)这个数强制类型转换成 int*, 然后解引用

va_end( ):

#define _crt_va_end(ap)      ( ap = (va_list)0 )

ap这个变量不用了,因为它是char *(字符指针),不用了就把它赋成空指针
接下来我们根据讲的这些可变参数列表的源码把上面写的程序还原一下,让刺程序变的跟清晰:

int Add(int n, ...)
{
    //va_list arg;
    char* arg;
    int i = 0;
    int sum = 0;
    //va_start(arg, n);
    arg  =(char*)&int + 4;
    for (i = 0; i < n; i++)
    {
        //sum = sum + va_arg(arg, int);
        sum = sum + (*(int*)((arg+=4)-4));
        //或者分成两部
        //arg = arg - 4;
        //sum = sum + *((int*)(arg-4));
    }
    va_end(arg);
    return sum;
}

对可变参数列表的源码剖析就到这。.然后我们说说用可变参数列表是应该注意的问题:
1.参数列表中至少有一个命名参数,如果连一个命名参数都没的话,就无法使用va_start
2.然后我们说说用可变参数列表是应该注意的问题:可变参数必须从头到尾按顺序一个一个访问,如果你在访问几个可变参数后想终止,这是可以的
3.这些宏无法判断实际存在参数的数量
4.这些宏无法判断每个参数的类型
5.如果在va_arg中指定了错误的类型,那么后果是不可预测的

猜你喜欢

转载自blog.csdn.net/JSBGO201506010102/article/details/80069864