可变参数函数的创建以及源码的剖析

可变参数函数


什么是可变参数?想象一下库函数的printf函数,这个函数不管用多少参数只要正确引用,都可以实现它的功能,但是当我们写函数时,其参数只能是固定的有限个,并且类型都固定,所以这就很局限。因此我们将参数的数量、类型可变的函数称为可变参数函数。


可变参数函数的创建


要想写可变参数的函数,首先你得对函数的栈帧有非常深的了解。

先放上一个问题:

请实现一个求平均值的函数 average ,不管传入多少整数,都可以求出这些整数的平均值。

average(5, 0, 3, 4, 4, 4);

像上面这个调用方式,第一个参数 5 代表此次要求多少个整数的平均值。

好了,废话不多说,直接先上我的代码:

#include<stdio.h>
#include<stdlib.h>
#include<stdarg.h>

//第一个参数为要求平均值的整数的个数,后面的参数为各个参数
int average(int count, ...)
{
    int sum = 0;
    int c = count;
    va_list num;
    va_start(num, count);
    while (count > 0)
    {
        sum += va_arg(num, int);
        count--;
    }
    va_end(num);
    return sum / c;
}

int main()
{
    int ret = 0;
    ret = average(5, 0, 3, 4, 4, 4);
    printf("结果为:%d\n",ret);
    system("pause");
    return 0;
}

先进行代码的刨析:

  • 主函数就不用解释了吧,简单创建变量用来接收结果,然后调用了要创建的函数并将最终的结果进行打印。
  • 首先是函数的定义,固定参数的函数我们都会,写的形参列表都是固定类型加形参名的方式,可是,可变参数不一样了,不过要狐疑一点,可变参数的第一个参数必须是一个固定类型的参数,后面的参数可以无限多。第一个我们要接收后面整数的数量,所以,直接定义为 int 型,形参名称我们可以定义为 count ,因为 count 不一样,所以你无法知道后面的参数有多少个,所以就用三个点代替,即”…”。
  • 声明完成了,我们开始刨析,求平均值的步骤一般都是先将各个参数相加起来,然后再除以参数的个数,所以我们需要创建一个变量sum来存整数的和。
  • 一个重要的变量就是 num,其类型是 va_list 这是一个宏定义,已经在库里封装好了,可直接使用,如果你打开这个宏定义,发现他就是一个char* 格式,即就是字符指针,这个变量非常的重要,我们需要它来找到后面的各个参数。
  • 这里我先把count 的值备份了一下,至于作用,你一会儿就会明白。
  • 下一句就是一个重要的宏定义了,记住,这个是宏定义,不是函数。
    首先是 va_start ,它的定义为:

    #define va_start(ap, v) (ap = (va_list)&(v) + _INTSIZEOF(v))

    什么意思呢?首先说名一下,_INTSIZEOF(v) 是一个宏定义,其作用是里面的参数所占空间按4字节取整,也就是当v所占的空间为1-4字节时,此宏定义的结果就为4,当v所占的空间为5-8字节时,此宏定义结果为8 ,以此类推。那么我们刚才在用的时候带入的v是变量 count ,其占用的空间大小为4字节,所以此宏定义结果为4,+前面的意思是 count 的地址强制转换为va_list也就是char* 型,总之就是取出count的地址,然后加上4,赋给 char 型指针 num ,你可能不懂这是什么意思,来结合图来分析一下:
    可变参数函数
    这张图片我只画出了函数栈帧的参数传递部分,结合我在图片中标注,首先是count 的地址,它指向的是函数中第一个参数,因为函数传参的顺序我在函数的栈帧中讲过,跟你函数声明时的顺序是相反的,所以将count 的地址取出来转化为 char* 然后在加上刚才算出来的 4 刚好就是跳过了第一个参数,也就是现在指向的是count后面的参数,这不正是我们想要的吗?

    这里总结一下va_start的用法和作用,va_start(ap,v) ,ap为一个字符指针,v一般为可变参数函数的第一个固定参数,作用是将ap指针指向可变参数函数的第一个可变的参数。
  • 那么指针已经指向了我们需要的第一个参数,而且后面的参数数量我们都知道,就是count 的值。所以我们现在可以通过建立一个循环让这个指针一个参数一个参数的向后移动,把需要的参数取出来不就行了?怎么么取呢?继续看下面。

  • va_arg 这也是一个宏定义,来看定义:

#define va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
我们可以进行一个替换,代码中的使用这个宏定义可以直接替换为这样的:
(*int*)((num += _INTSIZEOF(int)) - _INTSIZEOF(int)))
那么通过计算,我们进一步可以写成这样:
(*(int*)((num += 4) - 4))

这个代码写得极其的巧妙,为什么呢,先是num+4即num向后挪4个字节,跳过当前参数,但是在强制转换为int*时又减去一个4,整体的意思也就是将num先向后挪动4个字节使它指向第二个我们需要的参数,但是int*强制转换的却是num减4的地址,也就是原来的地址,强转之后再将其解引用,即取出第我们需要的第一个参数,但是num却已经指向了我们需要的第二个参数。也就是时这一句代码完成了俩个操作,难道不巧妙吗?

在这里总结一下va_arg的用法和作用(ap,t),ap是一个字符指针,t是一个数据类型,意思为从ap指向的当前位置取出一个t类型的数,并将ap指针移动到这个数的下一个参数的位置。
值得注意的是!!!va_arg每次用一次,指针就会自动移向下一个参数,所以,如果要多次使用这个值的话,最好将取出来的值保存起来。
  • 所以这里我用一个while循环把各个参数加到sum中,最后返回sum/count刚好就是平均值。
  • 这里有一个注意事项,num是一个移动的指针,通过它可以进行访问栈中任意的参数,所以,为了防止被误用,尤其是万一修改了其指的值,后果将不堪设想,所以,在遍历完参数后我们一般将这个指针释放,即 va_end(num) 其定义为:

#define va_end(ap) ((void)(ap = (va_list)0))

作用就是将ap指针置空。

总结

所以可变参数函数的创建就都是类似的,通过va_start将指针指向可变的第一个参数,然后通过va_arg来逐个获取可变参数,一定要记得,参数获取完成后一定要用va_end将指针置零。

猜你喜欢

转载自blog.csdn.net/qq_38590948/article/details/80114505