序言
我们自己编写的函数通常参数是固定的,这样使得某些功能不能得到我们想要的结果,比如我们想求出2个数的最大值的代码不能用于求处3个数的最大值,因此,C语言定义了可变参数列表来编写参数个数不确定的函数。具有可变参数列表的函数就是:函数定义时,参数列表中的的参数不完全定义;调用该函数时,可以根据实际情况传入多个参数,且可以成功完成其函数功能。而该函数的参数列表就是可变参数列表
可变参数列表的使用
// demo 1:求两个数据中的最大值 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <windows.h> int FindMax(int x, int y) { if (x > y){ return x; } return y; } int main() { int x = 0; int y = 0; printf("Please Eneter Two Data# "); scanf("%d %d", &x, &y); int max = FindMax(x, y); printf("max = %d\n", max); system("pause"); return 0; }
//demo 2:求任意多个数据中的最大值(至少一个),要求不能使用数组 //因为目前参数个数不确定,那么函数编写的时候,参数个数也无法确定,换句话说,函数也就没法编写 //不过,C提供了满足该场景的解决方案:可变参数列表 #include <stdio.h> #include <windows.h> //num:表示传入参数的个数 int FindMax(int num, ...) { va_list arg; //定义可以访问可变参数部分的变量,其实是一个char*类型 va_start(arg, num); //使arg指向可变参数部分 int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据 for (int i = 0; i < num - 1; i++){//获取并比较其他的 int curr = va_arg(arg, int); if (max < curr){ max = curr; } } va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL return max; } int main() { int max = FindMax(5,0x11,0x22,0x33,0x44,0x55); printf("max = %d\n", max); system("pause"); return 0; }
demo1很简单,demo2是什么鬼?什么是va_list,什么是va_start,什么是va_arg,什么是va_end。接下来,我将带领你们一步一步理解这段代码。
我先告诉你们这段代码的作用是什么而将这段代码的底层实现的放在后面讲解,先简单的有一个直观的感受,就比如我们想要吃饭就只需要会用筷子即可,不需要知道筷子的构成。当然,讲清楚作用后会来讲述它们的底层原理,现在让我们从最简单如何使用可变参数列表开始讲解~
在正式讲解之前,我们需要回顾下相关函数栈帧里面的知识点,不清楚的可以看这篇文章
😼syseptember的个人博客:图解函数栈帧😼http://t.csdn.cn/8zpGH
🚩知识点1:函数的调用需要为函数形参开辟空间,并且为形参开辟空间是在调用函数前进行的。
🚩知识点2:形参开辟空间的顺序是从右向左的,即将最右边的形参最先压栈。
🚩知识点3:为形参所开辟的空间是连续的。
🫵现在我们正式开始学习可变参数列表~🫵
-------------------------------------------------------------------------------------------------------------------------
当函数定义中参数列表中至少出现了一个具体的形参,未出现的形参用...代替,那么这个函数的参数列表就是可变参数列表。
结合栈帧的相关知识,我们来理解一下demo2的代码。demo2中FindMax的定义中出现了一个具体的形参num和可变参数列表
主函数调用FindMax函数时,传递的实参是5,0x11,0x22,0x33,0x44,0x55。函数的功能是找出这5个数的最大值,在调用该函数前,会为形参分配空间,并且形参从右往左依次分配
为形参分配好空间后,就需要为FindMax函数分配栈帧了
FindMax中定义了一个变量arg,该变量类型是va_list,本质上是char*类型,
在FindMax函数中掉用va_start(arg, num)使得arg指向位于低地址处的可变参数
ps: 我们现在只需要知道va_start可以完成这件事即可,后面会将va_start是如何工作来完成这件事的
然后将max的初始值通过max = va_arg(arg, int)赋值为0x11,此时max就是可变参列表的第一个参数,进入for循环,curr= va_arg(arg, int)可以理解为将下一个可变参数赋给curr,在进行常规的比较寻找max操作,这样就保证了所有的可变参数都和max进行了比较从而可以找出可变参列表中最大的参数。最后va_end(arg)的作用就是将arg置空防止出现野指针。
我们现在复习一下上述长相和函数一样的代码作用
🚩代码1:va_list arg:定义的了一个char* 名为arg的变量
🚩代码2:va_start(arg, num):另arg变量指向可变参数列表中的第一个参数
🚩代码3:curr = va_arg(arg, int):将当前arg指向int变量赋值给curr并且arg指向下一个int变量
🚩代码4:va_end(arg):将arg置为空指针,防止出现野指针
再次提醒:现在我们只需要知道它们可以完成这件事,现在你不需要思考如何完成的
提问:代码3中va_arg(arg,int)是取出arg指向的int变量,并且另arg指向下一个int变量,那如果可变参数全部是char类型,上面代码是否还可以找出最大值呢?
答案是可以的。原因在于当可变参数类型为短整型参数时,会隐式提升为int型,当可变参数类型为float时,会被自动提升为double类型
不信?我们再来看一下汇编指令
movsx可不是一般的mov指令哟,movsx会将数据先进行整型提升,在将数据赋给寄存器
这说明了将char类型作为可变参数部分,首先会将char类型参数进行整形提升再压栈,这样就和比较int类型数据大小的过程是一模一样的了。无需对FindMax函数进行更改,如果此时多此一举将curr = va_arg(arg, int)改成curr = va_arg(arg, char)会出现什么样的结果呢?原本是读取arg所指向的4个字节(int值)变成读取arg所指向的1个字节(char值),而又因为char类型的参数已经被提升为int类型,所以只会读取arg所指向的一个字节。因此不要想当然地认为可变参数是char类型所以函数体也要对char类型进行操作。注意:short类型也同理
float类型作为可变参数会被自动转换为double类型数据
总结:
通过查看汇编,我们看到,在可变参数场景下:1. 实际传入的可变参数如果是 char , short ,float,实际传递的非可变参数是char,short,编译器在编译的时候,会自动进行提升(通过查看汇编,我们都能看到)2. 函数内部使用的时候,根据类型提取数据,更多的是通过 int 或者 double 来进行注意事项
- ❗可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。
- ❗参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。
- ❗这些宏是无法直接判断实际存在参数的数量。
- ❗这些宏无法判断每个参数的是类型。
- ❗如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。
可变参数的原理
在可变参数的使用部分,我们知道了通过va_list,va_start,va_arg,va_end可以访问所有的可变参数,但是我们不知道这些宏具体是怎么实现的,现在我将逐个介绍每一个宏的实现和原理
-------------------------------------------------------------------------------------------------------------------------
🐯va_list,va_end
va_list:char*的一个别名
va_end(arg):是一个宏,将arg置空
重点是va_arg(arg,int)和va_start(arg,num)
🦁va_arg
va_arg(arg, int):取出arg指向的int数值,并将arg指向下一个int数据
先记住_INTSIZEOF(n) 的意思:计算一个最小数字 x ,满足 x>=sizeof(n) && x%4==0 ,其实就是求出一种 4 字节对齐的方式是什么:比如sizeof(n)为1,2,4时,_INTSIZEOF(n)就是4,sizeof(n)为5,6,8时,_INTSIZEOF(n)就是8.思考:为什么要有4字节对齐? 因为可变参为短整型时会提升为整形,所以ap的偏移量应该是4字节的整数倍。再来看va_arg(arg,int)的含义:(*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))等价于(*(int*)(arg+=4)-4),arg首先指向下一个int数据,其次通过减去arg的偏移量得到该数据的起始地址,再强制转换为int类型的地址并且解引用取出该int数据。注意:当我们实参传短整型时,如果va_arg(arg,char)那么会解引用只能得到一个字节的数据,但是arg仍然会指向后4个字节的数据,所以在函数体内t通常是int或者double。va_arg的功能:1. 把当前元素提取出来。2. 使arg指向下一个待访问元素.🐯va_start
va_start(arg,num):另arg指向可变参数列表中的第一个参数
va_start(arg,num)相当于arg = (char*)&num+_INTSIZOF(num),前面说过,当sizeof(num)的值为1,2,4时,_INTSIZEOF(num)为4;当sizeof(num)的值为5,6,7,8时,_INTSIZEOF(num)的值为8.所以va_start(arg, num)相当于让arg指向后4个或8个字节的数据,这个设计是非常巧妙的,因为当非可变参数num为短整型时,会先提升为int在压栈,即使传递的第一个实参num是char类型,arg也会指向可变参数列表的第一个参数,因为在传递参数时num作为形参会提升为4字节,并且va_start(arg,num)使arg=(char*)&arg+4。
🦝_INTSIZEOF(n)
现在我们明白了va_start可以让arg指向可变参数列表的第一个参数。我们现在剩下最后一个问题,_INTSIZEOF(n)为什么可以得到最小x,并且满足x>=sizeof(n)&&x%4==0?
为了方便理解,我们将sizeof(n)直接记作n(char-1 short-2 int-4)
第一步理解:4的倍数
既然是4的最小整数倍,那么本质是:x=4*m,m是具体几倍。对于sizeof(n)等于7来讲,m就是2,x就是8,所以m是多少取决于sizeof(n)是多少
- 如果n能整除4,则m就是sizeof(n)/4-->n/4
- 如果n不能整除4,则m就是sizeof(n)/4+1-->n/4+1
这两种写法合并成一种写法就是sizeof(n)+sizeof(int)-1/sizeof(int)--->n+3/4
- 如果n%4==0&&n/4==k那么n+3%4==3&&n+3/4==k
- 如果n%4==k(1<=k<=3)&&n/4==m那么4<=n+3<=4&&n+3/4==m+1
第二步理解:最小4字节对齐数
搞清楚了满足条件是最小几倍的问题,那么计算一个x使得x>=n&&x%4==0就变成了
((n+sizeof(n)-1)/sizeof(n))[ 最小倍数 ]*sizeof(int)[ 单位大小 ]--->(n+3)/4*4
第三步理解:理解源代码中的宏
上面代码在功能上已经和源代码等价了,现在将上述代码简化记w=n+3
w/4*4相当于先讲w右移2位再将x左移两位,最后的结果相当于讲w最右边2个比特位清零!
所以不需要先/再*,直接另w & ~3也可以将最右边2个bit位清零,所以就有源代码((sizeof(n)+sizeof(int)-1)/sizeof(int)) & ~(sizeof(int) - 1))