我们出于某些需要,经常需要编写一些可变参数参数的函数。从而获得更大的灵活性。
我们经常使用的printf就是一个很好的例子,这种可变参数一般通过一个格式化字符串来表明后面的参数类型和个数,从而使得函数内部可以解析可变的参数列表。
我们先来解析printf函数。
int printf(const char* format,…);
这是printf的函数原型,格式化字符串后面跟着可变参数列表。而我们调用printf的时候可以这样。
printf(“%d%c%s”,5,’c’,”string”);
原型里面的。。。可以用好多个参数替代。
我们下面就利用这种可变参数来实现一个可以给多个数字求和的函数,原型如下:
Int sum(intnum, …);
该函数接受一个后续参数的个数,然后把他们求和并返回。
实现如下:
#include <stdarg.h>
#include <stdio.h>
int sum(int num,...){
int sum = 0;
va_list args;
va_start (args,num);
for(int i = 0; i < num; i++){
sum += va_arg(args, int);
}
va_end(args);
return sum;
}
int main(int argc, char *argv[]){
int result=sum(5,1,2,3,4,5);
printf("result = %d\n", result);
}
上面的代码中用到了几个新鲜的宏定义(va_xxx)现在解释一下
1. Va_list 这是一个类型定义,在stdarg.h中如下定义
Typedef char*va_list
由此可见这其实就是一个字符指针类型。
2. Va_start这个宏用来出事后可变参数列表。如下定义
#define __va_argisz(t) (((sizeof(t) + (sizeof(int) + 1)/sizeof(int) * sizeof(int))
#define va_start(ap,pN) ((ap) = ((va_list)(&pN +__va_argsiz(pN))
第一个宏的意思是获得类型t对的size对int补齐以后的结果。比如你的机器是
Char 是1,Short 是2,Int 4,Long 是 8
那么__va_argsiz对char,short,int运算结果都是4,对long运算结果是8.
理解了第一个,第二个宏就简单了,
他其实就是从最后一个定参参数的地址+这个参数本身占用的长度,得到下一个参数的地址。
3. Va_arg(ap,t)宏,该宏能够获取下一个参数,实现如下:
#define va_arg(ap,t) (*(t*)((ap) += __va_argsiz(t)) -__va_argsiz(t)))
这个宏定义可以分开理解,第一步是让ap+=__va_argsiz(t),这样ap就指向了下一个参数,当然也有可能是末尾。
第二步是ap-__va_argsiz(t)这样又回到了当前参数指针上,然后强制类型转换成需要转换的类型t。
4. Va_end(ap),
#define va_end(ap) (ap= (va_list)0) 很简单,把ap变成NULL。
这些宏介绍完了,我再说说内存模型,根据c/c++的调用约定,参数会被压在程序栈当中,按照“后面的参数先压栈”的规定,比如我们调用函数
这里需要注意,函数堆栈向下生长Func(x,a,b,c)内存中的堆栈模型如图:
所以不是A,B,C自上而下的顺序。
有了这样的认识我们就好办了,我们假设x是定参的最后一个,那么a,b,c就是可变参数列表。
1. 我们定义va_list args这可以理解为定义一个char*指针。
2. 我们使用va_start(args, x),这一步让args这个指针指向了参数A的地址。
3. 我们通过va_arg(args, int)来获取下一个参数的地址,这是args只向B,同时返回了A的指针,并转换成int*类型。
4. B,C的指针以此类推。
5. 最后va_end(args)把args变成NULL。
这就是可变参数列表解析的整个过程,其实很简单,但还有一些不是很简单的事情……
__va_argsiz宏为什么获取的总是int的倍数?
因为c/c++中约定,参数size小于int是,要自动向上转换成int大小,不对齐时向上补全。
也就是我们传递一个char类型的参数给函数,他在堆栈上也是开辟4byte的空间。跟int一样。
那么如果你细心,应该能注意到一个问题,我们队va_arg宏使用如下写法,
Va_arg(args,char)会有什么后果?
还好,通常编译器会禁止这样的用法的。