可变参数的使用

可变参数的使用

在写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的函数格式与该指针并不同,你需要再包一层函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值