一、背景
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服务期高级架构。