个人对移动语义一些理解

本文介绍了C++中的移动语义,详细讲解了数据类型、值类别(左值、右值、临终值)的概念,重点分析了std::move的作用和应用场景。通过示例展示了如何利用移动语义优化性能,减少不必要的对象复制,从而提高程序效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

个人对移动语义一些理解


需要知道的一些概念

数据类型

(英语:Data type),又称资料型态资料型别,是用来约束数据的解释。在编程语言中,常见的数据类型包括原始类型(如:[整数]、[浮点数]或[字符])。

值类别

这是语义层面概念。C++中,左值和右值是表达式的分类,一个表达式必然是左值或右值之一。C++11把上述左值性(lvalueness)扩充为更复杂的值类别(value category),包含左值(lvalue),纯右值(prvalue)和临终值(xvalue)三个基本分类(fundamental)

关于值类别,有一点需要注意,值类别是用于告诉编译器如何处理这个资源。

  • 左值:意味着以后可能还会用这个资源。
  • 右值:告诉编译器以后不会再用到这个资源,编译器做一些优化比如节省拷贝构造或者直接销毁等

右值和左值是一种看待资源的观点,没有绝对之分,不能单纯的用字面常量或者临时变量等定义去理解。一个变量是左值还是右值取决于表达式,根据表达式定义来推断某个变量是左值还是右值,以便于编译器处理变量代表的资源,即告诉编译器当前如何对待某些资源。在日常使用中,判断变量的值类别即是左值还是右值需要根据当前使用情况判断,举个例子

class String
{
public:
	String(const char * other){ cout << "create\n"; }
    ~String() { printf("destory\n"); }
private:
	char* m_string;
	uint32_t m_size;
};

class Entity
{
public:
	Entity(String& name):m_name(name){}
private:
	String m_name;
};
int main()
{
    // String tmp("mj")
    Entity name("mj");
    // ~tmp
}
  • main函数 创建对象 name 的时候,会创建一个临时变量(发生了隐式转换) 假设String tmp("mj"),作为 name 构造函数的参数,tmpmain函数中是右值(临时变量,只用一次),但是在 name构造函数中,tmp (引用) 是个左值,因为可能后续也要用。

右值引用:例如 String&& 。右值引用本质上是引用了一个变量(和引用的定义有关)。有句话说,右值引用本身是个左值。在这里,需要理解:以一个类的构造函数为例

Entity(String&& name)
		:m_name(name)
int main()
{
    Entity name("mj");
}
  • 右值引用是数据类型:对一个变量(资源)的引用,这个变量(资源)在其他函数视为右值;在本例中就是 main函数中会创建一个 String的临时变量,这个变量在 mian函数视作右值(只用一次,临时的,编译器可以随意优化),但是传入 ‘Entity’ 的构造函数之后,是一个左值,因为可能还要用。
  • 左值是类别,这个变量(资源)的引用在当前函数视为左值

右值引用属于右值的范畴,这里着重讲解右值引用,是 因为在实际引用中,参数更多通过引用的形式传递。


std::move

人为的进行值类别转换,转换成右值。一般来说让一个左值变成右值。这里变化只是语义变换,更改类值类别,不会修改数据,不是什么很神奇的东西,就相当于换个角度对待(一个不恰当的例子:番茄,有些人叫西红柿,本质是一个东西)。编译器可以根据值类别来确定是否进行优化。

为什么某些情况需要使用,因为有时候人比编译器知道的信息更多,知道某些资源只用一次,便可转化为右值。

具体应用

思考以下,为什么需要进行值类别转换?先给出结论:减少开销。

class String
{
public:
	String(const char * other)
	{
		cout << "create\n";
		m_size = strlen(other);
		m_data = new char[m_size];
		memcpy(m_data, other, m_size);
	}
	String(const String& other)
	{
		cout << "copy\n";
		m_size = other.m_size;
		m_data = new char[m_size];
		memcpy(m_data, other.m_data, m_size);
	}

	void Print()
	{
		for (uint32_t i = 0; i < m_size; i++)
		{
			printf("%c", m_data[i]);
		}
		printf("\n");
	}

