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

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

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

                                                                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.mzph.cn/news/766815.shtml

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

相关文章

MySQL表内容的增删查改

在前面几章的内容中我们学习了数据库的增删查改&#xff0c;表的增删查改&#xff0c;这一篇我们来学习一下对表中的内容做增删查改。 CRUD : Create(创建), Retrieve(读取)&#xff0c;Update(更新)&#xff0c;Delete&#xff08;删除&#xff09; 1.创建Create 我们先创建…

Zabbix Web界面中文汉化

要想达到上图的效果&#xff0c;第一步先查看 /usr/share/zabbix/assets/fonts/ [rootservice yum.repos.d]# ll /usr/share/zabbix/assets/fonts/ 总用量 0 lrwxrwxrwx. 1 root root 33 3月 23 16:58 graphfont.ttf -> /etc/alternatives/zabbix-web-font 继续查看graph…

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

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

vue实现饼图渲染的步骤

1) 创建一个DOM对象,有自定义的高和宽. 2) 引入Echarts软件包并导入到对应文件内 npm i Echarts import 文件.js script src.../文件 3) 初始化一个对象 4) 对象的方法实现饼图渲染 data内的数据,且当一个对象已经渲染一遍,再执行这个,会对setOption的参数进行更新,其…

Linux的学习之路:2、基础指令(1)

一、ls指令 上篇文章已经说了一点点的ls指令&#xff0c;不过那还是不够的&#xff0c;这篇文章会介绍更多的指令&#xff0c;最起码能使用命令行进行一些简单的操作&#xff0c;下面开始介绍了 ls常用选项 -a 列出目录下的所有文件&#xff0c;包括以 . 开头的隐含文件。 -d…

02课程发布模块之部署Nginx

部署Nginx 部署网关 通过Nginx访问后台网关&#xff0c;然后由网关再将请求转发到具体的微服务,网关会把请求转发到具体的服务 upstream gatewayserver{server 127.0.0.1:63010 weight10; } # 网站首页对应的虚拟机 server {listen 80;server_name www.51xuecheng.cn…

Yoast插件:您的WordPress网站SEO优化利器

在之前的文章中我们介绍了如何低成本使用WordPress来搭建个人网站&#xff0c;相信很多朋友都希望自己的网站能够被搜索引擎所收录&#xff0c;并获得更高的排名&#xff0c;从而吸引更多的流量和用户。如果是的话&#xff0c;您需要了解SEO&#xff08;搜索引擎优化&#xff0…

使用git+ssh访问github,避免下载资源失败

一、创建github账户之后&#xff0c;记住注册邮箱和账户名 我的邮箱&#xff1a;yuanyan23mails.ucas.ac.cn 账户名&#xff1a;thekingofjumpshoot 下边的相关位置需要用自己的邮箱和用户名替代 二、输入本地生成秘钥和公钥命令&#xff0c;并且生成公私钥对 ssh-keygen …

初识进程的地址空间、页表

一、&#x1f31f;问题引入 &#x1f6a9;代码一&#xff1a; #include<stdio.h>#include<unistd.h>int g_val100;int main(){pid_t idfork();if(id0){//子进程while(1){printf("I am a child pid:%d ppid:%d g_val:%d\n",getpid(),getppid(),g_val);…

AOF 持久化是怎么实现的?

资料来源 : 小林coding 小林官方网站 : 小林coding (xiaolincoding.com) AOF 日志 试想一下&#xff0c;如果 Redis 每执行一条写操作命令&#xff0c;就把该命令以追加的方式写入到一个文件里&#xff0c;然后重启 Redis 的时候&#xff0c;先去读取这个文件里的命令&#xf…

使用Intellij idea编写Spark应用程序(Scala+Maven)

