传值返回 和 传引用返回 有什么区别?
传值返回的过程:先 生成一个临时对象,拷贝需要传的那个对象,然后这个临时对象再作为 返回值 返回,因而会调用一次拷贝构造函数
结合例子理解:
看这个函数:
传值返回:返回对象d 的拷贝
class Date{
//....
}
Date func()
{
Date d;
return d;
}
int main()
{
func();
return 0;
}
func 函数结束,会析构对象 d,对象d会被销毁,因此需要 一个临时对象返回
说一下返回值接收问题
1、用 Date ref 接收:需要一次拷贝构造,但是这里存在权限放大
int main()
{
Date ref= func();
return 0;
}
2、用 const Date ref 接收:因为返回值为 临时对象(具有常性,权限为只读),若用 Date ref 接收,会使其权限放大,因此要 const 修饰,使权限缩小
int main()
{
const Date ref= func();
return 0;
}
3、用 Date& ref :上面两种情况都需要一次拷贝构造,这里使用引用直接省去一次拷贝
int main()
{
Date& ref= func();
return 0;
}
传引用返回:返回对象d 的别名
Date& func()
{
Date d;
return d;
}
结合上面的第三种返回值接收方式:
int main()
{
Date& ref= func();
return 0;
}
ref 是临时对象的别名,则 ref 也就是 对象d 的别名
此时注意!!!
首先,函数 func 结束阶段,会调用 对象d 的析构函数,析构掉 对象 d,数值都被析构成一些其他数值(如你定义的析构数值)
其次,函数 func 真正结束后, func的函数栈帧销毁,对象d 指向的那块空间也“释放掉”(数值不一定是 你定义的析构数值 了),此时别名 ref 相当于“野指针”了,指向一片“不属于你的空间”(对象别名的底层是指针,返回对象的别名,实际上是返回对象的指针)
所以,调试可以看到,ref 的日期数值 不是 对象d 的数值,而是一些奇怪的随机数,因为这片空间不是对象d 的,而是系统的了(系统自己会覆盖一下其他数上去)
另外,你在上面那段代码的基础上多加几个函数(随便定义几个),运行后可以发现,居然 ref 的日期数值又变其他数了
(根源就是 ref 指向的空间已经是系统的了,新定义的函数在同一块栈区开辟函数栈帧,新函数的数值可能会覆盖了这片空间,就可能直接覆盖到 ref所指的那片空间)
小结一下上面这段话
返回对象是一个局部对象或者临时对象,出了当前func函数作用域,就析构销毁了,那么不能用引用返回
虽然引用返回可以减少一次拷贝,但是用引用返回是存在风险的,因为引用对象在func函数栈帧已经销毁了
如果出了函数作用域,返回对象还在,才能用引用返回
注意其中的本质:存在函数栈帧销毁的风险
那么我们可以 将对象定义成 static ,定义在静态区,则不受栈区的影响
又或者 对象可以是 new 出来的,new在堆区开辟空间
但我们也不能以 是否在栈区来判断
看个反例:*赋值运算符重载的返回值 this 也在栈上,但是这个返回不会出事
解析: * this 是 对象本身,对象在main的作用域里创建,因此出 main作用域才析构销毁而出函数作用域不会销毁,所以 此处能引用返回
从这里又可以理解:只要你定义的对象,不在当前的函数栈帧里面就可以引用返回(这个函数销毁了也不会影响到自己)
// 赋值运算符重载
Date& operator=(const Date& d) {
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
记住一个核心:
出了作用域,返回对象还在没有析构,那就可以用引用返回,减少拷贝()
a、返回对象生命周期到了,会析构,传值返回
b、返回对象生命周期没到,不会析构,传引用返回
为什么要减少拷贝?
1、没有拷贝,提高效率
2、多拷贝生成一个对象,就要多用一次析构函数,也是开销
当要拷贝的内容非常多时,通常析构内容也不少,同时往往一个函数不传引用,会影响到很多个对象,性能开销将大大增加
因此,虽然传引用的条件稍微苛刻,但是尽量还是传引用