一、什么是可变参数函数
C语言允许定义参数数量可变的函数,这称为可变参数函数(variadic function)。这种函数需要固定数量的强制参数,后面是数量可变的可选参数。
其中,强制参数必须至少一个,可选参数数量可变,类型可变,可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定。
其实我们早就接触过可变参数函数了,C 语言中最常用的可变参数函数例子是 printf()和 scanf()。这两个函数都有一个强制参数,即格式化字符串。格式化字符串中的转换修饰符决定了可选参数的数量和类型。(是吧,printf中可以有自定义个%d,没毛病)
可变参数函数的参数列表的格式是,强制性参数在前,后面跟着一个逗号和省略号(…),这个省略号代表可选参数。比如:int fun(int, …) (我随便举的例子啊)
.
.
二、可变参数函数的实现
1、问题引入
我们先来思考这样一个问题,作为本节的引入:
如果我们要预先写一个可变参数的累加求和函数,其强制参数类型为int,用它来表示我们一共要传入多少个可变参数。于是我们大概可以有这么一个框架:
double getSum(int NumofPara, ...)
{
int i = 0;
double sum = 0.0;
for( i = 0; i < NumofPara; i++ )
{
sum += ??
}
}
发现什么问题没有?
由于已经知道要传入多少个可变参数,所以求和思路就是,for循环遍历NumofPara次,每次都把sum加上一个可变参数。
思路很清晰,没毛病。但是,问题来了。由于是可变参数,我们无法提前得知可变参量的名字,也就没法访问这些可变参数。(你参数列表里都是一串省略号了,你怎么可能提前知道变量名,所以自然而然也无法表示、无法访问这些变量了)
2、实现思路
为了解决上述问题,C语言规定:
当编写可变参数函数时,必须用 va_list 类型定义参数指针,以获取可选参数。可变参数函数要获取可选参数时,必须通过一个类型为 va_list 的对象来进行访问,它包含了参数信息。
这种类型的对象也称为参数指针(argument pointer),它包含了栈中至少一个参数的位置。可以使用这个参数指针从一个可选参数移动到下一个可选参数,由此,函数就可以获取所有的可选参数。va_list 类型被定义在头文件 stdarg.h 中。
这么说可能太过官方,太抽象了。我们来举个例子。
假设我们有一个可变参数函数getSum(int NumofPara, …),然后现在我们代入具体值,比如getSum(3, 7, 8, 9),通过上面的介绍我们知道,第一个3是强制参数,表明后面跟了3个可变参量,而后面的7、8、9则为具体的可变参量。
根据C语言的要求,我们需要在getSum(int NumofPara, …)函数中定义一个va_list类型的指针。
然后它是怎么实现“访问、获取可选参数”的呢?
C语言里是这样实现的:通过某种机制(等会儿会讲)让强制参数和可选参数在内存中以连续的方式存放(强制参数在前),同时让va_list指针指向最后一个强制参数,即第一个可选参数前的强制参数。然后,通过另一种机制,每次访问va_list所指的参量之后,指针自动向后移位。进而当下一次再访问va_list的时候,访问的就是下一位的值。这样就可以访问各个可变参数了。大概就是这样了,讲得太接地气了。
3、具体实现函数
那么,C语言又是怎么实现上面提到的这些机制的呢?
由此引入两个函数。
void va_start(va_list argptr, lastparam);
是va_list指针的初始化函数,用来初始化指针,也就是实现让其“先指向最后一个强制参数”的功能。
于是自然而然的,该函数的第一个参数是一个va_list 类型的指针,第二个参数是可变参数函数中最后一个强制参数,即第一个可选参数前的强制参数。
va_start函数中,va_list进行初始化,指针指向末尾的强制参数。va_start结束后,初始化完成,指针自动移位到下一个参数,即第一个可变参数。(虽然感觉有点奇怪)
那么怎么访问va_list指针当前指向的可变参数呢?引出第二个函数:
type va_arg(va_list argptr, type);
其第一个参数是已经初始化完成的va_list指针,第二个参数则为可变参数的类型,返回的参数就是当前va_list指针所指的可变参数,所以类型也跟传入的可变参数类型相同。
每一次通过va_arg函数访问完一次参数后,va_list指针会自动移位到下一位。
C语言还规定,当不再需要使用参数指针时,必须调用宏 va_end来终结该指针,其实说白了就是释放内存。(如果想使用宏 va_start 或者宏 va_copy 来重新初始化一个之前用过的参数指针,也必须先调用宏 va_end)
4、具体实现示例
好了,说了这么多,我们通过完善之前写了一半的getSum函数来具体了解一下,可能就会豁然开朗明明白白了:
double getSum(int NumofPara, ...)
{
int i = 0; //用于for循环
double sum = 0.0; //用于求和
va_list pointer; //新建一个va_list类型的指针
va_start(pointer, NumofPara); //初始化指针,指针指向确定
for( i = 0; i < NumofPara; i++ )
{
sum += va_arg(pointer, double); //通过va_arg函数来访问可变参数,返回类型为double
} //同时,每次va_arg函数结束后,va_list指针指向下一位
va_end(pointer); //终结指针,释放内存
return sum;
//P.S. 有个问题要注意一下,在main函数里调用的时候,应该写成getSum(3, 7.0, 8.0, 9.0),否则得到的可能是0。
}
接下来简单补充一下上面提到的所谓“机制”:
它的实现原理利用了内存的压栈技术,将参数压入(push)栈内,使用时,再逐个从栈里pop出来
需要注意的是,压栈的顺序是从最右边参数开始的,再向左逐个压入,根据栈的原理,在取参数时,就从第一个可变参数开始了。
在进程中,堆栈地址是从高到低分配的.当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈过程,堆栈地址不断递减。
(所以取的时候就是从低到高,也就是上面草图中从左到右从3到7到8到9)
三、不小心写多了一个标题
上面说得比较粗糙,可能有一些地方说错了。里面具体原理我也还没去深究。以后吧。
参考资料:
C语言可变参数函数
【干货】C语言可变参数
C语言可变参数列表知识总结
C语言中可变参数的使用方法
基于本文知识点,下一次再来追补一些关于vsprintf函数的知识点。