欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言中可变参数的使用方法

程序员文章站 2024-03-15 22:22:12
...

写在前面

其实,可变参数这个东东自从入门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)