demo 2:求任意多个数据中的最大值(至少一个),要求不能使用数组
因为目前参数个数不确定,那么函数编写的时候,参数个数也无法确定,换句话说,函数也就没法编写
不过,C提供了满足该场景的解决方案:可变参数列表
使用
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdarg.h>
int FindMax(int num, ...)
{
va_list arg; //char*
va_start(arg, num); //指向可变参数部分
int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
for (int i = 1; i < num; i++)
{
int cur = va_arg(arg, int);//获取并比较其他的
if (max < cur)
{
max = cur;
}
}
va_end(arg);
return max;
}
int main()
{
int max = FindMax(5, 11, 22, 33, 44, 55);
printf("%d\n", max);
return 0;
}
通过反汇编我们可以看到形参从右往左依次压栈
如果你是这种直接往函数里面写实参的形式,这和我们传入变量的汇编稍有不同,但是这不重要,知道区别就可以
重要的是我们关注的栈顶寄存器esp,esp 记录的是栈顶的地址,在此处打断点我们可以通过内存中esp寄存器看到内存中压入栈中的形参,这里改为十六进制方便观察
va_list,va_start之类都是宏,我们先说两个最简单的
第一个va_list 就是定义一个char* 类型的指针arg,va_end就是把指针arg置空,这两个很好理解
va_start(arg,num)这个宏的作用呢就是让arg这个char*指针指向可变参数部分
va_arg(arg,int)就是通过传入的类型来提取出每个形参,这里传入int,就以4字节提取,就把所有可变参数列表里面的整形都提取出来了
demo 3: 如果将参数改成char类型,求char类型变量中的最大值,代码会有问题吗?
结果并未受影响,可是,我们解析的时候,是按照va_arg(arg, int)来解析的,这是为什么?
movsx : 相当于进行整形提升,看起来你传入的是char一个字节,但实际上在可变参数这里它已经隐式的将char提升成了整形
也就是说 movsx eax,btye ptr [e] 就是把char 类型e变量整形提升放到eax中
所以你按照int来解析没问题
注意事项
可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你
想一开始就访问参数列表中间的参数,那是不行的。
参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。
如果一个命名参数都没有直接三个点,编译器都直接报错。
这些宏是无法直接判断实际存在参数的数量。
这些宏无法判断每个参数的是类型。
如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。
原理
那上面除了va_list ,va_end我能理解,中间那两个宏我理解不了,所以要理解就必须在进一步
必须要看看宏是如何定义的
va_list 就是一个char类型
va_start(arg, num)的宏定义呢是下面这一坨,翻译一下就是 arg = (char) &num + 4 最终就让arg指向可变参数部分了,INTSIZEOF(n) 的意思就是4字节对齐,num本身就是4字节的int类型,所以这里就是4
上面呢那个内存图是以4字节显示的,也能看,但是其实按1字节更好说
arg = (char*) &num + 4
va_list把arg定义成了char类型,让&num也就是第一个参数5的地址强转成char类型 ,char类型+1就加1,i(nt指针+1就+4)。
va_list是char类型,方便后续按照字节进行指针移动
就是下面这个图和上面那个内存图一个意思,我不是画了红色的轨迹嘛
最终就让arg指向可变参数部分了
va_arg具体干了什么?
显然他要能做到依次提取11,21,31,41,51这五个形参,当然都是4字节整形嘛,因为你就是按va_arg(arg,int) int来提取的。
这个宏也比较狠,它做了两件事
先看ap += _INTSIZEOF( t ) ,_INTSIZEOF( t ) 说人话就是 _INTSIZEOF( int ),int 按4字节对齐还是4
也就是说先让ap啊,也就是arg指针,它在va_start之后指向可变参数部分也就是图上的11,ap += _INTSIZEOF( t ) -> ap += 4就让arg指向了21,至此arg已然是往后移动到下一个可变参数了!
这还没完,+= 以后也有返回值就是+=以后的地址又减去了4,也就是说又减回到了11这个地址然后通过你传入的类型t进行强转解引用,
也就是上面宏中 * (t * )解析出了其中一个可变参数!
你传入的不是int嘛,那就按照Int类型强转再解引用
当然了,如果你提取时va_arg(arg,char),传入的是char类型肯定就不对了,因为人家默认给你整形提升了,你还按照char提取
总结一下就是:
1.把”当前元素“提取出来
2. arg指向下一个待访问元素
那他怎么知道要提取多少次呢?你不是传入了num吗,这样我们利用循环就可以依次解析出所有可变参数了
看着下面的图,记住循环五次,按照上面的逻辑脑中实验一下就会发现最后arg会跑到00EFF8AC提出最后一个参数51,至此完成!
我们可以想象一下printf中设置的%d , %c ,%f … 之类不就是在确定类型 和个数吗,当然printf的实现还是蛮复杂的!
至此只有一个问题就是INTSIZEOF(n)这个宏它到底做了什么?你说是4字节对齐(向上取整)那怎么办到的?
宏是与类型无关的,进入里面都是sizeof(n),sizeof只看类型,上面不就是传入了int num变量 和 一个 int类型吗,但这不重点要谈的,但你得知道
你往里面传入char or short这种<4字节的类型,出来的就是4,你传入5,6,出来的就是8就是这个意思
这个宏说人话就是 INTSIZEOF(n) 计算的结果一定是能够整除4的最小整数,而且能够向上4字节取整
为什么要有这个4字节对齐?
因为入栈时如果是短整型本身就是按4字节对齐方式开辟的(整形提升),人家按4字节写的,你现在提取时只能按照人家的规则提取数据
第一步理解:求4的倍数m
既然是4的最小整数倍取整,那么本质是: x = 4*m , m是倍数,对7来讲,m就是2,对齐结果就是8
而m具体是多少,取决于n是多少
如果n能整除4,那么m就是n/4
如果n不能整除4,那么m就是n/4+1
比如 m = 3/4+1 = 1 m = 6/4 + 1 = 2
上面是两种情况,如何合并成为一种写法呢?
常见做法是 ( 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,1<=r<4 那么m就是 (n+4-1)/4->(能整除4部分+r+3)/4,其中
4<=r+3<7 -> 能整除4部分/4 + (r+3)/4 -> n/4+1
为什么 r 的范围是1 <= r < 4 呢?为什么不能是5?
我画了个图尝试理解一下,把你的n按照4一块一块分,分不够4的只有1,2,3也就是 1<= r < 4
第二步理解:求最小4字节对齐数
已经求出满足条件最小是4的几倍问题,现在只需要再乘以4就是能够整除4的最小整数
也就是
我们可以看到除4再乘4,你以为可以消掉是把?
不行,你还非得按照括号优先级,先计算最小几倍,再乘以4
不信的话你带入一个2,按照消掉的逻辑算算结果对吗?
2+3=5 不对,必须按照优先级先算倍数(5/4)*4=4
这样就能求出来4字节对齐的数据了,其实上面的写法,在功能上,已经和源代码中的宏等价了
第三步理解:理解源代码中的宏
拿出简洁写法:((n+4-1)/4)* 4,设w=n+4-1, 那么表达式可以变化成为 (w/4) * 4,而4就是2^2,w/4,不就相当于右移两位吗?,再次 * 4不就相当左移两位吗?先右移两位,在左移两位,最终结果就是,最后2个比特位被清空为0!
需要这么费劲吗?
w & ~3 不香吗?
所以,简洁版:(n+4-1) & ~(4-1)
原码版:( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ),无需先 / , 在 *
关于这个w / 2 相当于右移一个比特位,你想想一个2,比特位不就是10,2除以2=1 ,比特位不就是01吗,就相当于右移1位位
乘以2就是反过来!