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

这就涉及到了移动构造函数调用的时机,更多关于右值,右值引用和移动构造函数详细的研究,待续。

  • 31
    点赞
  • 158
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值