文章目录
- 1.什么是可变参数列表
- 2.可变参数列表的分析与使用
- 2.1使用
- 2.2分析原理
- 2.3分析原码
1.什么是可变参数列表
对于一般的函数而言,参数列表都是固定的,而且各个参数之间用逗号进行分开。这种函数在调用的时候,必须严格按照参数列表中参数的个数进行传参,否则编译器就会报错。
如果现在我要求2个数中的最大值,那么就可以这样写:
现在我的需求变了,我要求5个数的最大值,那怎么写?
如果现在我要求6个数的最大值呢? 你还要将代码继续写下去吗,那也太麻烦了吧,我的需求变一点点,你的代码就得变。
当然,也可以先将数放在一个大的数组里面。但是现在不让你使用数组,那你该怎么办呢?----使用可变参数列表
此处的...
就是可变参数列表,num表示传入参数的个数。
可变参数至少有一个明确的参数。…表示其它元素,其它元素可以有,也可以没有
那么如何使用可变参数列表呢?
2.可变参数列表的分析与使用
可变参数列表的使用需要借用四个宏。
- va_list
- va_start
- va_arg
- va_end
关于这四个宏的功能我们能后面会详细讲到。
2.1使用
注意事项:
- 可变参数必须从头到尾逐个访问,如果你一开始就想访问中间的元素,这是办不到的。
- 参数列表中至少有一个参数,如果一个都没有,则无法使用va_start
- 这几个宏是无法直接判断实际存在参数的个数的,必须给他传递参数个数
那不对呀,我们使用的printf就是使用的可变参数,那我们没有给它传递明确的参数呀?
其实我们给它传递参了参数的个数,我们的%d,%c等格式控制符就说明了我传递了几个参数。- 这些宏无法判断每个参数的类型。
2.2分析原理
接下来,我们就开始分析一下它的底层原理是如何实现的:
在了解过函数栈帧的形成后,我们知道函数调用时是会进行参数传递的;而且参数在栈帧的形参过程中是从右向左入栈的。(函数栈帧的创建与销毁可看这里)
在汇编语言中,通过查看内存我们看见看到确实是这样的
此时我们我们的最后压入栈中的元素5,也就是num就在内存中的该位置:
此时我们先猜测一下,我的num就是我最后压入栈中的元素(在栈中的地址较小),那先前压入的元素,就在num上面。既然我能找到num元素,那我取出num的地址再加上一,不就指向先前压入的元素了;那我就能访问他们,再继续让指针移动,就可以将他们全部访问到。那到底是不是这样实现的呢?而且地址+1是加4/8个字节,那其它类型(char、short)是怎么办的呢?
下面我们就先来测试一下对于char类型,它是怎么做的:
在调试过程中给你,我们可以发现,char类型的数据在入栈是也是压入4个字节,为什么会这样呢?
movsx是什么汇编指令?我们以前都是用的mov
看到这我就明白了,char类型的数据会整型提升为整型,然后在压入栈中。
这样就可以实现,无论外部数据如何变化,该函数都可以让指针+4/8个字节找到数据了。
因此,通过汇编我们可以看到,在可变参数场景下:
- 实际传入的参数使char、short、float,在编译器编译的时候,会自动进行提升。
- 函数内部使用的时候,根据类型提取数据(更多的是通过int、double来进行)
2.3分析原码
- va_list
该类型,其实就是对char*的重命名,在此我们也就不赘述了。
- va_end
该宏的作用就是将我们的arg指针置为NULL了,避免了野指针。
- va_start
这里我们的编译器对它们进行了封装,而且该宏又调用了两个宏
将3个宏替换一下,就是下面的结果。
#define __crt_va_start_a(arg, num) ((void)(arg= (char*)(&(num)) + _INTSIZEOF(num)))
该宏是什么意思呢? 就是对num进行取地址,然后强转为char*指针,再+4,赋值给arg;最后将该指针强转为void类型。
这里为什么是+4呢?
- 这里的+4其实是为了让数据在内存中4字节对齐(向上取整)
这个_INTSIZEOF宏我们稍后再看。为什么强转为void类型
- 待…
执行va_start(arg, num),此时arg指针就指向了第一个元素
- va_arg
再执行int max = va_arg(arg, int);、
我们来看一下这个宏又是再干什么
#define __crt_va_arg(arg, int) (*(int*)((arg += _INTSIZEOF(int)) - _INTSIZEOF(int)))
该宏先执行画红线的部分,即先将arg向下动,完成指向下一个元素的任务,然后再用arg减去刚才移动的距离,又回到刚才的位置(注意:arg没回来),最后通过强制转换,提取出符合类型大小的数据
该宏有两个功能:
- arg指向下一个元素
- 使arg回指,然后取出地址中的内容
一行代码就执行了两个作用,很巧妙。
然后代码通过循环num-1次就遍历了所有元素。
- 下面我们就来研究一下_INTSIZEOF宏是如何计算指针走
_INTSIZEOF(n)的意思就是:计算一个最小数x,满足x>=n && x % 4 == 0 其实就是一种4字节的对齐方式。
例如:
- n是1,2,3,4,对n进行sizeof(int)最小整数被取整的问题 就是4。
- n是5,6,7,8,对n进行sizeof(int)最小整数被取整的问题 就是8。
那为什么要这样做呢?
- 因为我们的数据在入栈的时候,都是按照4/8字节对齐的方式存储的,既然存的时候是按照4字节对齐的方式存的,那你取的也要按照4字节对齐的方式取。
那该宏是怎么办到的呢?
既然是对齐到4的最小整数倍处,那么本质是:n对应的4的最小整数倍 = 4*m。对n=7来说,m就是2,4的最小整数倍(对齐数)就是8。
- 如果n能整除4,那么m就是n/4
- 如果n不能整除4,那么m就是n/4+1
上面两种情况如何合并为一种情况呢?
(n + sizeof(int) - 1)/sizeof(int) ---->(n + 4 - 1) / 4
- 如果n能整除4,那么m就是 (n+4-1)/ 4 ---->(n+3)/4,此时+3就不起作用,就是n/4
- 如果n不能整除4,那么n=最大整除4的部分+R(
R为n%4
), 1<=R<4。那么m就是 (n+4-1)/ 4 ---->(最大整除4的部分+R+3)/4,其中 4<=R+3 <7,那最后m就等于了n/4 + (R+3 ) / 4------>n/4+1
知道了一个数x是4的最小几倍,那求x对应的4的对齐数就是:
(n + sizeof(int) - 1)/sizeof(int) * sizeof(int) ---->((n+4-1)*4)/4--- 最小几倍 ---
现在和源码还不太一样,那我们写一个简洁版
设n+4-1 = w,那表达是就变为了( w / 4) * 4,而4就是2 * 2,那w/4不就相当于右移两位,w*4就相当于左移两位;先右移两位,在左移两位,最终的结果就是将最后两个比特位置为0了嘛!
需要这么麻烦嘛?
直接w & ~3就可以了呀
所以最终式子就变成了这样(n+4-1)& ~ (4-1),这不就跟源码一样了嘛
源码: