对于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语言的初学者,菜鸟一枚,如果出现错误或者不合适的地方,请斧正,谢谢。