一.左值与右值
左值:可以取地址的表示数据的表达式,左值可以出现在赋值符号左边
右值:不能取地址的表示数据的表达式,右值不能出现在赋值符号左边
int fun()
{
return 0;
}
int main()
{
int a = 0;//a->左值
const int b = 1;//b->左值
int* p = &a;//*p->左值
a + b;//右值
func();//右值
10;//右值
}
二.左值引用与右值引用
左值引用:给左值取的别名,符号:type&
- 左值引用只能引用左值,不能引用右值
- 但是const左值引用既可引用左值,也可引用右值
右值引用:给右值取的别名,,符号:type&&0
- 右值引用只能右值,不能引用左值
- 但是右值引用可以move以后的左值
无论左值引用还是右值引用,都是在取别名,理论上来说不会开辟额外的空间
int a = 0, b = 0;
int& ref1 = a;//左值引用给左值取别名
const int& ref2 = a + b;//临时对象具有常性,左值引用想要绑定右值要将上const
int&& ref3 = a + b;//右值引用给右值取别名
int&& ref4 = move(a);//右值引用给move以后的左值取别名
说明:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
三.使用引用减少拷贝
当函数传参或者传返回值时,传的是自定义类型对象,如果处理不当,将会发生多次拷贝,尤其是需要深拷贝的类,大大降低效率,使用引用就可以有效解决这些问题。
1.左值引用减少拷贝
- 引用传参
- 传引用返回
左值引用的短板:当函数返回对象是局部对象,出作用域就会被销毁,此时只能传值返回,倘若该对象需要深拷贝,付出的代价是很大的。
为了解决这一问题,右值引用在C++11应运而生。
2.右值引用减少拷贝
(1)深拷贝的类DeepCopy
class DeepCopy
{
public:
DeepCopy(int* p)
:_p(p)
{}
~DeepCopy()
{
delete[] _p;
}
private:
int* _p;
};
int main()
{
DeepCopy d1 (new int[5]{ 1, 2, 3, 4, 5 });
DeepCopy d2 = d1;
return 0;
}
DeepCopy类就是一个需要深拷贝的类,因为它的成员着管理一块资源,析构时需要释放资源。如果使用编译器提供的浅拷贝构造,会导致同一块空间释放两次,因此需要我们自己提供深拷贝构造,同样,赋值也需要自己提供。
DeepCopy(const DeepCopy& d)
{
_p = new int[d._n];
_n = d._n;
for (int i = 0; i < _n; i++)
{
_p[i] = d._p[i];
}
cout << "拷贝构造" << endl;
}
DeepCopy& operator=(const DeepCopy& d)
{
if (this != &d)
{
delete[] _p;
_n = d._n;
_p = new int[_n];
for (int i = 0; i < _n; i++)
{
_p[i] = d._p[i];
}
}
cout << "拷贝赋值" << endl;
return *this;
}
(2)传值返回发生拷贝构造
传值返回的场景:
DeepCopy Fun()
{
DeepCopy d(new int[3]{ 1,2,3 }, 3);
return d;
}
int main()
{
DeepCopy d1 = Fun();
return 0;
}
整个过程如下:
由于是传值返回,所以先用d1拷贝构造一个临时对象,再用临时对象拷贝构造d。本来是分两步的,但连续的构造被编译器优化成一步。
请注意:临时对象是右值,const左值引用可以接收右值,故可以匹配拷贝构造函数
(3)传值返回发生移动构造
我们可以将右值分为两类:
- 纯右值:内置类型的右值,包括字面常量,表达式结果,函数返回值等等
- 将亡值:自定义类型的右值,即函数返回值,如它的名字一样,它不会再被使用,马上就会被销毁。
对于将亡值的拷贝,我们不用照着它的模版重新生成一份,而是直接转移它的资源,这样代价会小很多。因此,我们可以针对这种将亡值专门设计一个构造函数来转移资源,这种构造函数叫移动构造
//DeepCopy中增加:
DeepCopy(DeepCopy&& d)
{
_p = nullptr;
std::swap(_p, d._p);
std::swap(_n, d._n);
cout << "移动构造" << endl;
}
int main()
{
DeepCopy d1(new int[5]{ 1,2,3,4,5 }, 5);
d1 = Fun();
return 0;
}
编译器会将返回的d1对象特殊处理,将它识别为右值,所以这里用一个右值构造临时对象。右值既虽然可以被const左值引用接收,但右值引用是更合适的,所以会匹配移动构造。同理,临时对象也是个将亡值,所以匹配的是移动构造。两次连续的构造会被编译器优化成一次移动构造。
不仅有移动构造,还有移动赋值:
//DeepCopy中增加:
DeepCopy& operator=(DeepCopy&& d)
{
if (this != &d)
{
std::swap(_p, d._p);
std::swap(_n, d._n);
}
cout << "移动赋值" << endl;
return *this;
}
//临时对象的资源不仅被转移走了,还得到了*this不要的资源,析构时会释放掉
int main()
{
DeepCopy d1(new int[5]{ 1,2,3,4,5 }, 5);
d1 = Fun();
return 0;
}
3.总结
引用的意义就在于减少拷贝
左值引用:直接减少拷贝:
1.引用传参 2.传引用返回但有些场景不能传引用返回(函数内的局部对象),因此还是无法避免深拷贝
右值引用:间接减少拷贝
和const左值引用进行区分,传将亡对象拷贝时匹配移动构造函数,直接转移资源
四.完美转发
(1)属性退化
一个右值引用与右值绑定之后,这个引用的属性是左值。换句话说,右值被右值引用之后属性退化成了左值,从原来的不可修改变为可修改。如果你不想它被修改,可以用const修饰引用,但它仍然是一个左值。
从底层角度来看,实际是右值引用使数据的存储位置发生改变,可以取到数据的地址。
这也能够解释为什么移动构造或移动赋值函数中,能够转移将亡对象资源的原因,因为它的属性退化成了左值,可以被修改。
(2)万能引用
模版参数+&&=万能引用:无论是左值还是右值,无论是const还是非const,都能接收
我们可以用万能引用验证第(1)点的结论
void fun(int& x)
{
cout << "void fun(int& x)左值" << endl;
}
void fun(const int& x)
{
cout << "void fun(const int& x)const左值" << endl;
}
void fun(int&& x)
{
cout << "void fun(int&& x)右值" << endl;
}
void fun(const int&& x)
{
cout << "void fun(const int&& x)const右值" << endl;
}
void PerfectForward(T&& t)//T&& 万能引用
{
fun(t);
}
int main()
{
int a = 10;
PerfectForward(a);//左值
PerfectForward(10);//右值
const int b = 8;
PerfectForward(b);//const左值
PerfectForward(move(b));//const右值
return 0;
}
(3)完美转发
如果想要在传递过程中保持对象的左值或右值属性不变,可以使用完美转发std::forward<T>(T是对象的类型)
将上面的fun函数稍作修改:
void PerfectForward(T&& t)//T&& 万能引用
{
fun(forward<T>(t));
}
五.C++11新增的默认成员函数
C++11新增了两个默认成员函数:移动构造和移动赋值。
如果没有实现移动构造,且拷贝构造,拷贝赋值重载和析构函数都没有实现,那么编译器会生成移动构造函数,对于内置类型逐字节拷贝;对于自定义类型,如果有移动构造则去调用移动构造,没有就退而且其次,调用拷贝构造。
移动赋值的生成规则类似。