C++返回值是类名和返回值是引用的区别及说明
作者:四月晴
返回值是类名和返回值是引用的区别
返回非引用类型
函数的返回值用于初始化在调用函数时创建的临时对象(temporary object),如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。
在求解表达式的时候,如果需要一个地方存储其运算结果,编译器会创建一个没命名的对象,这就是临时对象。C++程序员通常用temporary这个术语来代替temporary object。
用函数返回值初始化临时对象与用实参初始化形参的方法是一样的。
当函数返回非引用类型时,其返回值既可以是局部对象,也可以是求解表达式的结果。
返回引用类型
当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身。
千万不要返回局部对象的引用!千万不要返回指向局部对象的指针!
当函数执行完毕时,将释放分配给局部对象的存储空间。此时对局部对象的引用就会指向不确定的内存!返回指向局部对象的指针也是一样的,当函数结束时,局部对象被释放,返回的指针就变成了不再存在的对象的悬垂指针。
返回引用时,要求在函数的参数中,包含有以引用方式或指针方式存在的,需要被返回的参数。
如果返回对象,最后多执行一次拷贝构造函数,如果返回引用,直接返回现存对象
#include <iostream> using namespace std; class Timer { public: Timer(); Timer(int, int, int); friend Timer &operator+(Timer&, Timer&); friend Timer operator-(Timer&, Timer&); friend ostream& operator<<(ostream &out, Timer &t); friend istream& operator>>(istream &in, Timer &t); private: int hour, minute, second; }; Timer::Timer() { hour = 0; minute = 0; second = 0; } Timer::Timer(int hour, int minute, int second) { this->hour = hour; this->minute = minute; this->second = second; } Timer & operator+(Timer& a, Timer &b) { a.second = a.second + b.second; a.minute = a.minute + b.minute; a.hour = a.hour + b.hour; if (a.second >= 60) { a.second = a.second - 60; a.minute++; } if (a.minute >= 60) { a.minute = a.minute - 60; a.hour++; } return a; } Timer operator-(Timer &a, Timer &b) { Timer c; c.hour = a.hour - b.hour; c.minute = a.minute - b.minute; c.second = a.second - b.second; if (c.second < 0) { c.second += 60; c.minute--; } if (c.minute < 0) { c.minute += 60; c.hour--; } return c; } ostream& operator<<(ostream &out, Timer &t) { out << t.hour << ":" << t.minute << ":" << t.second << endl; return out; } istream& operator>>(istream&in, Timer &t) { cout << "Input hours and minutes." << endl; in >> t.hour >> t.minute >> t.second; return in; } int main() { Timer t1, t2, t3, t4; cin >> t1 >> t2; cout << "t1=" << t1; cout << "t2=" << t2; t3 = t1 + t2; cout << "t3=t1+t2=" << t3; t4 = t1 - t2; cout << "t4=t1-t2=" << t4; return 0; }
刚开始我将函数声明为:
friend Timer&operator+(Timer&,Timer&); friend Timer&operator-(Timer&,Timer&);
对于<<,>>的重载声明的和长代码一样。但是这样做之后我发现出现异常。
在Time &operator+(Timer&t1,Timer&t2)里我声明了一个Timer的局部变量,而当函数调用结束后,该指针变成了悬挂指针。
所以,一定谨记不要在返回值为引用的函数中返回局部变量。
C++函数返回值和返回引用问题
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
这就涉及到了移动构造函数调用的时机,更多关于右值,右值引用和移动构造函数详细的研究,待续。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。