va_list 可变参数宏,同标识符…相同,用于传递可变参数
当函数需要传递的参数个数不能确定时,如 printf,使用…声明接下来的多个参数,
在函数实现中使用va_list、va_arg等宏取出参数使用
具体使用方法如下
void func(first_type first_arg, ...){
va_list argptr;//声明参数列表指针
va_start(argptr, first_arg);//初始化参数列表指针,将其指向第二个参数
second_type var = va_arg(list, second_type);//将参数按类型取出,并将指针指向下一个参数
//int intval = va_arg(list, int);
...
va_end(list);//list = NULL,使用结束将指针悬空
}
可以看出,可变参数要求我们必须传递第一个参数,并且需要知道参数的个数和类型才能使用
int printf(const char *fomat, ...)
对于 printf 来说,它从第一个参数——格式化字符串中对未知个数的参数进行处理,并得知类型信息
以下是 va_list 在标准库中的一些使用,比如可以自己封装I/O函数
int vscanf(const char *format, va_list ap); // 从标准输入/输出格式化字符串
int vfscanf(FILE *stream, const char *format, va_list ap); // 从文件流
int vsscanf(char *s, const char *format, va_list ap); // 从字符串
C/C++参考手册给出的使用示例
void error( char *fmt, ... ) {
va_list args;
va_start(args, fmt);
fprintf(stderr, "Error: ");
vfprintf(stderr, fmt, args);
fprintf(stderr, "\n");
va_end(args);
exit(1);
}
//Intel x64, VC/gcc 环境下可以得到预期效果
void var_args_func(const char * fmt, ...){
uintptr_t ap = ((char*)&fmt) + sizeof(uintptr_t);
va_list list;
va_start(list, fmt);
if (list == ap) { printf("equal\n"); }
printf("list = %p\n ap = %p\n", list, ap);
printf("%d\n", va_arg(list, int));
printf("%d\n", va_arg(list, int));
printf("%s\n", va_arg(list, char *));
va_end(list);//list = NULL
printf("%d\n", *(int*)ap);
ap += sizeof(uintptr_t);
printf("%d\n", *(int*)ap);
ap += sizeof(uintptr_t);
printf("%s\n", *((char**)ap));
}
int main(){
var_args_func("%d %d %s\n", 0, 1, "hello world");
}
C函数的默认调用方式将函数参数从右向左保存在栈上(由高地址向低地址),
这里从第一个参数的地址入手,依次将指针偏移,访问栈中的每个参数,
但是实际上,参数的压栈要遵循内存对齐的规则,恰巧x64、VC/gcc 环境下参数在栈中的位置是按pointer大小对齐的,
这里不加修饰地偏移指针去访问参数才能得到正确结果
这里只介绍x86及x64的实现
此处的 va_copy 是C99的新增内容,用于 va_list 间的复制,可以看到VC的实现即是简单的赋值
继续向下看
这里定义了va_list,可以看到它实际上是一个char *
接下来的实现对x86与x64架构做了不同的处理
__crt_va_start_a(ap, x) 被扩展为函数调用 ((void)(__va_start(&ap,x)))
//这里有一个__va_start 函数第二个参数为 … 的问题
即 va_list 指针与函数第一个参数传递给 __va_start
按上面的实验代码,可以将 __va_start 简单看为由第一个参数获得参数列表的地址,
并将 ap 指向下一个参数
接下来 __crt_va_arg(ap,t)
当变量所占字节数超过8或者3、5、7时,扩展为
**(t **)((ap += 8) - 8)
表明此时栈中保存的不是参数本身,而是参数的指针
当变量字节数为1、2、4、6、8时,扩展为
*(t*)((ap += 8)-8)
即参数被保存在栈上,且按8字节对齐(将参数"压栈"后,不足8字节的用零占位)
最后 va_end 将 ap 悬空
C中_ADDRESSOF宏的实现就是简单的取址
可以想到,_INTSIZEOF 宏的作用是用来取到字节对齐的偏移量,_int size,得到 int 整数倍的字节数
看一下它的具体操作
((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
要将 x 向上取为数字 n 的整数倍时,可以将 x 表示为 kn + r,r ∈ [0, n-1]
容易想到将 (kn + r + n-1)/n 即可得到向上取整的倍数k(k = k 或 k+1)
相当于 (x + n-1)/n*n,n 为2的 m 次方时,(x + n-1)/n*n 即为 ((x + n-1)>>m)<
((sizeof(n) + 3) & 0xFC //1111 1100
即x86、VC下,从右向左将参数依次保存在栈帧上,
__va_start_a 将 ap 指向参数 v 的下一个参数的实际起始地址
__va_arg 将 ap 移动到下一个参数位置并返回当前参数地址
__va_end 同样是将指针悬空
GNU下,va_list 及 va_arg 等宏实现如下
实现为gcc内部数据结构及函数,原理应当类似,暂且不表
进一步了解 内存对齐
进一步了解 函数调用过程与栈帧
2019/12/7