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


    <stdarg.h>标准库的使用

    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, "
    ");
    	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
    "); }
        printf("list = %p
      ap = %p
    ", list, ap);
        
        printf("%d
    ", va_arg(list, int));
        printf("%d
    ", va_arg(list, int));
        printf("%s
    ", va_arg(list, char *));
    
        va_end(list);//list = NULL
    
        printf("%d
    ", *(int*)ap);
    
        ap += sizeof(uintptr_t);
        printf("%d
    ", *(int*)ap);
    
        ap += sizeof(uintptr_t);
        printf("%s
    ", *((char**)ap));
    }
    
    int main(){
    	var_args_func("%d %d %s
    ", 0, 1, "hello world");
    }
    

    C函数的默认调用方式将函数参数从右向左保存在栈上(由高地址向低地址),
    这里从第一个参数的地址入手,依次将指针偏移,访问栈中的每个参数,
    但是实际上,参数的压栈要遵循内存对齐的规则,恰巧x64、VC/gcc 环境下参数在栈中的位置是按pointer大小对齐的,
    这里不加修饰地偏移指针去访问参数才能得到正确结果


    va_list、va_arg在VC中的具体实现

    这里只介绍x86及x64的实现
    定义
    此处的 va_copy 是C99的新增内容,用于 va_list 间的复制,可以看到VC的实现即是简单的赋值

    继续向下看
    __crt_va_start
    va_list
    这里定义了va_list,可以看到它实际上是一个char *

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

    x64实现

    va_list及va_arg的x64VC实现
    __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实现

    va_list及va_arg的x86VC实现
    _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
    即相当于将低 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
    GNU 可变参数实现
    实现为gcc内部数据结构及函数,原理应当类似,暂且不表


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


    2019/12/7

  • 相关阅读:
    典型用户模版和场景
    第一冲刺阶段——个人工作总结05
    第一冲刺阶段——个人工作总结04
    第一冲刺阶段——个人工作总结03
    第一冲刺阶段——个人工作总结02
    第一冲刺阶段——个人工作总结01
    学习进度条7
    构建之法阅读笔记06
    个人总结
    第十六周进度条
  • 原文地址:https://www.cnblogs.com/kafm/p/12721794.html
Copyright © 2020-2023  润新知