先来讲可变参数函数,这个最常见的就是printf,scanf
一般来说这两个函数应该算是最早接触的那一批才是,但是其参数数目可变的特殊性却被我忽略了好久,直到前一阵程老师上课才发现
于是就稍微整理下好了,主要是va_list, va_start, va_arg, va_end的使用
不多说,直接上代码:
sum是一个求和的函数,其第一个参数为参数数目,后面是要求和的数
#include <stdio.h> #include <stdarg.h> int sum(int num,...); int main(void) { int a=sum(8,2,5,3,6,4,7,8,5); int b=sum(4,5,1,2,3); printf("a=%d, b=%d\n",a,b); getch(); return 0; } int sum(int num,...){ va_list ap; int s=0; va_start(ap,num);/* ap初始化,从num开始*/ while (num>0){ s+=va_arg(ap,int);/*从参数里取一个新值,同时ap向后移至下一个参数的位置*/ --num; } va_end(ap); return s; }
输出结果为a=40,b=11,好用
可变参数函数的声明特点是,参数列表里有“...”出现,表示此处可以接纳无限多个参数
我们都知道函数传参的时候是从右往左依次压栈的,所以参数的地址是连续的,只要从头往下一直移啊移的又不越界,多少个参数的值都能取得到。
在具体代码里,首先定义一个va_list指针ap,取各个参数的值就靠他了;
va_start给ap的地址初始化;注意此时ap指向的地址其实是num的下一个参数
之后每次调用va_arg时,会返回ap指向的参数的值,同时ap会移动至下一个参数的地址
所以在调用va_arg时要传入你要读入的变量的类型(即大小)使之能移动合适的距离
最后使用va_end来释放资源。
通过翻头文件,发现其实va系列的全都是宏:
va_list的:
typedef char * va_list;
(原来就是一个char*的指针啦)
va_start的:
#define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v))
(可以看到ap指向的是v的地址+v的大小,即v的下一个参数)
va_arg的:
#define va_arg(ap,t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
(可以看到,会让ap挪到下一个参数,同时返回ap挪之前那个参数的值)
va_end的:
#define va_end(ap) (ap = (va_list)0)
(看上去好像没什么用,但是说不准哪个平台下va_list是分配的堆内存,那届时这里的释放操作就很必要了,所以va_end不能省)
同时注意到他取变量的大小,使用的不是sizeof而是另一个宏_INTSIZEOF:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) -1))
n的大小都取整到int的倍数了
这意味着就算你输入了va_arg(ap,char),他也会向后移4个字节指向下一个变量。即:传进来的char变量,实际上占用了int的大小!
令人不禁想起之前提到的struct内存对齐。
上网查询资料,得到了“默认参数提升”(Default Argument Promotions)这个说法:
当函数的参数数量不定时,会发生默认参数提升。默认参数提升,会使char,short变量转换为int类型再压进栈,而float变量会转换为double类型再压进栈。
这样就可以理解为什么printf里%d同时支持int到char,%f同时支持float和double了。scanf里面有%lf来对应double,是因为scanf传递的是地址而不是变量,不受默认参数提升的影响。
看如下代码:
#include "stdio.h" printit(a,b,c){ printf("%u\n%u\n%u\n",sizeof(a),sizeof(b),sizeof(c)); } int main(void) { float a=3.45f; char b='C'; double c=6.6; printit(a,b,c); return 0; }
注意到函数printit(a,b,c),其参数都完全没有说明其类型。这个能通过tc和gcc的编译
最后执行的结果竟然是3个int的大小!
可以看到,在参数类型未知的情况下,会全部当做int来处理。鉴于浮点数硬转整数的奇葩性,且该特性未写入标准,我认为没有人会使用这种写法。
当然程老师课上的演示代码中也有缺失类型的函数出现。不得不赞叹程老师的课还是有很多可以挖掘的细节。
最后关于默认参数提升的编译器细节实现,可以移步看某位大牛的博客:
4 条评论
博客写的挺专业的!
果酱啦,其实就是看了几篇别人文章后总结的,原创内容没多少
printit(a,b,c)这个函数为什么我用VC6.0不能实现呢?
报错说不认识 a,b,c。
一般书上都是写函数一定要定义参数的类型。
你这里却这样写,还实现了。你是进行过什么设置吗?
好久没用vc6了= = 刚刚重新装了并进行了试验,的确无法通过编译
那个时候我记得是做了试验才写的,至少vc9(vs)就编译不了,所以才会列出能通过的编译器
关于不写参数类型而默认视为int,这个应该没有纳入到规范里,所以既没实用价值也没什么参考价值,只是顺道提一下
还是谢谢你的指正