C++函数的返回过程基本可以分为两个阶段,返回阶段和绑定阶段,根据两个阶段中需要返回的值的类型不同(返回值和引用),和要绑定的值的类型(绑定值和引用)会产生不同的情况。
最基本的规则是先返回,再绑定,返回和绑定的时候,都有可能发生移动或者拷贝构造函数的调用来创建临时对象,并且只会发生一次。更具体的,当返回值的时候,函数返回之前,会调用一次拷贝构造函数或移动构造函数,函数在绑定到值的时候,会发生一次拷贝构造函数或移动构造函数,如果返回时已经调用了构造函数,则绑定时不会再调用,而是直接绑定。返回引用或者绑定到引用,在各自阶段,不会产生构造函数的调用。
下面举例阐述各种可能的情况(例子中很多情况我们是不该这样写的,本文只讨论如果这样做了会怎样)。
关于右值引用和移动构造函数的解释,后面会写篇东西做个总结。这里仅需要一条规则,那就是在需要调用移动构造函数的情况,如果类没有定义移动构造函数,则调用它的拷贝构造函数,后面对未定义移动构造函数的情况,不再赘述。
为叙述方便,定义绑定值和返回值:
绑定值就是函数赋给的变量,返回值就是函数的返回值,最终的调用形式类似:
返回值 fun()
{
.....
return 返回值;
}
绑定值 = func();
定义一个类:
class myClass
{
public:
//构造函数
myClass()
{
cout << "construct" << endl;
}
//拷贝构造函数
myClass(const myClass& rhs)
{
cout << "copy construct" << endl;
}
//移动构造函数
myClass(myClass&& rhs)
{
cout << "move consturct" << endl;
}
//析构函数
~myClass()
{
cout << "desctruct" << endl;
}
};
1.绑定值类型为值类型,返回值类型为值类型,返回的是局部变量:
在函数返回阶段,调用类的移动构造函数创建返回值,并绑定到绑定值上。移动构造函数的调用发生在函数返回阶段。
例子:
myClass fun()
{
myClass mc;
return mc;
}
int main()
{
myClass result = fun();
}
输出:
construct
move consturct
desctruct
desctruct
2.绑定值类型为值类型,返回值类型为值类型,返回的是局部变量的引用:
首先,这不是一个好的做法,企图返回一个局部变量的引用结果通常并不会令人满意。函数调用结束后,函数帧被回收(实际上只是设置了栈顶的地址),函数栈中的临时变量不再有意义。但是这么做通常也不会造成什么副作用,因为在函数返回阶段,会调用拷贝构造函数(注意,即使定义了移动构造函数也不会调用),将生成的新的临时对象,绑定到绑定值上。
例子:
myClass fun()
{
myClass mc;
myClass& mcRef = mc;
//如果是想返回这个局部变量的引用,并且在函数外部对其进行修改,那必然要失望了。
return mcRef;
}
int main()
{
myClass result = fun();
}
输出:
construct
copy construct
desctruct
desctruct
3.绑定值类型为值类型,返回值类型为值类型,返回的是全局变量的引用:
和2中的情形类似,但是由于全局变量的生命周期超过当前函数,所以即使函数返回,变量仍然存活。在函数返回阶段,仍然会调用拷贝构造函数,将产生得临时对象绑定到绑定值上,却依然不能返回全局对象。
例子:
通常,返回作为参数而传递进来的引用的情况更加常见,此时的参数对于函数来说,和全局变量类似,即函数返回,变量的生命周期也不会结束。
myClass fun(myClass& param)
{
//最终只能得到param的拷贝
return param;
}
int main()
{
myClass mc;
myClass result = fun(mc);
}
输出:
construct
copy construct
desctruct
desctruct
4.绑定值类型为值类型,返回值类型为引用类型,返回的是局部变量或局部变量的引用:
和情况2情形类似的是,希望以引用的方式返回局部变量或者局部变量的引用,通常也不能取得令人满意的结果;但与情况2的情形不同的是,情况2基本不会产生什么副作用,情况2中在函数返回阶段对对象进行拷贝,此时对象完整;但是在本情况中,就会产生严重的副作用。由于返回值类型为引用类型,在函数返回阶段,并不会调用拷贝构造函数,而在绑定阶段,绑定值是值类型,才会产生拷贝构造函数的调用,而此时函数已经返回,拷贝函数拷贝的是函数中的临时变量,此时已经析构了,再进行拷贝,只会得到一个被析构对象的拷贝,通常,这会产生严重的错误。简单的说,这种情况下,会先析构临时对象,在拷贝这个临时对象。
例子:
myClass& fun()
{
myClass mc;
//或者myClass mcRef = mc; return mcRef
return mc;
}
int main()
{
myClass result = fun();
}
输出:
construct
desctruct
copy construct
desctruct
5.绑定值类型为值类型,返回值类型为引用类型,返回的是全局变量的引用:
基本的情况和2是类似的,即并不能真的的到全局变量的引用,但是也不会产生什么副作用(除非调用者就是希望或者这个全局变量,并进行修改)。由于全局变量在函数结束之后并不会被析构,所以对它调用拷贝构造函数是安全的。和情况4类似,拷贝函数的调用也不是发生在函数返回阶段,而是发生在绑定阶段。
例子:
同样使用参数为引用类型的函数举例:
myClass& fun(myClass& param)
{
return param;
}
int main()
{
myClass mc;
myClass result = fun(mc);
}
输出:
construct
copy construct
desctruct
desctruct
上述5中情况是将返回值绑定到值类型的情形,所有情形中,均会出现拷贝或移动构造函数的调用,最终绑定之后,都不可能得到原来的对象,企图通过这种方式来修改原对象,必然会失败。
如果绑定值类型为引用类型,返回值类型为值类型,返回的是局部变量,局部变量的引用或者全局变量会怎么样呢?
这种情况编译器会报错,由于函数返回的是值类型,必然会产生移动构造函数或者拷贝构造函数,产生一个临时对象,而临时对象是一个右值,我们常说的引用,其实是左值引用,而一个右值对象是不能够绑定到一个左值引用的,所以编译器会报错。
我们可以将函数的返回值版绑定到右值引用变量(使用myClass&&),我们下面讨论返回右值引用的情况。但是,有时候返回一个右值引用并且使用它,可能并不能产生想要的结果(如本文最后例子)。
6.绑定值类型为右值引用,返回值类型为值类型,返回的是局部变量或局部变量的引用或全局变量的引用:
函数返回阶段,由于是返回值类型,所以会调用移动构造函数或者拷贝构造函数,创建一个不具名的临时变量(其实它就是一个右值),可以把它绑定到一个右值引用上。 但是对它的修改是没有什么意义的,本来函数创建的这个不具名临时变量,会在函数返回之后被析构,由于我们增加了一个指向它的引用,所以这个临时变量的生命周期被延长了,直到指向它的引用离开作用域,它才会析构。
例子:
myClass fun()
{
myClass mc;
return mc;
}
int main()
{
myClass&& result = func();
}
输出:
construct
move consturct
desctruct
desctruct
myClass fun()
{
myClass mc;
myClass& mcRef = mc;
return mcRef;
}
int main()
{
myClass&& result = fun();
}
输出:
construct
copy construct
desctruct
desctruct
7.绑定值类型为为引用类型,返回值类型为引用类型,返回局部变量或局部变量的引用:
由于函数返回阶段和绑定阶段都是引用类型,所以不会产生任何拷贝或移动构造函数的调用,最终绑定值类就是函数的局部变量,但是由于函数返回后,局部变量已经被析构,在保存这个局部变量的引用没有意义,往往会引起一些诡异的错误。
例子:
myClass& fun()
{
myClass mc;
//或者myClass& mcRef = mc; return mcRef;
return mc;
}
int main()
{
//在函数返回之后,我们甚至还能使用这个result,但是仅仅是编译器将原来result的位置继续解释成result变量,
//很有可能很快这个位置会被新的栈帧覆盖,继续使用result,可能会出现“诡异”的问题
myClass& result = fun();
}
输出:
construct
desctruct
8.绑定值类型为引用类型,返回值类型为引用类型,返回全局变量的引用:
通常,当我们这是我们的真实意图,函数在返回阶段和绑定阶段均不存在构造函数的调用,最终绑定值就是需要返回的全局变量,对最终绑定值的任何修改,均会反映到全局变量上。
例子:
依然使用参数为引用类型的函数举例。
myClass& fun(myClass& mc)
{
return mc;
}
int main()
{
myClass mc;
myClass& result = fun(mc);
}
输出:
construct
desctruct
9.一个思考:右值是不能绑定到左值引用的,但是当把右值付给左值会怎样呢?
如下
int main()
{
myClass mc;
myClass result = move(mc);
}
答案是会调用移动构造函数创建一个新的对象,并且绑定到绑定值。
输出:
construct
move consturct
desctruct
desctruct
另一个非常类似的例子:
int main()
{
myClass mc;
myClass&& mcRightRef = move(mc);
myClass result = mcRightRef;
}
和第一个非常类似,但是result的生成是调用拷贝构造函数而非移动构造函数完成的。
输出:
construct
copy construct
desctruct
desctruct
这就涉及到了移动构造函数调用的时机,更多关于右值,右值引用和移动构造函数详细的研究,待续。