深入理解可变参数列表
创始人
2024-05-13 08:47:30
0

目录

1.前言

2.基本使用方法

        1.引入

        2.相关宏介绍

3.原理剖析

         1.传参

         2.va_list

         3.va_start()

         4.va_arg()

         5.va_end() 

 4.注意事项

 5.总结


1.前言

       在C语言中,对于一般的函数而言,参数列表都是固定的,而且各个参数之间用逗号进行分开。而除了这些函数外,还有些函数的参数列表是不固定的,例如我们常用的printf()函数,会根据我们传入的参数个数来调整最终打印的个数。本期我们会从宏观到微观,从如何使用可变参数列表到可变参数列表实现原理来理解可变参数列表。

2.基本使用方法

        1.引入

        假设,我们需要实现一个函数FindMax()来比较num个数的最大值,但我们不清楚num的具体值。此时我们就可以通过可变参数列表的形式设计这个函数,如下:

#include
#includeint FindMax(int num, ...)
{va_list arg;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, 2, 4, 7, 4, 3);printf("%d", max);return 0;
}

 首先,我们需要包含stdarg.h头文件,然后我们通过va_list,va_start(),va_arg(),va_end()

这四个宏来实现对可变参数部分的访问,进而实现求出最大值的功能。可变参数部分用...来表示。

需要注意的是,函数中必须要有一个参数,编译器需要通过这个参数的地址来确定可变参数部分的地址(后面讲解)。在本题中,第一个参数用于传入可变参数部分的数量,因为函数内部是无法确定有多少个可变参数需要被访问。

        2.相关宏介绍

        四个宏的功能如下表所示:

符号及使用说明
va_list arg

定义可以访问可变参数部分的变量,实际上是个

char*类型

va_start(arg,num)使arg指向可变参数部分(通过压栈的特点)
va_arg(arg,int)

通过指针的方式得到参数,int表示每次arg向后读取

4个字节

va_end()arg使用完毕,将arg置为0

 所以,我们使用可变参数列表的基本步骤分为以下几步:

1.定义一个va_list类型变量

2.使用va_start()使变量指向可变参数部分

3.通过va_arg()访问可变参数

4.访问完毕后使用va_end使va_list变量置0


注意,使用va_arg()访问可变参数时只能按照顺序向后访问,可以中途停止,但是不能返回或者跳跃访问(后面分析)

  

3.原理剖析

        上面我们了解了可变参数列表的基本使用方法,但是在这之中还存在着一些注意事项,下面我们将通过栈帧和底层代码的实现来分析可变参数列表的实现原理(使用上面求最大值的例子)。提示:下面会使用到往期函数栈帧的内容,传送门:C语言之函数栈帧(动图详解)。

        1.传参

        我们知道,函数调用前会将参数按照从右往左的顺序进行压栈,形成临时变量,可变参数列表也不例外,会将我们传入的参数压入栈中:

        对应栈空间如下: 

 


          2.va_list

        我们查看va_list宏的定义如下:

 va_list实际上是一个char*类型的指针,其指向大小为一个字节的空间。因此上面的

va_list arg实际上是char* arg。


         3.va_start()

        查看va_start()宏的定义如下(底层定义时通过多个宏一起实现,便于封装):

//最终展开相当于#define va_start(ap,v) ((void)(ap = (char*)(&(v)) + _INTSIZEOF(v)))

 对于_INTSIZEOF(n)宏,作用是将n所占字节大小按照4字节进行对齐,即向上取整。如n占2个字节,这个宏的值就为4,n占6个字节,这个宏的值就为8(原因见下面注意事项)。

 所以,va_start(arg,num)的作用就是取出num的地址并强转为char*,然后向下偏移4个字节并赋给arg,通过栈空间我们可以发现通过以上操作arg就指向了可变参数部分。 

这便说明了为什么可变参数列表为什么至少需要一个参数是已知的,因为需要确定可变参数部分的地址。


         4.va_arg()

        查看va_arg()宏的定义如下:

