前面提及参数为空的函数默认是变参函数。那么变参函数如何使用?
(我们需要<stdarg.h>头文件)
1、我们使用va_系列函数/宏来操纵可变参数。va_系列函数包含va_start,va_arg,va_end,使用的类型包括va_list。
如何使用?
va是Variable Arguments的首字母缩写。
下面是一个计算多个值得和的函数。
#include <stdarg.h>
int Sum(int n, ...){
int i;
int sum = 0;
va_list mark;
va_start(mark, n);
for (i = 0; i < n; ++i){
sum += va_arg(mark, int);
}
return sum;
}
int main(){
printf("%d\n", Sum( 4, 1, 2, 3, 4));
return 0;
}
解释上面的代码:
一、mark用来记录当前解析到那个参数了。刚创建的时候指向NULL。
二、va_start的作用是让mark指向第二个参数的地址。
三、va_arg的作用是将当前mark指向的参数转变为 int 类型,返回给调用者,并将mark执行下一个参数。
其实还有一些问题:为什么va_start可以将 mark 指向第二个参数地址?va_start总是根据 n 获得了第一个参数的地址(使用取值运算符),但是并不知道 n 的字节数呀。
是这样的,C语言函数所有参数都是4个字节(对于32为程序来说)。
所有第二个参数位置就是第一个参数的内存地址+4。后面的va_arg也就是将mark加上4来获取下一个参数的位置的。
看下面这段没有使用va_系列函数/宏的等效代码:
#include <stdarg.h>
int Sum(int n, ...){
int i;
int sum = 0;
char * mark = NULL;
mark = ((char*)& n);
mark += 4;
for (i = 0; i < n; ++i){
sum += * (int*) mark;
mark += 4;
}
mark = 0;
return sum;
}
int main(){
printf("%d\n", Sum( 4, 1, 2, 3, 4));
return 0;
}
看完后,您应该清楚va_系列函数究竟是怎么回事儿了。
您还有疑问?为什么是+4而不是-4?栈明明是向下生长的呀?
是这样的:您肯定知道C函数参数是从右往左压栈的,那您知道为什么吗?正是为了更好地实现变参函数。假设您知道函数栈的结构,那您就应该能想清楚,如果按照从右往左的顺序压栈,那么左边的参数相对于EBP的偏移就是固定的,那么就可以为变参函数生成统一的代码。
试想,如果是从左往右压栈,最左边的参数相对于EBP的偏移随着参数个数会改变。假设第一个参数是有名参数,您在变参函数中通过名称访问了第一个参数,编译器该如何为您生成代码呢?难道还要根据调用情况去生成函数的代码?可行,却不现实。
(这里还关联到函数参数计算顺序问题,这是一个老生常谈的问题了,其特性依赖于具体编译器的实现,还是不要了解并避免这个问题比较好)
您可能注意到了我没有使用 va_end这个函数,这是不好的。您可以在看看上面的等效代码,里面就有va_end,就是那句 mark = 0。你可以看到,va_end的作用是将mark重置为初始状态,并没有释放什么内存或者做什么高端的操作,常常没有丝毫作用。不过为了良好的编程风格,写上无妨。
2、常用变参函数有printf,fprintf,sprintf,vprintf,vfprintf,vsprintf。
v系列函数只是将可变参数列表...替换成了va_list类型参数。va_list参数指向...所代表的第一个参数的内存地址。如
vprintf对应于printf,他们的类型分别是:
int vsprintf( char *, va_list);
int sprintf( char *, ...); // 这个就不举例子了
下面给出一个vsprintf的示例,在一些项目中很常见。
// 自定义的添加日志函数
void LogAppend( char * format, ... ) {
// 可以在这里先输出时间信息到文件中
va_list marker;
va_start( marker, format);
char buf[1024];
vsprintf( buf, format, marker);
// 最后将buf写入文件中
}
您可能想到了,上面的函数类似于 fprintf。上述函数较fprintf的优势在于,您可以向输出信息中添加更多的自定义讯息,典型的有 日志记录的时间。