在使用过程中,通常会有参数个数不确定,最常见的是 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;
}
如上的示例中,通过第一个参数指定了入参的数量,同时也约定了类型。
使用方式
在使用时,其步骤如下:
- 调用之前定义一个
va_list
类型的变量,也就是字符串指针,一般变量名为ap
,用于指向堆栈。 - 通过
va_start(ap, var)
初始化ap
,指向可变参数列表中的第一个参数,其中var
就是...
之前的那个参数。 - 接着调用
va_arg(ap, type)
依次返回指定类型参数,并将指针指向下个变量,第二个参数为获取参数的类型。 - 最后关闭定义的变量,实际上就是将指针赋值为
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);