//最终展开相当于#define va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

 由于压栈形成的临时变量的地址空间是连续的,所以va_arg(arg,int)的作用就是将ap向下偏移4个字节并改变arg的值,然后再回到起初的位置 (arg没有改变)向下读取4个字节的值作为va_arg(arg,int)宏的值。简单来说,va_arg(arg,int)起到了两个作用:1.将arg指向下一个参数  2.取出原位置的参数

        动图如下:

 通过以上原理,我们就可以发现va_arg()宏的访问顺序是顺序且单向的,无法进行返回或跳跃访问。


         5.va_end() 

        查看va_end()宏的定义如下:

//最终展开相当于#define va_end(ap) ((void)(ap = (char*)0))

因此,va_end(arg)实际上就是将arg的值置为0,结束可变参数列表的访问


4.注意事项

        目前,我们还剩最后一个问题,为什么va_start和va_arg宏在定义时偏移的字节都需向上取整,而不是直接偏移sizeof(n)个字节?我们通过以下例子来说明:

#include
#includeint FindMax(int num, ...)
{va_list arg;va_start(arg, num);int max = va_arg(arg, char);for (int i = 1; i < num; i++){int cur = va_arg(arg, char);if (max < cur){max = cur;}}va_end(arg);return max;
}
int main()
{char a = 'a';char b = 'b';char c = 'c';char d = 'd';char e = 'e';int max = FindMax(5, a, b, c, d,e);printf("%c", max);return 0;
}

        我们将传入的参数改为字符型变量,其余地方不变,va_arg的参数依旧是int,我们发现最终运行的结果依旧是我们想要看到的结果:

         到这里我们可能就疑惑了,char类型的变量占一个字节,而我们va_start(arg,num)或va_arg(arg,int)进行偏移的时候一次是偏移四个字节,而参数间地址差为一个字节,显然不能正确依次访问我们传入的5个可变参数部分。我们查看汇编代码如下:

        通过以上汇编,我们发现字符型实参在形成临时变量时使用的是movsx+push命令,不同于整形变量的mov+push命令。

movsx的作用为:

将数据进行符号扩展,再进行传送,即将char类型数据扩展为int类型数据后再进行传送。

类似的还有movzx,其作用为:
将数据进行零扩展,再进行传送,二者的区别就在于扩展时是补符号位还是零。


一般来说,如果传入的参数是短整形,一般要进行int类型提升,汇编指令为movsx(movzx),

如果传入的是float,则会提升为double类型

        由此,我们知道了当函数传参形成临时变量时,会先进行扩展提升放入寄存器中,然后再将扩展后的数据压入栈中。即我们最终形成的临时变量实际上不是占一个字节,而是四个字节。因此,实际上va_arg()的参数为int才是正确的,如果为char则最后解引用结果就是向后访问一个字节。(不过由于我们的机器采用小端存储,数据低位放在低地址处,因此两种写法结果一样)

//对于上面的例子
va_arg(arg,int);   //正确的
va_arg(arg,char);  //严格来说错误

        回到之前的问题,为什么偏移的字节需要向上取整?我们可以假设我们传入的类型是char类型,由于传参时扩展提升的存在,可变参数部分之间地址的差距实际上是扩展提升后的字节,即4个字节,而此时如果我们直接使用sizeof,求得的结果为1个字节,最后arg指针只会向下偏移1个字节,以致无法正确指向下一个参数。因此利用_INTSIZEOF(n)宏使偏移量进行向上4字节取整。

 5.总结

1.我们可以通过va_list,va_start(),va_arg(),va_end()四个宏来实现可变参数列表

2.使用可变参数列表时定义函数至少要有一个已知变量

3.可变参数列表的访问是顺序且单向的

4.对于短整形和float类型,传参形成临时变量时数据会发生扩展提升


 以上,就是本期的全部内容。

制作不易,能否点个赞再走呢qwq

