C 动态参数使用详解

2018-12-09 language c/cpp

在使用过程中,通常会有参数个数不确定,最常见的是 printf 这类的函数,只有在使用时才能确定参数的个数以及其类型。

这里详细介绍其使用以及基本原理。

简介

头文件 stdarg.h 中对相关的宏进行了定义,其基本内容如下所示:

typedef char * va_list;

#define _INTSIZEOF(n)       ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(arg_ptr,v) (arg_ptr = (va_list)&v + _INTSIZEOF(v))
#define va_arg(arg_ptr,t)   (*(t *)((arg_ptr += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define va_end(arg_ptr)     (arg_ptr = (va_list)0)

其中 _INTSIZEOF 是一个内部使用的宏,用户仅使用后面的三个宏定义,也就是 va_start() va_arg() va_end() 三个宏,使用示例如下所示:

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

int max(int num, ...)
{
    int tmp;
    int min = -0x7FFFFFFF; /* 32系统中最小的整数 */

    va_list ap;
    va_start(ap, num);
    for (int i = 0; i < num; i++) {
         tmp = va_arg(ap, int);
         if (tmp > min) {
            min = tmp;
         }
    }
    va_end(ap);
    return min;
}

int main(int argc, char *argv[])
{
    int n, m;

    n = max( 5, 5, 6, 3, 8, 5);
    m = max( 7, 5, 1, 9, 8, 5, 7, 0);

    printf("%d\t%d\n", n, m);

    return 0;
}

如上的示例中,通过第一个参数指定了入参的数量,同时也约定了类型。

使用方式

在使用时,其步骤如下:

  1. 调用之前定义一个 va_list 类型的变量,也就是字符串指针,一般变量名为 ap,用于指向堆栈。
  2. 通过 va_start(ap, var) 初始化 ap,指向可变参数列表中的第一个参数,其中 var 就是 ... 之前的那个参数。
  3. 接着调用 va_arg(ap, type) 依次返回指定类型参数,并将指针指向下个变量,第二个参数为获取参数的类型。
  4. 最后关闭定义的变量,实际上就是将指针赋值为 NULL,也就不再指向堆栈。

其中 ap 可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。

其中的使用关键是如何获取变量的类型,通常有两种方法:A) 提前约定好,如上面的示例;B) 通过入参判断,如 printf() 类型。

基本原理

函数传参是通过栈传递,保存时从右至左依次入栈,以函数 void func(int x, float y, char z) 为例,调用该函数时 z、y、x 依次入栈,理论上来说,只要知道任意一个变量地址,以及所有变量的类型,那么就可以通过指针移位获取到所有的输入变量。

另外,在 x86 的 CPU 中,每个变量的地址都是 sizeof(int) 的倍数,即使像 char 只占了一个字节,但是后面的参数会跳过三个字节,以四字节对齐。

简单来说,只要事先定义一个指针,每次读取一个参数,指针就向后移动一次即可。

源码解析

其中的 _INTSIZEOF 宏是用来将某个类型大小转换为 int 大小的整数,假设在 32bit 机器上 sizeof(int)=4 sizeof(long double)=10_INTSIZEOF(long double) 为 12,也就是上述的对齐策略。

通过 va_start() 获取了第一个参数,

使用示例

常见的用法还有获取省略号指定的参数,例如:

void foobar(char *str, size_t size, const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	_vsnprintf(str, size, fmt, ap);
	va_end(ap);
}

假设,在调用上述的函数时,如果在 _vsnprintf() 中会再调用类似的函数,那么可以通过 va_list args; va_copy(args, ap); 复制一份,这样即使原参数被修改也不影响。

va_list args;
va_copy(args, ap);
some_other_foobar(str, size, fmt, args);