一、背景
C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
二、move语义
作用:就是将左值转换为右值。
三、左值和右值
- 左值可以取地址,位于等号左边
- 右值不能取地址,位于等号右边
四、左值引用和右值引用
引用的本质是别名,传参时引用可以避免拷贝,并且在函数内部可以修改外部的值。
4.1 左值引用
定义:能指向左值,不能指向右值的引用称为左值引用。代码示例:
int a = 5;
int &left_ref_a = a; // 左值引用指向左值,ok
int &left_ref_a = 5; // 左值引用指向右值,错
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是,const 左值引用是可以指向右值的:
const int& left_ref_a = 5; // ok
因为,const 左值引用不会修改指向的值,因此可以指向右值。这也是为什么要使用const &作为函数参数的原因之一,因为const &即可以指向左值又可以指向右值。比如:vector的push_back成员函数,如果没有const &,那么在push_back右值的时候就会报错。
// void push_back(const T& val);
vector<int> vc;
vc.push_back(5);
4.2 右值引用
顾名思义就是指向右值的引用,用来专门指向右值的,右值引用的标志是&&。
int a = 5;
int &&right_ref_a = 5; // ok
int &&left_ref_a = a; // 编译报错,指向了左值
right_ref_a = 50; // 右值引用的作用,可以修改右值
右值引用可以指向左值吗?可以,通过move就可以。
int a = 5;
int &&right_ref_a = 5; // ok
int &&right_ref_a = std::move(a); // 通过move将左值转为右值
4.3 左值引用和右值引用的思考
- 实际上,std::move移动不了什么,唯一的作用就是把左值强制转化为右值;
- 右值引用的本质是什么??为什么要有右值引用?右值引用能够指向右值,本质是把右值提升为左值,并定义一个右值引用通过std::move指向该左值。
int main()
{
int &&right_ref_a = 5;
right_ref_a = 6;
// 上面的代码等价于
int tmp = 5;
int &&right_ref_a = std::move(tmp);
right_ref_a = 50;
std::cout << "tmp: " << tmp << std::endl; // tmp=??? 5还是50,答案是:50
}
- 左值引用和右值引用本身是左值还是右值?答案是:左值。因为声明出来的左值引用和右值引用都是有地址的,位于等号左边,所以都是左值。验证代码如下:
// 形参是右值引用
void ChangeValue(int &&right_val)
{
right_val = 100;
}
int main()
{
int a = 5;
int &left_ref_a = a;
int &&right_ref_a = std::move(a);
ChangeValue(a); // 编译报错,a是左值
ChangeValue(left_ref_a); // 编译报错,left_ref_a是左值
ChangeValue(right_ref_a); // 编译报错,right_ref_a是左值
ChangeValue(std::move(a)); // 编译ok
ChangeValue(std::move(left_ref_a)); // 编译ok
ChangeValue(std::move(right_ref_a)); // 编译ok
// 这三个左值的地址是一样的
std::cout << "&a " << &a << std::endl;
std::cout << "&left_ref_a " << &left_ref_a << std::endl;
std::cout << "&right_ref_a" << &right_ref_a << std::endl;
}
4.4 小结
- 从性能上讲,左值引用和右值引用都能避免拷贝,没什么区别;
- 右值引用即可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const T&也能指向右值);
- 作为函数形参时,右值引用更加灵活,虽然const的左值引用也能做到左右值都能接受,但是它无法修改,有一定的局限性。
4.5 右值引用和std::move的使用场景
右值引用优化性能,避免深拷贝。
场景:对于还有堆内存的类,我们需要实现它的深拷贝构造函数,如果没实现,会调用该类的默认复制构造函数,导致多次释放同一资源。
4.5.1 浅拷贝重复释放
#include <iostream>
using namespace std;
class A {
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag) return a;
else return b;
}
int main() {
{
A a = Get(false);
cout << "main finish" << endl;
return 0;
}
4.5.2 深拷贝构造函数
class A {
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a):m_ptr(new int(*a.m_ptr)){
cout << "Copy constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
4.5.3 移动构造函数
核心:当复制构造函数和移动构造函数同时存在时,会优先调用移动构造函数。移动构造函数只是将对象的资源做了浅拷贝,从而避免的深拷贝,提高性能。这也就是所谓的移动语义,右值引用的一个重要作用就是支持移动语义。前提是需要实现移动构造函数。如果没有实现就会调用复制构造函数!
class A {
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a):m_ptr(new int(*a.m_ptr)){
cout << "Copy constructor A" << endl;
}
// 移动构造函数,可以浅拷贝
A(A&& a):m_ptr(a.m_ptr){
a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
cout << "Move constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
文章参考与<零声教育>的C/C++linux服务期高级架构。