【C++11】右值引用与移动语义

目录

什么是左值 / 右值

在C++中,所有的值如果不是左值,就是右值。

左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。

  • 有名字的对象都是左值,右值没有名字。
  • 还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。

举例

cpp 复制代码
int ii = 3;    		//ii是左值,可以取地址,3是右值,无法取地址
int  jj = ii+8; 	//jj是左值,ii+8是右值
T a  = getTemp();   //a是左值,getTemp函数的返回值是右值(临时变量)
					//如果getTemp返回的是引用,则是左值

左值引用,右值引用

C++98中的引l用很常见,就是给变量取个别名,在C++11中,因为增加了右值引l用的概念,所以C++98中的引用都称为了左值引l用。

  • 右值引用就是给右值取个名字。
  • 语法:数据类型&& 变量名 = 右值;
  • 功能:右值引用就是给右值起名,一旦完成起名,则从临时变量(右值)变成了变量(左值),临时变量(右值)获得新生,也就是拥有了新的生命周期,只要变量名字存在,就一直存活。
cpp 复制代码
#include <iostream>
using namespace std;

class AA
{
public:
	int m_a = 9;
};

AA getTemp()
{
	return AA();
}

int main()
{
	int&& a = 3;	//3是右值
	
	int b = 8;		//b是左值
	int&& c = b + 5;//b+5是右值

	AA&& aa = getTemp(); //getTemp()的返回值是右值

	//右值一旦取了名字,就成了普通变量,也就是成了左值,a是左值,c是左值,aa也是左值
	cout << a << endl;
	cout << c << endl;
	cout << aa.m_a << endl;
	return  0;
}

常量左值引用

常量左值引用是个奇葩,它可以算是一个万能的引用类型,它可以绑定左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。

语法:const 类型& 变量名 = 左值/右值

在函数传参的时候有时候会用到,挺重要的

移动语义

右值引用的目的就是为了实现移动语义。

引入:

如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。

深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。

如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++11新增加的移动语义就能够做到这一点。

实现移动语义要增加两个函数:移动构造函数和移动赋值函数

移动构造函数的语法:类名(类名&& 源对象){}

移动赋值函数的语法:operator=(类名&& 源对象){}

功能:对右值使用移动语义,效率高很多

cpp 复制代码
#include <iostream>
using namespace std;


class AA
{
public:
	int* m_data = nullptr;	//数据成员
	
	AA(){}

	//给数据成员分配内存
	void alloc()
	{
		m_data = new int;		//分配内存
		memset(m_data, 0, sizeof(int));	//初始化
	}

	//拷贝
	AA(const AA& a)
	{
		cout << "调用了拷贝构造函数\n";
		if (m_data == nullptr) alloc();		//如果没分配内存,就分配
		memcpy(m_data, a.m_data, sizeof(int));	//拷贝
	}

	//移动构造函数
	//如果参数是右值会调用这个
	AA(AA&& a)
	{
		cout << "调用了移动构造函数\n";
		if (m_data != nullptr) delete m_data;		//如果没分配内存,就分配
		m_data = a.m_data;
		a.m_data = nullptr;
	}

	//赋值
	AA& operator=(const AA& a)
	{
		cout << "调用了赋值函数 \n";
		if (this == &a) return *this;	//避免自我赋值

		if (m_data == nullptr) alloc();
		memcpy(m_data, a.m_data, sizeof(int));	//拷贝
		return  *this;
	}

	//移动赋值函数
	//如果参数是右值会调用这个
	AA& operator=(AA&& a)
	{
		cout << "调用了移动赋值函数 \n";
		if (this == &a) return *this;	//避免自我赋值

		if (m_data != nullptr) delete m_data;		
		m_data = a.m_data;
		a.m_data = nullptr;
		return  *this;
	}

};


int main()
{
	AA a1;
	a1.alloc();
	*a1.m_data = 3;
	cout << *a1.m_data << endl;

	AA  a2 = a1;	//调用拷贝构造函数
	cout << *a2.m_data << endl;

	AA a3;
	a3 = a1;	//调用赋值函数
	cout << *a3.m_data << endl;

	auto f = [] {AA aa; aa.alloc(); *aa.m_data = 8; return aa; };//返回AA类对象的lambda函数
	AA a4 = f();		//返回的临时变量作为右值,调用移动构造函数
	cout << *a4.m_data << endl;

	AA  a5;
	a5 = f();	//调用移动赋值函数
	cout << *a5.m_data << endl;

	return 0;
}

注意事项

  • 1)对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?
    • C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。
    • 左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
cpp 复制代码
	AA a1;
	a1.alloc();
	*a1.m_data = 3;
	cout << *a1.m_data << endl;

	AA  a2 = move(a1);	//调用移动拷贝构造函数
	cout << *a2.m_data << endl;
  • 2)如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
  • 3)C++11中所有容器都实现了移动语义,避免对含有资源的对象发送无谓的拷贝。
  • 4)移动语义对于拥有资源(如内存,文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
相关推荐
枫叶丹418 分钟前
【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(三)
开发语言·前端·javascript·华为·harmonyos
SomeB1oody30 分钟前
【Rust自学】16.3. 共享状态的并发
开发语言·后端·rust
西猫雷婶34 分钟前
python学opencv|读取图像(四十七)使用cv2.bitwise_not()函数实现图像按位取反运算
开发语言·python·opencv
Xzh04231 小时前
c语言网 1127 尼科彻斯定理
数据结构·c++·算法
背太阳的牧羊人1 小时前
分词器的词表大小以及如果分词器的词表比模型的词表大,那么模型的嵌入矩阵需要被调整以适应新的词表大小。
开发语言·人工智能·python·深度学习·矩阵
wanghao6664552 小时前
Java基础面试题总结(题目来源JavaGuide)
java·开发语言
qystca3 小时前
【16届蓝桥杯寒假刷题营】第2期DAY5
c++·算法·蓝桥杯·贡献度
HYUJKI3 小时前
自定义注解
java·开发语言·spring
这是我583 小时前
链表的介绍
数据结构·c++·其他·链表·visual studio·介绍·图文结合
步、步、为营3 小时前
C#牵手Blazor,解锁跨平台Web应用开发新姿势
开发语言·前端·c#