	~String() 
	{
		delete m_data;
		printf("destory\n");
	}

private:
	char* m_data;
	uint32_t m_size;
};

class Entity
{
public:
	Entity(String& name)
		:m_name(std::move(name)){  }

	void PrintName(){ m_name.Print(); }
private:
	String m_name;
};

int main()
{
	Entity name("mj");
	name.PrintName();
}

首先来观察这段代码,一个很常见的代码。String是一个类,用于存放字符串。Entity中有一个成员变量就是 String 类的对象。思考一个问题,当创建一个 Entity 对象的时候,一种经常会出现的初始化便是给一个字符串常量作为参数。思考一下,会发生什么,会创建多少个 String 的对象。

运行结果:

create
copy
destory
mj
destory

观察运行结果:

​ create 代表以字符串常量为参数创建了一个 String 对象,copy 代表用拷贝构造方法创建了一个对象。即创建了两个 String 对象。而且在 调用 name.PrintName 之前,有一个 String 对象进行了析构。

可以知道,在创建 name 对象的时候,程序创建了一个临时的 String 对象,方便讨论记为 tmp 。为什么会创建一个 tmp,因为Entity的构造函数的参数是 String 类型,故发生了隐式转换,申明一个 tmp 对象,它的参数是 name 的参数 即 “mj”,然后,tmp作为 name 的构造函数参数。待 name 对象创建完成,tmp销毁。

为什么会发生copy,显而易见, Entity 有个成员变量时 String 类型,初始化的时候参数是 String 类型,故发生拷贝构造。

思考:
1. 在 main 函数中,一个临时的 String tmp 是左值还是右值。
2. 在 Entity的构造函数中,作为参数传入的 tmp,是左值还是右值。

答案:1. 右值 2.左值

右值告诉编译器,我只用一次。编译器会自动进行优化,本例中就是用完销毁,减少内存占用。


减小开销,优化性能

在这种情况下,如何优化能。仔细观察发现,tmp 是一个必须要创建的临时变量(右值),Entity 的成员变量 m_name 也要进行拷贝构造初始化。既然 tmp 本来只用一次然后销毁,那可不可以在 m_name 初始化的时候,把 tmp 的资源 “偷” 过来,这样实际上只需要 申请 一个 String 对象所需要的资源。减少了开销。

如何实现,需要给 String 类增添一个以 String右值引用(String&&)为参数的构造函数。这个右值引用是为了和左值引用( String&)区分开来,实现函数重载。

添加代码:

	String(String&& other) noexcept
	{
		cout << "move\n";
		m_size = other.m_size;
		m_data = other.m_data; //不申请内存,直接指向 other 已分配的资源
		other.m_size = 0;
		other.m_data = nullptr;// 让 other 析构的时候,不释放资源,因为有其他对象使用
	}

修改代码:

Entity(String&& name) //不要添加 const ,因为这个参数是会在构造 m_name 时候被修改的,添加了就相当于左值的效果了
		:m_name(std::move(name))//为啥这里加std::move 因为在这个函数中,name 是一个左值,不修改的话 在构造 m_name 的时候编译器会调用 左值为参数的 构造函数 String(const String& other) 而不是 String(String&& other) noexcept
{  }

再次运行

create
move
destory
mj
destory

拓展使用 移动赋值运算符

添加赋值运算符重载

String& operator=(String&& other)noexcept
	{
		if (this != &other)
		{
			cout << "move\n";
			delete[] m_data;
			m_size = other.m_size;
			m_data = other.m_data;
			other.m_size = 0;
			other.m_data = nullptr;
		}

		return *this;
	}

执行代码

int main()
{
	String firstName("jian");
	String secondName("ma");
	cout << "firstName:";
	firstName.Print();
	cout << "secondName:";
	secondName.Print();
	cout << "----------------\n";
	firstName = std::move(secondName);//就好像把secondName的资源移动到 firstName 中。
	cout << "firstName:";
	firstName.Print();
	cout << "secondName:";
	secondName.Print();
}

运行结果:

create
create
firstName:jian
secondName:ma
----------------
move
firstName:ma
secondName:
destory
destory

总结:

移动语义可以帮助我们进行性能优化,减少拷贝。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值