首先,函数形参和返回值如果在不使用引用的情况下是按照所谓传值(by-value)的方式来进行传递的。其本质是由编译器产生临时对象,将实参或者返回值拷贝给临时对象,之后的操作都是在临时对象上进行,当函数结束的时候再释放掉临时对象。
那么,当我们面对类类型对象作为形参和返回值时,其构造函数及析构函数是如何运作的呢?下面通过一系列代码来进行追踪:
通过这个程序可以看到,我在主函数中定义了一个对象,在foo函数中也定义了一个局部对象,那么总共会发生几次构造几次析构呢?答案在这里:
可以看到,在程序中总共存在着两次构造函数和两次拷贝构造函数,但是只有三次析构函数。也就是说,存在一个对象没有被析构,这是为什么呢?下面通过对foo函数的伪代码展开来进行分析。
Foo()的C++伪代码:
//第一次构造
xx.XX::XX();
XX _temp0;
//第一次拷贝
_temp0.XX::XX(xx);
void foo(XX&_result, _temp0)
{
//第二次构造yy.XX::XX();
//赋值yy.operator=(_temp0);
//第二次拷贝_result.XX::XX(yy);
return;
}
//第一次析构yy.XX::~XX();
//第二次析构_result.XX::~XX();
//第三次析构_temp0.XX::~XX();
可以看出,main()函数中的对象xx并不参与析构,可以理解为对象xx将于整个程序结束后方由编译器析构释放。此时,无法在控制台中追踪析构情况。
接下来讨论一个变种情况,主函数代码修改如下:
可以看到,此时我在主函数中又申请了一个XX类型的对象t用来保存foo函数的返回值,这次运行又会发生什么事呢?
可以看到,析构函数的调用又少了一次!!
再次对foo函数进行展开分析:
Foo()的C++伪代码:
//第一次构造xx.XX::XX();
XX _temp0;
//第一次拷贝_temp0.XX::XX(xx);
XX t;
void foo(&t,_temp0)
{
//第二次构造yy.XX::XX();
//赋值yy.operator=(_temp0);
//第二次拷贝t.XX::XX(yy);
return;
}
//第一次析构yy.XX::~XX();
//第二次析构_temp0.XX::~XX();
可以看到,由于主函数中的对象t取代了临时对象_result,所以导致对_result的析构调用不存在了,而t本身是main函数中的对象,所以和xx一样在main函数结束的时候才会被析构调用,故无法追踪。
在对象作为返回值时,存在一种称为NRV的优化措施,该措施对于上面的代码展开的影响如下:
Foo()的C++伪代码:
//第一次构造xx.XX::XX();
XX _temp0;
//第一次拷贝_temp0.XX::XX(xx);
XX t;
void foo(&t,_temp0)
{
//第二次构造t.XX::XX();
//赋值t.operator=(_temp0);
return;
}
//第一次析构_temp0.XX::~XX();
可以看到,在开启NRV优化的情况下,局部对象yy将被临时对象_result或是用来保存返回值的对象t所替代,所以对于对象yy的析构调用也不存在了,所以只会有一次析构调用。