如何使用变参函数?

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Andy123321aa/article/details/53057911

对于C语言的初学者来说printf函数是经常被使用的。但是不知道大家在初学的时候有没有注意到printf这个函数可以定义成这样:

printf("%d",a);         //有两个形参

也可以是这样:

printf("%d %d",a,b);    //有三个形参

可以发现printf函数可以定义任意多个形参。所以printf函数是一个变参函数。

那么,变参函数的原理究竟是什么呢,我们如何去书写变参函数?

在这里我写了一个简单的变参函数和大家分享一下,代码如下:

#include <stdio.h>

#define  _AUPBND                (sizeof (int) - 1) 
#define  _ADNBND                (sizeof (int) - 1)

#define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd))) 
#define MYva_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) 
#define MYva_end(ap)              (void) 0 
#define MYva_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

int sum(int n,...)                          //利用变参函数求和 
{
    int buf[20];                            //缓存数组,用于存储传入的形参 
    int sum = 0,i;                          
    char* t;                                //定义一个char*类型的变量 
    MYva_start(t, n);                       //将第二个形参的地址赋给t变量 
    for(i = 0;i < n;i++)                    //循环进行取后边的形参和进行求和计算 
    {
        buf[i] = MYva_arg(t,int);           //取后面的形参并将当前形参的值取出赋给buf缓存数组 
        sum += buf[i];                      //求和运算 
    }   
    MYva_end(t);                            //结束,将t指向NULL (虽然注释掉对结果没有影响,但是为了程序逻辑的严谨,还是加上吧。)
    return sum;                             //返回计算结果 
} 
main()
{   //sum函数第一个参数是需要求和的数的个数,后边的是要求和的数值
    printf("%d",sum(4,1,2,3,6));
}

先简单做个说明。C编译器通常提供了一系列处理可变参数的宏,以屏蔽不同的硬件平台造成的差异,增加程序的可移植性。这些宏包括va_start、va_arg和va_end等。但是我们只是写一个简单的变参函数,宏的名字等当然可以自己随便取啦。

我猜初学者看到代码开头定义的宏函数,一定会蒙圈的。那么我们就来简单分析一下。

众所周知,函数的形参是通过栈来传递的。所以说,变参函数至少有一个参数是确定的。假定我们的栈是向低地址生长的(x86架构的计算机栈是向低地址生长),而函数入栈的方向是从右向左入栈。也就是说,最后一个形参最先入栈。所以,调用函数sum(4,1,2,3,6)之后的栈是这样的:

(低地址在上) 栈:
第一个形参: 4
第二个形参: 1
最后一个形参: 6

因为现在函数只知道第一个形参的数据类型和值,所以就需要顺藤摸瓜,通过那几个宏函数在栈中找到后面的n个形参。

知道了这些,变参函数的原理就基本掌握了,那几个宏函数理解起来也不是那么费劲了。

我们以MYva_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))这个为例:

此宏函数的作用是将ap指针指向第一个不确定的形参,以我们的函数sum(4,1,2,3,6)为例,就是由第一个形参,根据栈的原理,找到第二个形参的地址。所以调用此宏函数之后,ap的地址就指向了参数’1’的地址。我们的程序在执行过程中,定义了char* t,将t作为实参传入了这个宏函数中,所以现在t就指向了参数’1’。

简单点讲,((char *) &(A))就是获取了第一个参数的地址,(_bnd (A,_AUPBND))计算出了A所占的地址空间,两者相加就得到了下一个参数的地址。所以我们将第一个形参n作为实参传入此宏函数进行计算。

到了这里大家可能又会有一个疑问。计算A占的地址空间,直接(sizeof (A))不就完了吗?为什么还要(_bnd (A,_AUPBND))呢?

我们找到了_bnd这个宏函数的定义:
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))

这样做其实是对sizeof后的结果进行的向上取了一个最接近bnd+1的倍数的数。bnd又是什么呢,我们找到bnd对应的实参_AUPBND,进而找到了_AUPBND的宏定义:
#define _AUPBND (sizeof (int) - 1)

所以我们知道宏函数_bnd其实是对sizeof后的结果向上取了一个sizeof (int)的倍数的数。假设你的cpu运行sizeof (int)的结果为4,再假设你在宏函数_bnd传入的X经过sizeof (X)计算的结果为17,那么宏函数_bnd的结果为就20。为什么会这样呢?因为栈的最小单元就是sizeof (int)。

好了,介绍完MYva_start(ap, A)这个宏函数的原理,相信其他宏函数也可以理解了。进而就可以掌握变参函数的原理。

但是变参函数由于语法并不是非常严谨,比如当你调用printf(“%d”,a,b);的时候,b就被忽略了,可能得不到你想要的结果,在应用中,变参函数也大多能被代替,所以变参函数的实际应用价值可能没那么大。

本人也是C语言的初学者,菜鸟一枚,如果出现错误或者不合适的地方,请斧正,谢谢。

猜你喜欢

转载自blog.csdn.net/Andy123321aa/article/details/53057911