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
来解析
那么在这里相比大家可以清楚的看到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的编译器,所以和其他的地方可能有出入。具体的,大家可以去参阅各个编译器的调用归约
同样的,CSAPP中也提到了调用相关的个数,大家可以去查阅
然后当我们的gp_offset
或者fp_offset
用完的时候,也就证明寄存器中的参数用完了, 这时候就要用栈上的参数了,这时候我们使用overflow_arg_area
,并且每次都让overflow_arg_area
加上对应的值
这里,我们来验证一下刚才的说法
有关overflow的实验大家可以自己试一下
我们可以看到,调用了两次int,我们的指针增加了两次8
然后调用了一次double,fp增加了一个16
到这里,变参列表的内部原理我们就都已经搞清楚了。
当然了,这里还是不能和模板中的变参列表弄混。
文章评论