函数可变参数的用法及原理

声明和定义

C语言中,经常会遇到带有可变参数的函数。对于可变参数函数的声明,只需要用『…』表示不定参数部分即可,如下面的func1。从错误示例func2func3可以看出,带可变参数的函数必须至少提供一个固定参数。后面的可变参数函数的实现原理可以知道,这个参数将用来对后面的可变参数进行寻址,以便实现对所有可变参数的访问。

1
2
3
void func1(int a, ...); //correct
void func2(...); //wrong
void func3(..., int a); //wrong

实现原理

为了理解带可变参数函数的实现机制,我们需要回顾下函数调用的内存模型。
当调用一个函数时,将触发如下动作:

  1. 开辟该函数的栈空间。
  2. 将当前的运行状态压栈。
  3. 将函数返回地址压栈。
  4. 在栈内为参数分配空间。
  5. 在栈内为局部变量分配空间。
  6. 执行该函数。

以常见的printf函数为例printf("%d and %c",a, b)。这里a是int型,不是char型,加上第一个格式化参数,本次printf()调用传递了三个参数。考虑到需要实现可变参数的机制,C语言进行参数压栈的时候,压栈的顺序是从右向左,即先将b入栈,再将a入栈,最后是format入栈。由于栈是向下(低地址)生长的,所以在知道了第一个参数format的地址之后,所有的参数地址都可以计算出来。函数调用内存模型如下图:
函数调用内存模型

可变参数的标准宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//可变参数标准宏头文件
#include "stdarg.h"
//申明va_list数据类型变量pvar,该变量访问变长参数列表中的参数。
va_list pvar;
//宏va_start初始化变长参数列表。pvar是va_list型变量,记载列表中的参数信息。
//parmN是省略号"..."前的一个参数名,va_start根据此参数,判断参数列表的起始位置。
va_start(pvar, parmN);
//获取变长参数列表中参数的值。pvar是va_list型变量,type为参数值的类型,也是宏va_arg返回数值的类型。
//宏va_arg执行完毕后自动更改对象pvar,将其指向下一个参数。
va_arg(pvar, type);
//关闭本次对变长参数列表的访问。
va_end(pvar);

用法示例,第一个参数指定了可变参数的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "stdarg.h"
using namespace std;
int sum(int count, ...)
{
int sum_value=0;
va_list args;
va_start(args,count);
while(count--)
{
sum_value+=va_arg(args,int);
}
va_end(args);
return sum_value;
}
int _tmain(int argc, _TCHAR* argv[])
{
cout<<sum(5,1,2,3,4,5);//输出15
}

解释型语言中的可变参数函数

以Python为例,如在函数中增加可变参数,只需要在函数定义中加入一个带*的形参,如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
#correct
def func1(a, *args):
pass
#correct
def func2(*args):
pass
#wrong 扩展位置参数要在位置参数后面
def func3(*args, a):
pass

Python虚拟机在编译函数时,发现参数中有像*args这种带星号的参数(在Python中我们将其称为扩展位置参数),会在函数编译后的代码中加入CO_VARARGS旗标,表示这个函数是含有扩展位置参数的函数,并在代码的局部变量中加入一个不可变的tuple(元组)型变量。当Python虚拟机执行在call_fuction中调用这个函数的时候,会将压栈的扩展位置参数依次弹栈,并将它们存储到编译阶段生成的那个tuple型变量中。因此,我们在Python的函数中访问扩展位置参数,就可以像访问列表一样用列表索引的方式进行访问了。

1
2
3
4
5
def func(*args):
for i in args:
print i
func(1,2,3,4,5,6)