C stdarg.h:可变参数va_list、va_arg等宏的使用及原理简介

标准库的使用

va_list、va_arg宏及 …的使用

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);
}

va_list宏的实现原理

//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大小对齐的,
这里不加修饰地偏移指针去访问参数才能得到正确结果


va_list、va_arg在VC中的具体实现

这里只介绍x86及x64的实现
C stdarg.h:可变参数va_list、va_arg等宏的使用及原理简介_第1张图片
此处的 va_copy 是C99的新增内容,用于 va_list 间的复制,可以看到VC的实现即是简单的赋值

继续向下看
__crt_va_start
C stdarg.h:可变参数va_list、va_arg等宏的使用及原理简介_第2张图片
这里定义了va_list,可以看到它实际上是一个char *

接下来的实现对x86与x64架构做了不同的处理

x64实现

C stdarg.h:可变参数va_list、va_arg等宏的使用及原理简介_第3张图片
__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 悬空

x86实现

C stdarg.h:可变参数va_list、va_arg等宏的使用及原理简介_第4张图片
_ADDRESSOF宏
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)< 即相当于将低 m 位置零

((sizeof(n) + 3) & 0xFC //1111 1100

即x86、VC下,从右向左将参数依次保存在栈帧上,
__va_start_a 将 ap 指向参数 v 的下一个参数的实际起始地址
__va_arg 将 ap 移动到下一个参数位置并返回当前参数地址
__va_end 同样是将指针悬空


GNU下,va_list 及 va_arg 等宏实现如下
va_list
C stdarg.h:可变参数va_list、va_arg等宏的使用及原理简介_第5张图片
实现为gcc内部数据结构及函数,原理应当类似,暂且不表


进一步了解 内存对齐
进一步了解 函数调用过程与栈帧


2019/12/7

你可能感兴趣的:(#,C语言,可变参数,va_list,va_start,va_arg,实现)