C++11:左值与右值|移动构造|移动赋值

news2024/11/18 8:47:13

                                               🎬慕斯主页修仙—别有洞天

                                              ♈️今日夜电波:マイノリティ脈絡—ずっと真夜中でいいのに。

                                                                0:24━━━━━━️💟──────── 4:02
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


 

目录

左值与右值

什么是左值?什么是左值引用?

什么是右值?什么是右值引用?

总结

移动构造与移动赋值

引入

纯右值和将亡值

移动构造与移动赋值

移动构造(Move Construction)

移动赋值(Move Assignment)

move

对于移动构造与移动赋值的一些注意事项

万能引用与完美转发


 

左值与右值

什么是左值?什么是左值引用?

        左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。 如下:

// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

什么是右值?什么是右值引用?

        右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。 如下:

// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1

总结

        左值可以被取地址,右值不可被取地址!左值引用和右值引用都不能互相给对方取别名!但是,const左值引用可以!右值引用可以move(左值)取别名!

        一个右值被右值引用后属性是左值!!!右值不能被修改但是右值引用后需要被修改!否则无法实现移动构造和移动赋值!

 

移动构造与移动赋值

引入

        接下来看一个场景:如下两个函数都可以传入右值,在C++11前这样对于左值以及右值是很难区分的,在引入右值后,就可以根据场景来使用左值引用还是右值引用了!

void Test(const int& aa)
{
	cout << "const int& aa :" << aa << endl;
	//aa = 30; err
}

void Test(int&& aa)
{
	cout << "int&& aa :" << aa << endl;
	aa = 30;
	cout << "int&& aa :" << aa << endl;

}

        如果是仅仅为了区分左右值那是不是太过鸡肋了?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的! 如下是之前我们实现的一个string类:

namespace lt
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

        我们在如下的场景中使用了多次的深拷贝会导致运行效率的降低:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。如下:lt::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(这是因为编译器优化,如果是一些旧一点的编译器可能是两次拷贝构造)。也就是至少要进行一次深拷贝,那么这样的代价也太大了!

lt::string to_string(int value)
{
	bool flag = true;
	if (value < 0)
	{
		flag = false;
		value = 0 - value;
	}
	bit::string str;
	while (value > 0)
	{
		int x = value % 10;
		value /= 10;
		str += ('0' + x);
	}
	if (flag == false)
	{
		str += '-';
	}
	std::reverse(str.begin(), str.end());
	return str;
}

int main()
{
    lt::string ret1 = lt::to_string(1234);
    return 0;
}

        我们可以利用右值的特性进一步的提升效率,首先理解两个概念:


纯右值和将亡值

        纯右值(Pure Rvalue)

  • 定义:纯右值通常指的是那些不与存储位置直接关联的表达式,例如临时对象、字面量、返回非引用类型的函数调用等。纯右值可以出现在需要移动或复制操作的语境中。
  • 特点:纯右值的一个重要特征是它们没有命名,因此无法被访问者直接引用。它们通常用于初始化或赋值给其他对象。
  • 例子:当一个函数返回一个非引用类型的值时,这个返回值就是一个纯右值,直到它被使用之前。

 

        将亡值(Expiring Value)

  • 定义:将亡值是指那些即将不再使用的对象的表达式,通常是因为作用域即将结束或者对象即将被销毁。将亡值可以通过返回类型为右值引用的表达式来表示。
  • 特点:将亡值的关键特性是它们所引用的对象的生命周期即将结束,这意味着可以进行资源的有效转移而不需要考虑后续使用。
  • 例子:当一个对象的生命周期即将结束时,它的成员或数组元素可以被视为将亡值。

移动构造与移动赋值

        我们可以根据将亡值的特性,使用右值引用识别出将亡值,将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了

