深入理解可变参数列表
创始人
2024-05-13 05:36:49
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

相关内容

热门资讯

《黑执事》经典台词 《黑执事》经典台词  1、一旦拒绝了信仰,就不能再踏入神的大门。  2、那是必须的,,不管何时,王只...
风雨哈佛路经典台词 风雨哈佛路经典台词  引导语:《风雨哈佛路》这部影片相信很多人都看过,亦是一部非常好看的影片,那么有...
文艺节目主持词 文艺节目主持词四篇  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观众。在一步步向前发展的...
幼儿园六一儿童节主持词 幼儿园六一儿童节主持词尊敬的各位来宾、各位朋友大家下午好!沐浴着和风丽日,我们即将迎来花团锦簇、芳香...
教师节表彰大会校长的致辞 教师节表彰大会校长的致辞范文(精选6篇)  在平平淡淡的日常中,要用到致辞的地方还是很多的,致辞讲求...
婚礼开场白主持词 婚礼开场白主持词  利用在中国拥有几千年文化的诗词能够有效提高主持词的感染力。随着社会一步步向前发展...
会主持人开场白台词 会主持人开场白台词2013年会主持人开场白台词    甲:新年的钟声即将敲响,时光的车轮又留下了一道...
领导主持词 领导主持词三篇  主持词已成为各种演出活动和集会中不可或缺的一部分。在现今人们越来越重视活动氛围的社...
升学宴致辞 升学宴致辞(精选15篇)  在现实生活或工作学习中,大家一定都接触过致辞吧,致辞具有“礼仪性”或“仪...
农村白事的主持词开场白 农村白事的主持词开场白(精选10篇)  在发展不断提速的社会中,越来越多的人会用到开场白,好的开场白...
生日主持词的开场白   生日主持词开场白(一)  各位同事和寿星们,各人晚顶好!在这天高气爽、丹桂飘喷鼻的夸姣季候,咱们...
旅游文化节主持词 旅游文化节主持词  主持词的写作需要将主题贯穿于所有节目之中。现今社会在不断向前发展,主持人参与的事...
主持人串词 主持人串词  一、串词的语言特征  (串词的语言,可以说是用尽了所有的修辞手法,我们不可能去全讲,因...
浪漫婚礼司仪主持词 浪漫婚礼司仪主持词  主持词是主持人在台上表演的灵魂之所在。在现在的社会生活中,很多场合都需要主持人...
公司迎春晚会的主持词 公司迎春晚会的主持词  主持词的写作需要将主题贯穿于所有节目之中。在当今不断发展的世界,主持人在活动...
少儿节目主持词 精选少儿节目主持词4篇  主持词已成为各种演出活动和集会中不可或缺的一部分。随着社会一步步向前发展,...
王家卫电影经典台词 王家卫电影经典台词(精选50句)  我们爱看王家卫的电影,不止爱他所创造的那个光影世界,更爱他电影中...
演唱会主持台词 演唱会主持台词  (甲)尊敬的各位领导,  (乙)各位来宾,  (甲)敬爱的老师,  (乙)亲爱的同...
《你的名字》经典台词 《你的名字》经典台词  你的名字,是谁的心事,还记得你的名字里面的经典台词吗?以下是小编为你精心整理...
教研活动主持词 教研活动主持词  主持人在台上表演的灵魂就表现在主持词中。在当下的中国社会,主持成为很多活动不可或缺...