使用Intellij idea编写Spark应用程序(ScalaMaven) 对Scala代码进行打包编译时&#xff0c;可以采用Maven&#xff0c;也可以采用sbt&#xff0c;相对而言&#xff0c;业界更多使用sbt。这里介绍IntelliJ IDEA和Maven的组合使用方法。IntelliJ IDEA和SBT的组合使用方法&#xf…

FANUC机器人零点标定的基本步骤(出厂数据)

FANUC机器人零点标定的基本步骤(出厂数据) FANUC 零点数据存在问题的机器人通常会出现以下几种报警: (1)SRVO-062报警 - 脉冲编码器数据丢失,机器人完全不能动,具体消除方法可参考以下链接中的内容: FANUC机器人SRVO-062报警原因分析及处理对策 (2)SRVO-075报警 -…

自己动手做一个批量doc转换为docx文件的小工具

前言 最近遇到了一个需求&#xff0c;就是要把大量的doc格式文件转换为docx文件&#xff0c;因此就动手做了一个批量转换的小工具。 背景 doc文件是什么&#xff1f; “doc” 文件是一种常见的文件格式&#xff0c;通常用于存储文本文档。它是 Microsoft Word 文档的文件扩…

探索 Flutter 中的 NavigationRail:使用详解

1. 介绍 在 Flutter 中&#xff0c;NavigationRail 是一个垂直的导航栏组件&#xff0c;用于在应用程序中提供导航功能。它通常用于更大屏幕空间的设备&#xff0c;如平板电脑和桌面应用程序。NavigationRail 提供了一种直观的方式来浏览应用程序的不同部分&#xff0c;并允许…

PyStructureFactor:隧道电离率中分子结构因子的 Python 代码

PyStructureFactor&#xff1a;隧道电离率中分子结构因子的 Python 代码 隧道电离是强场和阿秒科学的核心。在本文中&#xff0c;我们提出了PyStructureFactor——一个通用的Python代码&#xff0c;用于计算强激光场下常见分子的隧道电离率的结构因子。数值实现基于积分表示…

004——内存映射(基于鸿蒙和I.MAX6ULL)

目录 一、 ARM架构内存映射模型 1.1 页表项 1.2 一级页表映射过程 1.3 二级页表映射过程 1.4 cache 和 buffer 二、 鸿蒙内存映射代码学习 三、 为板子编写内存映射代码 3.1 内存地址范围 3.2 设备地址范围 一、 ARM架构内存映射模型 &#xff08;以前我以为页表机制…

使用插件将swagger文档转html或pdf

github上有maven开源插件swagger2markup将swagger文档转为.adoc格式的文档&#xff0c;另外一个maven开源插件asciidoctorj-pdf则可以将.adoc格式的文档转为html和pdf。由于GitHub访问不稳定&#xff0c;在gitee上有镜像项目。所以我就贴gitee上的项目地址了。 实现从swagger文…

局域网内的手机、平板、电脑的文件共享

在日常工作生活中&#xff0c;经常需要将文件在手机、平板、电脑间传输&#xff0c;以下介绍三种较为便捷的方法&#xff1a; 1.LocalSend 该软件是免费开源的&#xff0c;可以在局域网内的任意手机、平板、电脑间传递文件&#xff0c;并且任意一方都可以作为“发送方”和“接…

MapReduce学习问题记录

1、如何跳过对某行数据的处理 第一行数据是字段名不需要处理&#xff0c;我们知道第一行偏移量是0&#xff08;行记录的时候是从数组首地址开始&#xff0c;到了行标识符进行一次计数&#xff0c;这个计数就是行偏移量&#xff0c;从0开始&#xff09;&#xff0c;我们根据偏移…

线程池的7大参数

线程池的7大参数 一、 corePoolSize 线程池核心线程大小 核心线程永远不会销毁&#xff0c;即使他们处于空闲状态&#xff0c;除非设置了allowCoreThreadTimeOut。任务提交到线程池后&#xff0c;首先会检查当前线程数是否达到了corePoolSize&#xff0c;如果没有达到的话&…