2020年7月14日

可变参数函数的声明使用省略号作为最后一个参数,例如 int printf(const char * format,...); 有关语法和自动参数转换的更多详细信息,请参阅可变参数。

从函数体访问可变参数使用以下宏:

以下宏在<stdarg.h>中定义

 

va_start

允许访问可变参数函数参数(函数宏)

va_arg

访问下一个可变参数函数参数(函数宏)

va_copy(C99)

制作可变参数函数参数(函数宏)的副本

va_end

结束可变参数函数参数(函数宏)的遍历

va_list(变量)

存储可变参数的变量

它的实现原理利用了内存的压栈技术,将参数压入(push)栈内,使用时,再逐个从栈里pop出来。需要注意的是,压栈的顺序是从最右边参数开始的,再向左逐个压入,根据栈的原理,在取参数时,就从第一个可变参数开始了。

使用方法

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

注意事项

  1. 定义的函数必须至少有一个固定参数;这么做的目的很明显,就是要定位变参的首地址,也就是固定参数地址+sizeof(固定参数类型)啦;
  2. 固定参数和可变参数之间可以没有任何关系;虽然经典的printf函数中,第一个固定参数里(格式化字符串)包含了后续变参的类型,但是这仅仅是函数功能的需要,语法上没有任何要求。

附上几个例子

例1

#include <stdio.h>
#include <stdarg.h>

void simple_printf(const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    while (*fmt != '\0') {
        if (*fmt == 'd') {
            int i = va_arg(args, int);
            printf("%d\n", i);
        } else if (*fmt == 'c') {
            // note : automatic conversion to integral type
            int c = va_arg(args, int);
            printf("%c\n", c);
        } else if (*fmt == 'f') {
            double d = va_arg(args, double);
            printf("%f\n", d);
        }
        ++fmt;
    }

    va_end(args);
}

int main(void)
{
    simple_printf("dcff", 3, 'a', 1.999, 42.5);
}

输出:
    3
    a
    1.999000
    42.500000

PS:在接收char的时候,会进行默认参数提升,变成int,所以需要以int型来接收char,同样,float也需要以double来接收,否则会出现编译错误。

E:\Clion\shiyan1\main.c: In function 'simple_printf':
E:\Clion\shiyan1\main.c:36:34: warning: 'char' is promoted to 'int' when passed through '...'
             int c = va_arg(args, char);
                                  ^
E:\Clion\shiyan1\main.c:36:34: note: (so you should pass 'int' not 'char' to 'va_arg')
E:\Clion\shiyan1\main.c:36:34: note: if this code is reached, the program will abort

那么C语言中什么时候会牵扯到默认参数提升呢?

在C语言中,调用一个不带原型声明的函数时:调用者会对每个参数执行“默认实际参数提升(default argument promotions)。

同时,对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。

提升工作如下:

——float类型的实际参数将提升到double

——char、short和相应的signed、unsigned类型的实际参数提升到int

——如果int不能存储原值,则提升到unsigned int 关于默认参数提升

例2

#if 1
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>

void simple_printf_2(const char *fmt,...){
    va_list args;
    va_start(args, fmt);
    int d;
    char c;
    float f;
    for(;'\0'!= *fmt;++fmt){
        if('%' != *fmt)
        {
            putchar(*fmt);
            continue;
        }else if(*fmt == '\r' || *fmt == '\n'){
            putchar(*fmt);
            continue;
        }else{
            ++fmt;
            if('\0'== *fmt){
                assert(0);
                break;
            }
            switch(*fmt){
                case '%':
                {
                    putchar('%');
                    break;
                }
                case 'd':
                {
                    d = va_arg(args, int);
                    printf("%d",d);
                    break;
                }
                case 'c':
                {
                    c = va_arg(args, int);
                    printf("%c",c);
                    break;
                }
                case 'f':
                {
                    f = (float)va_arg(args, double);
                    printf("%f",f);
                    break;
                }

            }
        }
    }
    va_end(args);
}
int main(void)
{
    simple_printf_2("jzywdsqa\r\n%djzy\r\n%cjzy\r\n%fdcff\r\n%f", 3, 'a', 1.999, 42.5);
}
#endif

输出
    jzywdsqa
    3jzy
    ajzy
    1.999000dcff
    42.500000

例3

#include <stdio.h>
#include <stdarg.h>
#include <math.h>
 
double sample_stddev(int count, ...) 
{
    /* Compute the mean with args1. */
    double sum = 0;
    va_list args1;
    va_start(args1, count);
    va_list args2;
    va_copy(args2, args1);   /* copy va_list object */
    for (int i = 0; i < count; ++i) {
        double num = va_arg(args1, double);
        sum += num;
    }
    va_end(args1);
    double mean = sum / count;
 
    /* Compute standard deviation with args2 and mean. */
    double sum_sq_diff = 0;
    for (int i = 0; i < count; ++i) {
        double num = va_arg(args2, double);
        sum_sq_diff += (num-mean) * (num-mean);
    }
    va_end(args2);
    return sqrt(sum_sq_diff / count);
}
 
int main(void) 
{
    printf("%f\n", sample_stddev(4, 25.0, 27.3, 26.9, 25.7));
}

输出:
   0.920258

最后还需要说明两点。首先,上面讲的内容与C语言的实现有关,而C语言的实现又依赖于cpu和操作系统,所以并不适用于所有的计算机,而且随机器的不同会由很大的区别。在<stdarg.h>内会看到大量的条件编译就是这个原因。其次,虽然函数定义中使用可变参数列表提供了很大的灵活性,但是对可变部分的参数C语言编译器不会进行类型检查,所以程序中要特别小心,确保参数的传递和接受是正确的。