C++规定函数参数传递有三种:传参数的值(称值传递,简称传值),传参数的地址(称地址传递,简称传指针) ,传参数的引用(称引用传递,简称传引用)。
首先看值传递函数怎么实现的?为了更形象的描述这一过程。笔者以函数GetMemory为例子,详细描述函数值传递的调用过程。GetMemory的代码实现如下所示:
char * GetMemory(int nNumber)
{
char *pStr = new char[nNumber];
return pStr;
}
int main()
{
char *pHello = NULL;
pHello = GetMemory(100);
strcpy_s(pHello, "Hello,Meimei");
delete[] pHello;
pHello = NULL;
}
值传递函数调用过程可分为三步:(1) 在堆栈创建形参副本及局部变量;(2) 函数执行,(3)函数退出,释放副本和临时变量。值传递函数调用过程如图6-1所示。
小心地雷:
- 在副本生成时,如果数据类型为类类型,函数会调用类的构造函数进行类对象构造和数据拷贝。
- 在堆栈释放副本时,如果数据类型为类类型,函数会调用类的析构函数进行类对象数据的释放。
函数的值传递了解完毕后,接着我们再看一下函数的引用传递。笔者同样函数例子为引子,描述引用传值函数调用和执行过程。
void GetMemory(char* &pStr, int nNumber)
{
pStr = new char[nNumber];
}
int main()
{
char *pHello = NULL;
GetMemory(pHello, 100);
strcpy_s(pHello, "Hello,Meimei");
delete[] pHello;
pHello = NULL;
}
引用传值函数调用过程同样可分为三步:(1)在堆栈创建引用形参,普通形参副本及局部变量;(2)函数执行,(3)函数退出,释放(引用)副本和临时变量。引用传值函数调用如图6-2所示
小心地雷:引用传值过程中,即使引用形参为类类型,在副本创建和释放时也不会发生构造和析构函数调用。
讲述了传值调用和传引用调用后,最后我们简述最后一种函数调用方式传指针调用。
char* GetMemory(int *pnNumber)
{
char* pStr = new char[*pnNumber];
return pStr;
}
int main()
{
char *pHello = NULL;
int nNumber = 100;
pHello = GetMemory(&nNumber);
strcpy_s(pHello, "Hello,Meimei");
delete[] pHello;
pHello = NULL;
}
指针传值函数调用过程亦可分三步:(1)在堆栈创建指针形参,普通形参副本及局部变量;(2)函数执行,(3)函数退出,释放副本和临时变量。指针传值函数调用过程如图6-3所示。
小心地雷:
- 指针副本即使为类指针,也不会调用类的构造函数,因为创建的不是类对象,而是类指针。
- 指针的解引用就是通常所说的间接寻址。指针的解引用就是获取指针变量中内存地址处的数据。
1.差异分析
至此函数的三种调用模式我们都已经讲述完了。三者之间的差异我想你也已经知晓了,它们可以简要的总结成下面几点:
(1)值传递(pass-by-value)过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟内存空间以存放由主调函数传进来的实参值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。如果我们想通过传值方式实现两数据的交换。这种做法是不正确的。例如:
void swap(int a, int b); // 函数功能:实现两数据的交换功能
int main()
{
int a = 1;
int b= 2;
swap(a, b);
return 0;
}
(2)引用传递(pass-by-reference)过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的引入。被调函数对形参的任何操作都被处理成对实参引入的操作,正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
(3)指针传递(pass-by-pointer)是传值调用的特例,即传的值为主调函数变量的地址。被调函数的形式参数同样在堆栈中为局部变量开辟内存空间,被调函数对局部变量的任何操作都操作都会作用在主调函数变量的地址之上。因此被调函数通过局部形参所做的任何操作都会影响主调函数中的实参变量。
2.效率分析
分析了函数的三种调用差异,根据函数三种参数传递的差异,接着分析下函数三种参数传递的效率,
三种参数传递的效率可以总结为下面四点:
(1)从执行效率上讲(这里所说执行效率,是指在被调用的函数体内执行时的效率)。因为传值调用时,当值被传到函数体内,临时对象生成以后,所有的执行任务都是通过直接寻址的方式执行的,而指针和大多数情况下的引用则是以间接寻址的方式执行的,所以实际的执行效率会比传值调用要低。如果函数体内对参数传过来的变量进行操作比较频繁,执行总次数又多的情况下,传址调用和大多数情况下的引用参数传递会造成比较明显的执行效率损失。
(2)虽然第四点说,函数的传值调用会比函数传地址和传引用效率高。但从整个程序执行角度考虑,传值调用未必就是效率最高的解决方案。假设有这么一个函数,假设其实参为大小为10000的数组,而每个数组成员为一个类对象。函数在被调用过程中,会发生10000次的构造函数调用以生成局部变量副本。例如下面这段代码。
CComplex Sum(vector<CComplex> vecComplexNumber) // Vector向量求和函数
{
CComplex Sum(0, 0) ;
vector<int>::iterator iterNum = vecComplexNumber.begin();
for (; iterNum != vecComplexNumber.end(); iterNum++)
{
Sum+= (*iterNum);
}
return Sum;
}
说明:可以想象这种情况下此函数的执行效率是非常低的。但事实上这种“傻”事,我们是经常干的。而且有时我们还在自鸣得意。此种情况下建议你选择传址或传引入方式实现。
(3)关于函数的调用有个特殊的地方,就是多态情况,如果形参是父类,而实参是子类,在值传递的时候,临时对象构造时只会构造父类的部分,是一个纯粹的父类对象,而不会构造子类的任何特有的部分,因为只有虚的析构函数,而没有虚的构造函数,这点是要注意的。如果想在被调函数中通过调用虚函数获得一些子类特有的行为,这是不能实现的。
(4)关于函数的健壮性,值传递比使用指针传递要安全得多,因为你不可能传一个不存在的值给传值参数或引用参数,而使用指针就可以,很可能传来的是一个非法的地址(没有初始化,指向已经delete掉的对象的指针等)。所以使用值传递和引用传递会使你的代码更健壮,具体是使用引用还是使用传值,最简单的原则就是看传递的是不是内建的数据类型,对内建的数据类型优先使用值传递,而对于自定义的数据类型,特别是传递较大的对象,那么优先使用引用传递。
请谨记
- 三种函数调用方式,各有各的好处,一般引用和指针效率相仿。传值调用效率依使用环境而定。
- 对内建的数据类型优先使用值传递,而对于自定义的数据类型,特别是传递较大的对象,那么优先使用引用传递。