移动构造(Move Construction)

        移动构造是一种特殊的构造函数,它接收一个右值引用作为参数,用于从临时对象(右值)中“窃取”资源,而不是复制资源。这样,临时对象的资源可以被新创建的对象直接使用,避免了不必要的资源分配和释放。

        移动构造函数的形参不能是const,因为移动构造后原对象的状态需要被修改(例如,指针设为NULL),以表示资源已被转移。同时,移动构造函数通常还需要检查自我赋值的情况,以避免将对象自身作为输入进行移动赋值。

        移动构造的主要应用场景包括:

  • 在函数中返回临时对象时,可以通过移动构造函数避免不必要的拷贝操作。
  • 在容器中插入临时对象时,可以通过移动构造函数实现高效插入和删除操作。
  • 在进行资源管理时,通过移动构造函数可以从一个对象转移资源所有权,提高性能。

        如下为上面提到的string的移动构造:

		// 移动构造
		string(string && s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}

        我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了

移动赋值(Move Assignment)

        移动赋值是一种特殊的赋值运算符,它同样使用右值引用作为参数,用于将一个对象的资源转移到另一个已存在的对象中。移动赋值避免了深拷贝,使得资源可以直接从一个对象转移到另一个对象,提高了赋值操作的效率。

        实现移动赋值时,通常需要进行以下步骤:

  1. 检查自我赋值,确保不是将对象赋值给自己。
  2. 使用std::move将资源从其他对象移动到当前对象。
  3. 将其他对象中该资源的状态置为适当的默认状态,例如将指针设为NULL。
  4. 返回当前对象的引用。

        移动赋值的主要应用场景与移动构造类似,都是在于优化资源的转移和管理,特别是在处理临时对象或即将被销毁的对象时。

         如下为上面提到的string的移动赋值:

		// 移动赋值
		string& operator=(string && s)
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}

        他也避免了不必要的资源复制,从而提高程序的性能。

move

        std::move是一个函数模板,用于将左值转换为右值引用,从而触发移动语义。std::move的引入使得程序员可以显式地告诉编译器他们想要转移资源而不是复制它们。C++11后STL容器插入接口函数也增加了右值引用版本 ,如下是几个例子:

        但是需要注意的是:move接受一个左值作为参数,并返回该左值的右值引用,它通过返回右值引用,std::move告诉编译器可以将该对象视为临时对象,从而触发移动构造函数或移动赋值操作符。std::move只是转换了对象的类型,并没有实际执行任何资源的转移。实际的资源转移发生在移动构造函数或移动赋值操作符被调用时。

对于移动构造与移动赋值的一些注意事项

        针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

        如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

        如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

        如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

        使用default 强制生成默认函数的关键字。使用delete禁止生成默认函数的关键字。final用于限制类的继承和函数的重写。override用于显式地表明派生类的成员函数重写了基类中的同名虚函数。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person(Person&& p) = default;
private:
	bit::string _name;
	int _age;
};

//
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p) = delete;
private:
	bit::string _name;
	int _age;
};

//
class Base {
public:
    virtual void foo() {}
};

class Derived : public Base {
public:
    void foo() override {} // 显式地表明重写了基类中的虚函数
};

//
class Base {
public:
    virtual void foo() final {} // 声明为final,禁止派生类重写该函数
};

class Derived : public Base {
public:
    // 尝试重写基类的foo函数会导致编译错误
    // void foo() {} // 编译错误
};

万能引用与完美转发

        首先,我们来理解这两个概念:

  • 万能引用
    • 定义:通过使用模板参数T与引用符号&&结合形成的T&&被称为万能引用。它能够根据传入参数的不同,既可以作为左值引用也可以作为右值引用。
    • 应用场景:万能引用主要用于函数模板中,使得函数可以统一处理左值和右值引用类型的参数。
  • 完美转发
    • 定义:完美转发是指函数模板在传递参数时保持参数的原始类别(左值或右值)不变的能力。
    • 实现机制:通过结合万能引用、引用折叠以及std::static_cast来实现。

