【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)移动语义对于拥有资源(如内存,文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
相关推荐
健康平安的活着14 分钟前
java之 junit4单元测试Mockito的使用
java·开发语言·单元测试
No0d1es16 分钟前
电子学会青少年软件编程(C/C++)5级等级考试真题试卷(2024年6月)
c语言·c++·算法·青少年编程·电子学会·五级
DjangoJason2 小时前
C++ 仿RabbitMQ实现消息队列项目
开发语言·c++·rabbitmq
m0_480502642 小时前
Rust 入门 KV存储HashMap (十七)
java·开发语言·rust
大阳1232 小时前
线程(基本概念和相关命令)
开发语言·数据结构·经验分享·算法·线程·学习经验
YA3332 小时前
java基础(九)sql基础及索引
java·开发语言·sql
奇树谦3 小时前
QT|windwos桌面端应用程序开发,当连接多个显示器的时候,如何获取屏幕编号?
开发语言·qt
weixin_307779133 小时前
VS Code配置MinGW64编译GNU 科学库 (GSL)
开发语言·c++·vscode·算法
froginwe114 小时前
HTML 框架:构建网页布局的基石
开发语言
Yn3124 小时前
在 Python 中使用 json 模块的完整指南
开发语言·python·json