"多少人都,生来纯洁完美,心底从不染漆黑。"
我们想要实现一个函数,这个函数的功能是返回一个整形的最大值。
emm,似乎有那点味道。但这应用场景似乎很受限制,因为这个函数比较的有效区间,只能装下两个数…… 也许你会说,那我就增添几个参数不就得了?
似乎问题,没有得到本质的解决。因为当你参数变多时,函数内的代码逻辑也得跟着更改。况且,如果遇到一个,你不知道会有多少个参数传入的场景,你又该如何选择参数个数的多少呢?
也许你会说可以将数据存储在一个数组中,将数组作为参数进行传递。但,你同样不知道应该创建多大的数组来适应这种情况。
难道我们对此就束手无策了吗?当然不是,C语言中提供了对这种不确定参数传入的解决方法
“可变参数列表”。
——前言
一、 可变参数列表
(1) 什么是可变参数
在计算机程序设计,一个可变参数函数是指一个函数 "拥有不定引数" ,即是它接受一个可变数目的参数。简单来说,就是函数的参数个数可变,参数类型不定的函数。
不同的编程语言对可变参数函数的支持有很大差异。
取自这里
而可变参数的应用场景,前言的例子,也已经很恰当的演示了。
(2) 可变参数的使用
参数列表宏定义" 四板斧"
va_list arg: 其实就是定义一个char*类型,方便后续按照字节进行指针移动
va_start(arg,n(第一个参数)): 让指针指向(...)里的参数部分
va_arg(arg,类型): 从(...)取数据,并根据类型 朝后指向下一个参数
va_end(arg): arg使用完毕,收尾工作。本质就是讲arg指向NULL
注意事项:
① 参数列表中至少有一个命名参数。
② 可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。③ 参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。
④ 这些宏是无法直接判断实际存在参数的数量。
⑤ 如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。
介绍了基本的可变参数的使用,我们来试试编写看。
这样,我们就能够很轻松地使用这个可变参数列表了,完成了我们对“不定参数”下,求最大值的需求。
仅仅三言两语讲解它的使用就截止了嘛?当然不是,因为从使用上来说,它确实很简单。可是要问你它底层是怎么实现的,指针是怎么移动的,如果传入的类型不是int呢?是字符型呢?是短整型呢?可变参数内部又是作何处理的?
二、可变参数原理
(1) 基本原理
要弄清楚可变参数如何传参,如何让指针指向不同的值,来完成对参数的读取,我们一定得看看这 宏定义 " 四板斧"的底层是什么。
① va_list:
va_list没有什么可以值得说的,因为它底层就是一个类型重定义的 char*指针。至于为什么是char*,因为在之后它好按照每字节进行偏移。
② va_start:
③ va_arg:
④ va_end:
这个同va_list 一样没什么讲头,就是让创建的arg指针,指向空,防止野指针。
(2) 图示四板斧
我知道,恐怕我就算将这四个宏定义函数讲得怎样绘声绘色,你也难以理解,更何况我讲得很烂。因此,以下根据图示看看这四个宏定义做了什么工作。
可变参数是函数调用,而这里和函数栈帧的开辟以及参数的创建强相关,为此,在进行图示之前,你可以先看看下面的几个问题,看你是否真的掌握了函数栈帧的细节。
根据上图,我们可以得出三个结论:
① 函数参数在函数调用前,就会在被压栈。
② 函数参数压栈的顺序是从右到左。
③ 每个参数的位置都是相对的,只要找到了第一个参数的位置,根据偏移量,就可以通过地址偏移的方式,获得其他参数值。
调用va_list 与 va_start;
迭代va_arg:
置空va_end:
(3) 整形提升
我们上面的实验是针对传入的是整型而言,现在我们来看看如果将传入的参数是" char"类型时,该函数还能否实现我们预期的功能?
很显然,这些字符里最大的就是 'e',所对应的ASCII码为101,完成了我们预期的功能。可是,我们明明传入的是char类型诶,为什么按照 "int"类型的大小偏移指针,没有出错呢?
唯一能给我们解释的,就是mov 与 movsx 。
汇编语言数据传送指令MOV的变体。带符号扩展,并传送。简单来说,使用movsx汇编命令,让原本是char类型的变量,整型提升成了int!不管是char还是短整型都会被整型提升!
因此,如果我们看到以下代码,其实是不正确的。
__crt_va_arg(arg, char);
__crt_va_arg(arg, short);
因此,为什么要按照4字节向上取整对齐? 本质上是为了 便于指针+偏移量的方式,按照字节读取每个地址处的参数值。
(4) 如何理解4字节对齐?
这里的前提是,32位平台下,sizeof(int)大小是4,其他情况我们不考虑。
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
单独这么看,似乎很难理解,为什么这个宏函数的功能能够实现4字节向上取整的功能。
第一步:
第二步:
第三步:
这就是大佬们的智慧,真的遥望而不可及。
总结:
当然,对于可变参数列表的使用肯定是比原理更加得现实的,比较对于最后讲的4字节向上对齐怎么来的,是需要一定的数学基础和推理的,难度在这里摆着。
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~