接下来,我们深入探讨这两个概念的重要性和实际应用:

        重要性

    • 完美转发确保了函数模板在调用其他函数时,能够将参数的左值或右值属性传递给被调用的函数,从而支持移动语义和避免不必要的拷贝。
    • 万能引用是实现完美转发的关键,因为它允许函数模板参数适应不同的引用类型。

       实际应用

    • 在编写泛型代码、库或者框架时,万能引用和完美转发可以帮助开发者设计出更加通用和高效的接口。
    • 例如,在实现泛型容器类或者智能指针时,完美转发可以确保元素在插入或移除时的资源管理是最优的。

        如下:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }


template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

        由于我们并没有使用完美转发,那么虽然我们是在万能引用下传入的值,但是由于右值被右值引用后属性是左值,因此会得到如下的结果:

        在使用了完美转发后:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }


template<typename T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));
}
int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

 


                         感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1540541.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一题 第三十期 洛谷 [USACO1.5] [IOI1994]数字三角形 Number Triangles

[USACO1.5] [IOI1994]数字三角形 Number Triangles 题目描述 观察下面的数字金字塔。 写一个程序来查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。 在上面的样例中&#xff0c;从 7 → 3 → 8 →…

深入解析实时数仓Doris:介绍、架构剖析、应用场景与数据划分细节

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! Doris是一款高性能、开源的实时分析数据仓库&#xff0c;旨在为用户提供毫秒级查询响应、高并发、高可用以及易于扩展的OLAP解决方…

基于“云”重构“百度云盘”

这一篇文章是和上一篇连着的哟&#xff01; # docker run -p 80:80 -d -v /data/owncloud/:/var/www/html owncloud 一、【安装完成】 二、【打开浏览器】 三、【回到这个熟悉的界面&#xff0c;掉。】 四、【上传文件】 试了可以看哇偶&#xff01;&#xff01;&#xff01…

生产计划数据模型,实现能源企业数字化高效管理

随着市场经济的快速发展&#xff0c;能源企业在经济发展中的地位也随之提高。但由于能源企业在生产计划经济管理上存在指标不平衡、市场观念落后和环保意识欠缺等问题&#xff0c;导致企业的经济效益降低。目前&#xff0c;提高企业的生产计划管理是改善能源企业现状最有利的途…

SQLiteC/C++接口详细介绍sqlite3_stmt类(六)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍sqlite3_stmt类&#xff08;五&#xff09; 下一篇&#xff1a; SQLiteC/C接口详细介绍sqlite3_stmt类&#xff08;七&#xff09; 17. sqlite3_clear_bindings函数 sqlite3_clear_bindings函…

JavaEE--小Demo--数据库建立

目录 实验准备 本次所要新建的文件 实验步骤 step1-demo.sql 1.在resources文件夹下新建demo.sql文件 2.打开此目录&#xff0c;并运行命令提示符 3.打开数据库mysql -uroot -p 4.创建数据库create database demo; 5.使用数据库use demo; 6.导入数据source demo.sql;…

【OpenGL手册-21】高级GLSL编程

一、说明 这章不会向你展示什么新的功能&#xff0c;也不会对你的场景的视觉效果有较大提升。但是&#xff0c;本文能够提供最深刻的GLSL表达&#xff0c;也深入探讨了一些GLSL有趣的知识&#xff0c;它们可能在将来能帮助你。基本来说有些不可不知的内容和功能在你去使用GLSL创…

MySQL面试题--开发(最全,涵盖SQL基础、架构、事务)

MySQL面试题--事务https://mp.csdn.net/mp_blog/creation/editor/136947072 MySQL面试题--MySQL内部技术架构https://blog.csdn.net/Timebro/article/details/136946046?spm1001.2014.3001.5501 MySQL面试题--最全面-索引https://blog.csdn.net/Timebro/article/details/136…

Git Commit 提交规范,变更日志、版本发布自动化和 Emoji 提交标准

前言 Git Commit 是开发的日常操作, 一个优秀的 Commit Message 不仅有助于他人 Review, 还可以有效的输出 CHANGELOG, 对项目的管理实际至关重要, 但是实际工作中却常常被大家忽略&#xff0c;希望通过本文&#xff0c;能够帮助大家规范 Git Commit&#xff0c;并且展示相关 …

波奇学Linux:网络套接字