相关内容

热门资讯

安卓系统的手机优缺点,全面解析... 你有没有发现,现在市面上手机种类繁多,让人挑花了眼?其中,安卓系统的手机可是占据了半壁江山呢!今天,...
平板有没有安卓系统,安卓系统引... 你有没有想过,平板电脑到底有没有安卓系统呢?这个问题听起来可能有点奇怪,但确实很多人在选购平板时都会...
安卓手机双系统好用不,安卓手机... 你有没有想过,你的安卓手机是不是也能像多面手一样,既能驾驭工作,又能享受娱乐呢?没错,说的就是那个神...
安卓系统怎么登录国际服,一键操... 你有没有想过,为什么有时候你的安卓手机上会出现那些国际服的游戏呢?是不是好奇怎么登录这些神秘的国外服...
安卓系统的时间天气没了,天气功... 最近你的安卓手机是不是也遇到了一个让人头疼的小问题?那就是——时间天气不见了!没错,就是那个曾经陪伴...
安卓好用的拍照系统,捕捉美好瞬... 你有没有发现,现在手机拍照功能越来越强大了?尤其是安卓手机,拍照系统简直让人爱不释手!今天,就让我带...
软件如何兼容安卓8系统,助您软... 你有没有发现,随着科技的飞速发展,手机软件更新换代的速度也是越来越快呢!这不,安卓8系统已经悄然来临...
安卓通用版系统下载,畅享智能生... 你有没有发现,最近手机界又掀起了一股热潮?没错,就是安卓通用版系统下载!这可是个让无数安卓用户兴奋不...
安卓无线点餐系统ph,PH技术... 你有没有想过,点餐也能变得如此轻松愉快?没错,就是那个我们每天都要面对的吃饭问题,现在有了安卓无线点...
安卓门禁系统怎么样,便捷通行新... 你有没有想过,每天回家时,只需轻轻一刷,门就自动打开了?这就是安卓门禁系统的魅力所在!今天,就让我带...
在电脑上模拟安卓系统,探索虚拟... 你有没有想过,在电脑上也能体验安卓系统的乐趣呢?没错,就是那种随时随地都能玩手机的感觉,现在也能在电...
飞机送餐安卓系统,空中美食新体... 你有没有想过,飞机上的美食是如何送到你手中的?是不是觉得这背后有着神秘的力量?其实,这一切都离不开高...
findx耍原生安卓系统,深度... 亲爱的读者们,你是否厌倦了那些花里胡哨的定制系统,渴望回到那个纯净的安卓世界?今天,我要带你一起探索...
一加系统属于安卓系统吗,引领智... 你有没有想过,手机里的那个神奇的“一加系统”到底是不是安卓系统的一员呢?这可是个让人好奇不已的问题哦...
小米2刷安卓系统吗,探索安卓系... 亲爱的读者,你是否曾经对小米2这款手机刷安卓系统的事情感到好奇呢?今天,就让我带你一探究竟,揭开小米...
安卓7.0系统线刷包,深度解析... 你有没有发现,你的安卓手机最近有点儿“蔫儿”了?别急,别急,今天就来给你揭秘如何让你的安卓手机重焕生...
白菜系统和安卓拍照,开启智能生... 你知道吗?最近我在用手机拍照的时候,发现了一个超级酷的功能,简直让我爱不释手!那就是——白菜系统和安...
安卓系统查杀病毒,全方位守护您... 手机里的安卓系统是不是有时候会突然弹出一个查杀病毒的提示?别慌,这可不是什么大问题,今天就来给你详细...
iso系统与安卓各系统哪个好,... 你有没有想过,手机操作系统就像是我们生活中的不同交通工具,各有各的特色和优势。今天,咱们就来聊聊这个...
中柏怎么换安卓系统,解锁更多可... 你有没有发现,中柏的安卓系统有时候用起来还挺不顺手的?别急,今天就来手把手教你如何给中柏手机升级安卓...