可变参数的使用
在写C代码的时候,有的时候会想要一个函数可接受变化数目的参数。类似C stdio库中的printf
syslog
等函数。于是就想着如何像标准库一样实现一些自己的可变参数函数呢?
比如printf函数,它还存在一个vprintf版本,定义如下:
#include <stdio.h>
int printf(const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
通过linux上command
man 3 printf
可查到
可发现,可变参函数定义中,使用...
来表示可变的参数表。 在vprintf函数中涉及到可变参的标准头文件<stdarg.h>
,以及一个宏定义va_list
。
1. stdarg.h 相关定义
man 3 va_arg
, 可发现有如下三个定义(可以是宏或者函数):
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
其实你完全可以参考
man 3 va_arg
的手册来了解可变参C代码。
- va_list: 是一个结构体,保存可变参数需要的相关结构,最主要的是当前参数的地址
- va_start:
last
代表可变参之前的那个参数,va_start
使ap指向last
参数的后一个参数的地址 - va_arg: 根据
type
类型取出并转换当前的参数,然后将ap后移对应长度 - va_end: 必须与
va_start
成对调用,因为有可能宏va_start
有一个{
等操作,所以必须成对出现。 - va_copy: 拷贝一个va_list到另一va_list, 相当于
*dest = *src
, 因为部分系统不支持该操作所以定义一个宏
具体宏定义可参考 C语言中可变参数的用法, 下面是x86上的对应定义
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //第一个可选参数地址
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址
#define va_end(ap) ( ap = (va_list)0 ) // 将指针置为无效
2. example
下面是一个根据fmt
字符串parse参数的例子,类似printf函数。
#include <stdio.h>
#include <stdarg.h>
void foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *s;
va_start(ap, fmt);
while (*fmt)
switch (*fmt++) {
case 's': /* string */
s = va_arg(ap, char *);
printf("string %s\n", s);
break;
case 'd': /* int */
d = va_arg(ap, int);
printf("int %d\n", d);
break;
case 'c': /* char */
/* need a cast here since va_arg only
takes fully promoted types */
c = (char) va_arg(ap, int);
printf("char %c\n", c);
break;
}
va_end(ap);
}
- 首先使用
va_list
声明一个结构ap用来遍历函数的参数栈(在汇编代码中,call一个函数需要将其参数压栈) - 然后使用
va_start
宏,将ap指向fmt变量后面的地址,也就是第一个可变参的位置 - 然后
va_arg
根据type取出对应的值,并且将ap的指针往后移动该type大小 - 循环上一步动作直到结束
- 调用
va_end
至于怎么判断结束,可以根据fmt中字符提示,也可以通过让最后一个参数等于0,检测参数是否为0实现。
3. vprintf、vsyslog
根据man手册,这些函数跟printf的区别是,它使用va_list
作为参数,而不是...
。如下code所示:
#include <stdio.h>
#include <stdarg.h>
void WriteFrmtd(char *format, ...)
{
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
int main ()
{
WriteFrmtd("%d variable argument\n", 1);
WriteFrmtd("%d variable %s\n", 2, "arguments");
return(0);
}
调用前需要先自己初始化va_list
,并需要自己调用va_start
va_end
。
4. 为什么已经有了printf函数还需要vprintf函数呢?
首先一定肯定是可以自定义,可以改造下printf的功能,前后做一些个处理。比如在va_start时候,自定义打印一些消息; 更灵活的控制打印参数,可以自定义从哪个参数开始打,等等。
另外一个我觉得很重要的原因是,printf可变参数函数,函数指针格式不确定,但是vprintf函数指针格式固定。可作为log的callback函数实现log的定制。
例如,我程序里面定义一个log函数,如下:
typedef int (*print_cb_t)(char *fmt, va_list ap);
print_cb_t vprint_cb = vprintf;
//print_cb_t vprint_cb = vsyslog;
//print_cb_t vprint_cb = vfprintf;
void log(char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
print_cb(fmt, ap);
va_end();
}
只要给vprint_cb赋值为不同的函数,就可以控制log函数往哪里输出了。是一种泛型的方式。
此处只是举例,实际上vsyslog vfprintf的函数格式与该指针并不同,你需要再包一层函数