domain:ipv4 还是ipv6 type:面向字节流还是... 虚拟机 云服务器禁止直接bind公网ip 服务器可以有多个ip&#xff0c;如果只绑定一个ip&#xff0c;只能收到来自一个ip的信息 任意地址绑定 关于port的问题 [0,1024]&#xff1a;系统内定的端口号&#xff0c;一般要用固定的应…

JetPack之LiveData

目录 一、LiveData简介1.1 LiveData是什么&#xff1f; 二、LiveData使用2.1 LiveData基础使用2.2 LiveData搭配Service模拟后台消息2.3 LiveData在组件中的数据传递 三、LiveData应用场景 一、LiveData简介 1.1 LiveData是什么&#xff1f; LiveData是一种可观察的数据存储器…

[数据结构初阶]二叉树

各位读者老爷好&#xff0c;鼠鼠我现在浅浅介绍一些关于二叉树的知识点&#xff0c;在各位老爷茶余饭后的闲暇时光不妨看看&#xff0c;鼠鼠很希望得到各位老爷的指正捏&#xff01; 开始介绍之前&#xff0c;给各位老爷看一张风景照&#xff0c;有读者老爷知道在哪里吗&#x…

02-MySQL数据库的基本使用与密码设置

一、服务端口 3306端口和33060端口&#xff0c;是我们启动数据库后开启的监听端口&#xff1b; 3306端口&#xff1a;是我们MySQL服务的监听端口&#xff0c;用来连接数据库使用&#xff1b; 33060端口&#xff1a;MySQL-shell服务的端口&#xff0c;MySQL-shell是MySQL架构集群…

基于霍夫检测(hough变换)的人眼瞳孔定位,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

Java设计模式 | 抽象工厂模式

抽象工厂模式 工厂方法模式中考虑的是一类产品的生产&#xff0c;如幼儿园只培养小朋友&#xff0c;鞋厂只生产鞋子。这些工厂只生产同种类产品&#xff0c;同种类产品称为同等级产品&#xff0c;即工厂方法模式只考虑生产同等级的产品&#xff0c;但是在现实生活中许多工厂都…

一文读懂代理与反向代理

反向代理与正向代理在代理服务器的角色和工作方式上有所不同&#xff0c;这两种代理方式主要取决于代理服务器代理的对象和处理方式。 区别&#xff1a; 正向代理&#xff1a; 正向代理是代理客户端的请求&#xff0c;客户端通过正向代理访问其他服务或资源。客户端知道自己正…

注解总结,Java中的注解,springboot中的注解

注解总结 1、Junit 开始执行的方法&#xff1a;初始化资源&#xff0c;执行完之后的方法&#xff1a;释放资源 测试方法&#xff0c;必须是&#xff1a;公有、非静态、无参无返回值的 在一个类中&#xff0c;可以定义多个测试方法&#xff0c;每个测试方法可以单独运行&#…

Qt/C++通用跨平台Onvif工具/支持海康大华宇视华为天地伟业等/云台控制/预置位管理/工程调试利器

一、前言 在安防视频监控行业&#xff0c;Onvif作为国际标准&#xff0c;几乎主要的厂商都支持&#xff0c;不仅包含了国内的厂商&#xff0c;也包括主要的国际厂商&#xff0c;由于有了这个标准的存在&#xff0c;使得不同设备不同安防平台之间&#xff0c;能够接入各个厂家的…

堆(数据结构)

堆的概念及结构 如果有一个关键码的集合K { &#xff0c; &#xff0c; &#xff0c;…&#xff0c; }&#xff0c;把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中&#xff0c;并满足&#xff1a; < 且 < ( > 且 > ) i 0&#xff0c;1&#xff…

Linux技巧|Awk 比较运算符

在处理一行文本中的数字或字符串值时&#xff0c;使用比较运算符过滤文本或字符串对于 Awk 命令来说非常方便。 在 Awk 系列的这一部分中&#xff0c;我们将了解如何使用比较运算符过滤文本或字符串。 比较运算符 Awk 中的比较运算符用于比较数字或字符串的值&#xff0c;它们包…