张银峰的编程课堂

可变参数函数

C语言函数参数从右向左压栈的设计方式,延伸出了函数可以有不确定个数参数的特性,称为可变参数函数。我们经常使用的printf()就是一个可变参数函数,它有一个强制参数,即格式化字符串。格式化字符串中的转换修饰符决定了可选参数的数量和类型。

printf("glimix.com");
printf("glimix.com %s-%d", "zyf", 2026);

一个可变参数的函数,需要固定数量的强制参数,后面是数量可变的可选参数,可变参数用省略号(...)指定参数表,且只能有一个(...)参数,而且必须位于参数末尾。

void foo(...);              // 错误:没有强制参数
void foo(param_list, ... );  // 正确:有强制参数列表param_list

还记得之前我们曾使用一个char*指针访问函数所有参数的手法么?让我们故技重施,来把它应用到可变参数函数上,看看效果如何。

#include <stdio.h>

void param_list(int count, ...)
{
    int *p = &count;
    p += 1;             // 指向第一个可变元素

    printf("params: ");
    for (int i = 0; i < count; i++)
    {
        printf("%d, ", *p++);
    }
    printf("\n");
}

int main()
{
    param_list(5, -1, 9, 6, 1, 2 );
    param_list(7, 0, 2, -6, 1, 2, 5, 8 );
}

glimix.com

param_list()第一个参数count指定了后面元素的数量,并假定元素的类型都是int。然后使用int*与指针算术运算对数值逐一输出。对于指针算术运算,现在只需要知道,p += 1;p++;都是将指针p向前移动一个型型大小,即移动到下一个元素处。

从程序运行的结果图来看,好像一切都还不错。但是C语言对于可变参数的访问提供了更加安全的访问方式,这就是一系列是的va_xxx宏。你可能在想,既然我们能用char*指针加上一定的类型偏移量就能得到所有的参数,还用va_xxx宏做什么?事实上,上述做法都是在一种理想状态下,假如编译器实现者在可变参数的实现中,加上了一些辅助数据用于记录某些信息,那通过指针算法就不能得到正确的参数地址了。因此,为了保证统一性,请使用va_xxx宏。

宏 va_xxx

为了解析这些不确定的参数,你需要包括<stdarg.h>文件,然后使用其中定义的几个宏来完成需求。

typedef char* va_list;
void va_start(va_list ap, prev_param); /* ANSI version */
type va_arg(va_list ap, type);
void va_end(va_list ap);

va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。这与之前我们使用char*指针,通过指针算术运算,将函数参数逐一输出类似。在这些宏的实现中,va_list的作用与此类似。

下面是使用这些宏的步骤:glimix.com

  • 定义va_list类型的变量ap。
  • 使用va_start宏初始化ap,第一个参数是ap本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数,初始化的目的是使ap指向可变参数表的第一个参数,
  • 使用va_arg返回可变的参数,它的第一个参数是ap,第二个参数是要获取的参数的指定类型。当数据返回后,va_arg内部会把ap的位置指向变参表的下一个变量位置。
  • 最后用va_end宏结束可变参数的获取。如果函数有多个可变参数的,依次调用va_arg获取各个参数。
#include <stdio.h>
#include <stdarg.h>
#include <limits.h>

int max_list(char *msg, int count, ...)
{
    int temp;
    int max_value = INT_MIN;

    va_list ap;
    va_start(ap, count);

    for (int i = 0; i < count; i++)
    {
        temp = va_arg(ap, int);
        if (temp > max_value)
            max_value = temp;
    }

    va_end(ap);

    printf("%s ", msg);

    return max_value;
}

int main()
{
    printf("max value: %d\n", max_list("list 1", 6, 2, 5, 9, 6, 1, 2));
    printf("max value: %d\n", max_list("list 2", 7, 12, 25, -5, 59, 65, 11, 26));
}

glimix.com

注意:va_start 宏第二个参数要求是(...)之前的那个参数,即最后一个强制参数,在max_list中就是count。想想看,既然要获取的是可变参数列表中的值,那使用它的前一个位置是理所当然的。关于这点,语言标准中也是如此说明。

va_start

Variable arguments handling

void va_start (va_list ap, paramN);

paramN

  • Name of the last named parameter in the function definition.
  • The arguments extracted by subsequent calls to va_arg are those after paramN.

不可传递

最后记得,可变参数列表不可通过函数再次传递。

void error_exit(const char *fmt, ...)
{
}

void error_dump(const char *fmt, ...)
{
    // 错误,可变参数列表不可再次传递 
    error_exit(fmt, ...);
}


int main()
{
    error_dump("", 1, 3.14, "glimix.com");
}