C语言中可变参数的使用方法
写在前面
其实,可变参数这个东东自从入门C语言开始就一直在使用,最经典的就是printf打印输出。不论是从事嵌入式开发,还是搞Android的NDK开发,经常会用到可变参数输出log,但是很多时候是用别人封装好的API,而忽略了事情的本质。
需求
平时我们写C语言函数时,一般是固定参数的,但是像打印输出格式化内容时,其参数个数就不确定了,类似如下:
printf("This is a sample, %s, %d, %u", "variable-argument", -20, 50);
对printf函数来说,需要格式化的内容是怎样的形式,完全由程序员决定,这就导致参数个数不定(从第二个参数开始,第一个参数是固定的字符串类型),因此printf的函数原型只能采用可变参数的形式。下面是C语言中printf原型:
extern int printf(const char *format,...);
va_start、va_arg、va_end和va_list
要实现可变参数功能,不得不介绍va_start、va_arg、va_end和va_list这4个宏,这是头文件stdarg.h中定义的,va在这里是variable-argument(可变参数)的意思。
typedef int *va_list[1];
#define va_start(ap, parmN) (void)(*(ap) = __va_start(parmN))
#define va_arg(ap, type) __va_arg(*(ap), type)
#define va_end(ap) ((void)(*(ap) = 0))
它的实现原理利用了内存的压栈技术,将参数压入(push)栈内,使用时,再逐个从栈里pop出来(这个有点像高级语言里的序列化?)。需要注意的是,压栈的顺序是从最右边参数开始的,再向左逐个压入,根据栈的原理,在取参数时,就从第一个可变参数开始了。具体实现我们就不做分析了,下一步,看看怎么使用这组宏。
另外:va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。
使用方法
- 先定义一个va_list类型的变量,比如ptr,它指向参数列表的首地址;
- 用va_start宏初始化ptr,它的第二个参数是第一个可变参数的前一个参数,有点拗口,其实就是最后一个固定参数啦;
- 用va_arg返回可变的参数,它的第二个参数是要获取的变参类型;
- 可以通过反复调用va_arg来依次取得变参值;
- 最后用va_end宏结束可变参数的获取;
注意事项
- 定义的函数必须至少有一个固定参数;这么做的目的很明显,就是要定位变参的首地址,也就是固定参数地址+sizeof(固定参数类型)啦;
- 固定参数和可变参数之间可以没有任何关系;虽然经典的printf函数中,第一个固定参数里(格式化字符串)包含了后续变参的类型,但是这仅仅是函数功能的需要,语法上没有任何要求,下面的例子就可以证明这一点;
例子
直接看代码:
#include <stdarg.h>
#include <stdio.h>
void variable_argument(int fix_argument1, int fix_argument2, ...)
{
va_list ptr;
va_start(ptr, fix_argument2);
int first = va_arg(ptr, int);
int second = va_arg(ptr, int);
int third = va_arg(ptr, int);
char* four = va_arg(ptr, char*);
float five = va_arg(ptr, double);
va_end(ptr);
printf("first is %d, second is %d, third is %d, four is %s, five is %f\n", first, second, third, four, five);
return;
}
int main(int argc, char** argv[])
{
variable_argument(1, 2, 5, 2, -5, "Hello", 3.14159);
return 0;
}
编译及运行结果如下:
ffmpeg@ubuntu:~/work/test$ gcc variable-argument.c -o variable-argument
ffmpeg@ubuntu:~/work/test$ ./variable-argument
first is 5, second is 2, third is -5, four is Hello, five is 3.141590
注意:
1. va_start的第二个参数最好是最后一个固定参数,而不是其他的固定参数,否则编译时可能报warning;但是如果是其他固定参数,运行结果也对,但考虑到不同编译器差异,最好用最后一个;
2. 第五个变参,虽然我们定义的是float类型,但是va_arg的第二个参数必须是double类型,这是因为编译器进行了精度提升(prompt),下面是编译时的warning说明;
warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
总结
可变参数可以应用在一些特殊灵活的场景中,最常见的还是在log管理系统中,包括Android的native开发里,也有很多类似实现,我们需要掌握它的基本使用方法,便于分析解决实际问题。
补充
在C99标准之前,无法在宏定义中使用可变参数定义,C99之后可以通过__VA_ARGS__
来代替可变参数,即省略号 “…” ,例如:
#define debug(...) printf(__VA_ARGS__)
#define debug(format, ...) fprintf(stdout, format, __VA_ARGS__)
#define debug(format, args...) fprintf(stdout, format, args)
需要注意的是,上述例子的可变参数不能省略,如果省略则可变参数前会残留一个逗号,编译会报错。
现在,有必要提一下“##”连接符号的用法,“##”的作用是对token进行连接,上例中format,args,__VA_ARGS都可以看作是token,如果token为空,“##”则不进行连接,所以允许省略可变参数。对上述例子重新改造如下:
#define debug(...) printf(##__VA_ARGS__)
#define debug(format, ...) fprintf(stdout, format, ##__VA_ARGS__)
#define debug(format, args...) fprintf(stdout, format, ##args)