本文适合对c++11刚入门的小伙伴阅读。
本文加深对左值引用、右值引用、左值右值的理解。
首先要理解一下将亡值:将亡值就是在这一行结束之后该值将被析构。最典型的将亡值就是匿名对象,如:Test()
。
c++11之前,有一些让人们蛋疼的地方。
需求1:需要转递引用来提高效率,那么我们函数定义是这样的void foo(Test &t){...}
。
需求2:现在我们想这样调用函数,foo(Test())
。what?编译不过。稍微解释一下,将引用绑定到一个匿名对象,完全没有意义,因为它可能很快就不在了。访问一个不在的对象,是多么恐怖的事情。
我们又想出了其他的办法,重载函数void foo(Test t){...}
。what?还编译不过。因为这样重载又二义性,编译器不知道你到底要调用哪个。
最后没辙了,我们只能放大招了,void foo(const Test& t){...}
只能这样了。const
不是只读的意思吗,怎么还可以这样用。对的const
干的活比较多,const Test& t{Test()};
这种用法就退化回const Test t{Test()};
,这两种写法都会创建一个新的对象,所以const
干了一个不属于它的活,这样即保障了传引用的高效又可以传入匿名对象。
需求3:但是我们又需要改变它的值,那好吧,我们只能用const_cast<Test&>(t)
强转后来改变他的值。
c++11引入了右值引用来帮助const
分担工作。
现在完全可以用void foo(Test&& t)
和void foo(Test& t)
两个函数来区分传入值是否是将亡值,并且可以重载,无二义性。
注意下面情况:
#include <iostream>
using namespace std;
class Test {
public:
Test() : x(0) {cout << "构造函数 this = " << this << endl;}
Test(int x) : x(x) {cout << "构造函数 this = " << this << endl;}
Test(const Test& another) : x(another.x) {cout << "拷贝构造 this = " << this << " from " << &another << endl;}
Test(const Test&& another) noexcept : x(another.x) {cout << "移动构造 this = " << this << " from " << &another << endl;}
~Test() {cout << "析构函数 this = " << this << endl;}
int x;
};
ostream& operator<<(ostream& out, const Test& t) {
out << "&t = " << &t << ", x = " << t.x;
return out;
}
class Aa {
public:
Aa(const Test& t) : t(t) { cout << t << endl; cout << this->t << endl;}
//Aa(const Test&& t) = delete; //这行就可以禁止将亡值来赋值,使编译时报错。
void foo() {cout << t << endl;}
private:
const Test &t;
};
int main()
{
Aa a{Test()};
a.foo();
return 0;
}
输出:
构造函数 this = 0x61fe0c
&t = 0x61fe0c, x = 0
&t = 0x61fe0c, x = 0
析构函数 this = 0x61fe0c
&t = 0x61fe0c, x = 0
解释:
第一步:入参const Test& t{Test()};
产生了一个临时的匿名只读对象,还是将亡值。
第二步:初始化列表t(t)
相当于将刚刚产生的匿名对象,赋值给成员对象&t
。
第三步:构造函数结束,匿名对象析构,之后再使用Aa
时很危险的。
将移动构造函数显视删除,可以避免这一点。
移动构造函数当然不只是这一点功能,它主要是在stl
中和std::move
配合提高效率。
左值引用和右值引用的区别
左值引用是起别名,如果这个对象已经析构,那么这个别名也应该一起失效。言外之意就是左值引用一定要保证它的生命周期小于等于它被引用的对象。
当将亡值出现的时候,左值引用表示无能为力,所以右值引用出现了。
右值引用也可以看作起名,只是它起名的对象是一个将亡值。然后延续这个将亡值的生命,直到这个的右值的生命也结束了。
除了入参时可以用到右值引用外,其他右值引用都显得多余。
比如Test&& t{Test()};
它和Test t{Test()}
是一样的,甚至可以这样Test&& t{}
它们都只一个普通对象,只调用一次构造函数。
总结
左值引用只能起别名,但不能给匿名对象起名。
右值引用其实就是给匿名(天生匿名或者通过std::move
将名字失效,这样的对象即将被析构)对象重新起名字。
我们一直所说的将亡值其实就是所谓的右值,其它有名字的都是左值,左值引用与左值配合,右值引用与右值配合。