More than code

More Than Code
The efficiency of your iteration of reading, practicing and thinking decides your understanding of the world.
  1. 首页
  2. daily
  3. 正文

Daily C/C++ 变参列表

2021年10月5日 557点热度 0人点赞 0条评论

Daily C/C++ 变参列表### Daily C/C++ 变参列表

昨天我发了一篇文章,里面有些许的涉及到了变参列表这个东西,今天就在这里好好说一下

参考文章

我们首先看printf,这个应该是我们平常用到的最多的函数之一了

/* Write formatted output to stdout from the format string FORMAT.  */
/* VARARGS1 */
int
__printf (const char *format, ...)
{
  va_list arg;
  int done;
  va_start (arg, format);
  done = __vfprintf_internal (stdout, format, arg, 0);
  va_end (arg);
  return done;
}

这个是他的源码,忽略掉那个vfprintf_internal,其实我们要看的就只有va_list,va_start,va_end和va_arg

其实还有一个va_copy,相比看也能看出来是复制的作用,我们这里就不多说了。

可以看到printf上面的定义中,参数列表里有...,这就代表了要使用变参列表

变参列表的原理也简单,我们获取参数列表起始地址的指针,然后根据需要一步一步的去指针对应的地址中取值即可

那么参数列表起始地址的指针,就是我们正常的最后一个参数的位置,在这里可以看到是format

(这里有一个点,就是其实我们把获取参数列表理解成从最后一个正常的参数中获取地址,是因为把这块的模型看成了都在栈上。实际上我们的参数也会在寄存器中储存,不过这个就交给编译器来就行)

所以我们使用va_list声明一个参数列表,然后使用va_start来初始化这个参数列表

然后我们就可以正常的使用参数了,printf中我们是直接把arg作为一个参数传给了vfprintf,这样在vfprintf里就可以使用这些参数。

那么到这里,我们就可以写一个自己的简单的printf函数了

void my_print(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stdout, format, args);
    va_end(args);
}

这里我们解析了参数后,将变参列表传给了vfprintf,他接收这样的参数并帮我们解析,然后输出到stdout中

那么有的同学可能就会想,你得到了arg,但是我们要怎么获取具体的值呢?你这样给了vfprintf不还是一个va_list的变量吗?

上面说过,原理就是移动指针和对应类型的解析,这一步标准库已经帮我们封装好了。

而我们要做的就是使用va_arg来解析

20211005095714

那么在这里相比大家可以清楚的看到va_arg是怎么用的了

那么再进一步,va_list是什么东西呢?

stackoverflow中这样回答

typedef struct {
   unsigned int gp_offset;
   unsigned int fp_offset;
   void *overflow_arg_area;
   void *reg_save_area;
} va_list[1];

va_list中储存了我们使用变参列表的必要信息

那么为什么要在后面加上一个[1]呢?因为我们在使用的时候要改变这个元素中的值,所以传参的时候都是传的指针,这里就是一个单元素的数组

那么我们看下面这几个成员变量

reg_save_area储存了一个储存寄存器中参数的区域的地址。(中文貌似表达的不好,就是a pointer to the area where stores the argument from register)

overflow_arg_area,看到名字我们也可以联想到,overflow其实就是说寄存器的溢出,也就是储存在栈中的那些参数的位置

gp_offset指的是一个偏移量,就是general purpose argument的偏移量,具体的就是整形或者指针形等,一会儿对比一下就明白了。

fp_offset指的就是float point argument的偏移量了,也就是浮点数的偏移量

那么这两个偏移量怎么用呢?我们在使用的时候,对于general purpose的变量,我们使用reg_save_area + gp_offset的地址来获取参数,然后将gp_offset的值加8。对于float point的变量,我们使用的是reg_save_area + fp_offset的地址来得到浮点数

其中,gp_offset的最大值是48,因为我们寄存器是8个字节,我们最多有6个寄存器用来传递参数,所以就是48的偏移量

而fp_offset的初始值就是48,每次读完一个浮点数就让他增加16,因为浮点数寄存器的大小是128位的。至于最大值,我认为应该是(48 + 16 * 8 = 176),这个和上面两个参考文章都不相同。因为我这里说的是gcc的编译器,所以和其他的地方可能有出入。具体的,大家可以去参阅各个编译器的调用归约

20211005102055

同样的,CSAPP中也提到了调用相关的个数,大家可以去查阅

然后当我们的gp_offset或者fp_offset用完的时候,也就证明寄存器中的参数用完了, 这时候就要用栈上的参数了,这时候我们使用overflow_arg_area,并且每次都让overflow_arg_area加上对应的值

这里,我们来验证一下刚才的说法

20211005102557

有关overflow的实验大家可以自己试一下

我们可以看到,调用了两次int,我们的指针增加了两次8

然后调用了一次double,fp增加了一个16

到这里,变参列表的内部原理我们就都已经搞清楚了。

当然了,这里还是不能和模板中的变参列表弄混。

标签: c++
最后更新:2021年10月5日

sheep

think again

点赞
< 上一篇
下一篇 >

文章评论

取消回复

COPYRIGHT © 2021 heavensheep.xyz. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS