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。
  

使用方法

  1. 先定义一个va_list类型的变量,比如ptr,它指向参数列表的首地址;
  2. 用va_start宏初始化ptr,它的第二个参数是第一个可变参数的前一个参数,有点拗口,其实就是最后一个固定参数啦;
  3. 用va_arg返回可变的参数,它的第二个参数是要获取的变参类型;
  4. 可以通过反复调用va_arg来依次取得变参值;
  5. 最后用va_end宏结束可变参数的获取;

注意事项

  1. 定义的函数必须至少有一个固定参数;这么做的目的很明显,就是要定位变参的首地址,也就是固定参数地址+sizeof(固定参数类型)啦;
  2. 固定参数和可变参数之间可以没有任何关系;虽然经典的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) 
  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值