这个知识点在大学里一般都不讲,翻阅所有的C语言入门书籍也没有相关的内容,但你不要认为它就那么遥不可以及,其实还是比较简单,知识有一点技术而已,废话少说,
我们用的最多的C函数是哪个?毫无疑问,是printf。但是你看过printf的声明式吗,那是相当诡异。随便拿一本带C库函数参考的书,可查到如下结果:
int printf(const char *format, ...);
那三个连续的点就代表大于或等于0个参数,再加上前面的format参数,所以printf函数至少要接受一个字符串,后面就随便了。但是这是如何实现的呢,不急,首先必须要了解C语言函数参数传递的机制。
C语言函数参数传递机制
我们知道,每个程序都有个用户栈,所有的函数调用都要使用它,比如被调用函数的实参及局部变量都要存在用户栈中,所以每个函数都有个栈帧。下面我用两个函数来说明这种调用机制:
int main(void) int add(int param1, int param2)
{ {
int a, b, c; int d;
a = 2; d = param1 + param2;
b = 3; return param1+param2;
c = add(a,b); }
return 0;
}
main函数在调用add函数时,main负责把b,a及eip(即返回地址)按顺序压栈,注意栈是向下增长的。然后执行call指令调用add函数。(ps: 其实在返回地址和局部变量d之间函数add还会压入一些寄存器,但是这对我们理解变长参数没有影响,总之要记住参数是自右向左压栈的)
如果上面说的你看不懂,那么jordanli22叫你记住:add(a,b);在前面的参数a的地址最小,&b比&a大(&b)= (char *)&a +sizeof(a);
处理变长参数的宏定义
处理变长参数说白了就是怎样获取那些在声明中没明说的参数,比如在printf("%d equals to %d.", a, b)中,第一个字符串参数可直接通过format取得,但a和b呢?这就要需要对参数传递的理解了。主函数调用printf时肯定是先压入b,再压入a,最后压入"%d equals to %d"的地址。
现在我们来获取a的地址,很简单,当然是 (char*)&format+sizeof(const char*)。其中&format是参数format在栈中的地址,而a正好format上面。
OK, 现在我们可以系统地讲解处理变长参数的宏了。
实际上,C语言的大侠已经设计好了一个处理可变参数的头文件供我们使用了。#include <stdarg.h>
请打开这个头文件,我将帮大家解读一下这个头文件的使用方法。
在头文件stdarg.h中先定义个类型 va_list,它是专门用来取可变参数的,如果要对付可变参数首先要定义一个va_list类型的变量ap:
typedef char* va_list;
因为在获取参数地址时必须知道它前一个参数的数据类型大小,所以要定义_INTSIZEOF(n),它的作用和sizeof类似,不过__va_sizeof(char),__va_sizeof(short)的值都是4:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
举例如下:
char sizeof(char) = 1 _INTSIZEOF(char) = 4
short int sizeof(short int) = 2 _INTSIZEOF(short int) = 4
int sizeof(int) = 4 _INTSIZEOF(int) = 4
long int sizeof(long int) = 4 _INTSIZEOF(long int) = 4
float sizeof(float) = 4 _INTSIZEOF(float) = 4
double sizeof(float) = 8 _INTSIZEOF(double) = 8
然后定义va_start获取第一个可变参数的地址:
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
va_arg用于获取ap当前指向的参数的值,并顺带把va_list移向下一个参数
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
当用完ap时,用va_end将其赋值为空,也就是0
#define va_end(ap) ( ap = (va_list)0 )
处理变长参数的一般框架
如果我们自己实现printf函数,代码的形式大概如下
int chparamf(const char *format, ...)
{
va_list ap;
va_start(ap,format);
循环使用 va_arg(ap,你要取的参数的数据类型)
va_end(ap);
}
举个简单的例子:无穷个数(double类型)相加
#include <stdio.h>
#include <stdarg.h>
#define PN printf("\n")
double add(double cont,...)//cont表示加数的个数
{
int i = 0;
double sum = 0;
va_list ap;//char * ap;
va_start(ap,cont);//ap指向了cont后的变量的首地址
for(i = 0; i < cont; i++)
{
sum = sum + va_arg(ap,double);//取ap指向的地址的值,以double的格式来取,同 //时ap指向了下一个变量的首地址
}
va_end(ap);//赋值为空
return sum ;
}
int main()
{
double sum1 = add(3, 4.0, 5.0, 6.2);//如果这里写4系统默认以int来存储,int是4个字节, //但上面函数已经用double来取这个变量了,所以必须 //写4.0,使系统默认以double来存储,也可以以下面
//这种方式存储
printf("%lf",sum1);
PN;
double a,b,c,d;
a = 3, b = 4, c = 5, d = 6.1;//这时候4会被系统转化为4.0
sum1 = add(a, b, c, d);
printf("%lf",sum1);
PN;
return 0;
}