C语言可变参数函数_初探

一、什么是可变参数函数

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类型的指针。

然后它是怎么实现“访问、获取可选参数”的呢?
图片讲解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函数的知识点。

猜你喜欢

转载自blog.csdn.net/weixin_44692935/article/details/103001787