c++11新特性——右值引用和move语义

一、背景

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 左值引用和右值引用的思考

  1. 实际上,std::move移动不了什么,唯一的作用就是把左值强制转化为右值
  2. 右值引用的本质是什么??为什么要有右值引用?右值引用能够指向右值,本质是把右值提升为左值,并定义一个右值引用通过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
}
  1. 左值引用和右值引用本身是左值还是右值?答案是:左值。因为声明出来的左值引用和右值引用都是有地址的,位于等号左边,所以都是左值。验证代码如下:
// 形参是右值引用
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 小结

  1. 从性能上讲,左值引用和右值引用都能避免拷贝,没什么区别;
  2. 右值引用即可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const T&也能指向右值);
  3. 作为函数形参时,右值引用更加灵活,虽然const的左值引用也能做到左右值都能接受,但是它无法修改,有一定的局限性。

4.5 右值引用和std::move的使用场景

右值引用优化性能,避免深拷贝。

场景:对于还有堆内存的类,我们需要实现它的深拷贝构造函数,如果没实现,会调用该类的默认复制构造函数,导致多次释放同一资源。

4.5.1 浅拷贝重复释放

#include  
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服务期高级架构。

你可能感兴趣的:(c++,c++,开发语言)