[开发语言][c++]:左值、右值、左值引用、右值引用和std::move()

左值、右值、左值引用、右值引用和std::move

    • 1. 什么是左值、右值
    • 2. 什么是左值引用、右值引用
    • 3. **右值引用和std::move的应用场景**
      • 3.1 实现移动语义
      • 3.2 **实例:vector::push_back使用std::move提高性能**
    • **4. 完美转发 std::forward**
    • 5. Reference

写在前面: 如果你也被左值、右值、左值引用、右值引用和std::move搞得焦头烂额,相关概念和理解不够深入,或者认识模棱两可,那么这篇文章将非常的适合你,耐心阅读,相信一定会有所收获~~


1. 什么是左值、右值

左值: 可以取地址、位于等号左边 – 表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)

右值: 没法取地址、位于等号右边 – 表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式)

便携方法:对表达式取地址,如果能,则为左值,否则为右值

int val;
val = 4; // 正确 ①
4 = val; // 错误 ②

上述例子中,由于在之前已经对变量val进行了定义,故在栈上会给val分配内存地址,运算符=要求等号左边是可修改的左值,4是临时参与运算的值,一般在寄存器上暂存,运算结束后在寄存器上移除该值,故①是对的,②是错的

2. 什么是左值引用、右值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝,其实现原理和指针类似。

左值引用:指向左值的引用,称为左值引用

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值

那么const左值引用可不可以指向右值呢?

​ 可以!!!

const int &ref_a = 5;

const左值引用不会修改指向值,因此可以指向左值和右值,这也是为什么要使用const &作为函数参数的原因之一,如std::vectorpush_back函数原型:

void push_back (const value_type& val); 
//如果没有const,vec.push_back(5)这样的代码就无法编译通过了。
//因为5是右值

右值引用:右值引用的标志是&&,可以指向右值,不可以指向左值。

int &&ref_a_right = 5; // ok
 
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
 
ref_a_right = 6; // 右值引用的用途:可以修改右值

自然而然就会出现这样一个问题:右值引用有办法指向左值吗?右值引用有啥作用?

有办法,std::move()

int a = 5; // left value
int &ref_a_l = a; // left reference.
int &&ref_a_r = std::move(a); //rvalue reference.
std::cout << ref_a_r << std::endl;

左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5.

std::move是一个非常有迷惑性的函数,不理解左右值概念的人往往以为它能把一个变量里的内容移动到另一个变量,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)。 所以,单纯的std::move(xxx)不会有性能提升!!!!

那么左值引用、右值引用本身是左值还是右值?

被声明出来的左值引用和右值引用都是左值,因为他们都是有地址的,也位于等号左边,这符合我们刚刚的定义。

右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值作为函数返回值的 && 是右值,直接声明出来的 && 是左值

左右值引用的区别

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

3. 右值引用和std::move的应用场景

按上文分析,std::move只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵活性,看上去还是挺鸡肋的。他们有什么实际应用场景吗?

3.1 实现移动语义

在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。 在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数拷贝构造函数赋值运算符重载析构函数等。

class Array {
public:
    Array(int size) : size_(size) {
        data_ = new int[size_];
    }
     
    // 深拷贝构造->当代码中有指针开辟堆内存时,
    // 		必须显式定义拷贝构造函数,开辟新的堆内存,存储拷贝后的指针数据,
    // 		否则两个对象的指针会指向同一个堆内存地址,当某一个对象析构后,
    // 		相应的堆内存就会释放掉,导致另一个对象内的指针成为悬浮指针!!!
  	// 浅拷贝->不涉及指针的拷贝
    Array(const Array& temp_array) {
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
     
    // 深拷贝赋值 const引用避免了传参拷贝,但是堆内存仍然需要深拷贝,所以需要用到std::move实现移动赋值
    Array& operator=(const Array& temp_array) {
        delete[] data_;
 
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
 
    ~Array() {
        delete[] data_;
    }
 
public:
    int *data_;
    int size_;
};

该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免 深拷贝 了,

关于深拷贝和浅拷贝的区别和联系,后续也会出一篇文章,链接在:深拷贝与浅拷贝
深拷贝构造->当代码中有指针开辟堆内存时,必须显式定义拷贝构造函数,开辟新的堆内存,存储拷贝后的指针数据,否则两个对象的指针会指向同一个堆内存地址,当某一个对象析构后,相应的堆内存就会释放掉,导致另一个对象内的指针成为悬浮指针!!!
浅拷贝->不涉及指针的拷贝

如:

class Array {
public:
    Array(int size) : size_(size) {
        data_ = new int[size_];
    }
     
    // 深拷贝构造
    Array(const Array& temp_array) {
        ...
    }
     
    // 深拷贝赋值
    Array& operator=(const Array& temp_array) {
        ...
    }
 
    // 移动构造函数(重载深拷贝构造函数),可以浅拷贝-> 形参是const& 构造函数内,对temp_array赋值,编译不通过~
    Array(const Array& temp_array, bool move) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
    ~Array() {
        delete [] data_;
    }
 
public:
    int *data_;
    int size_;
};

这么做有2个问题:

  • 不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。-> 重载拷贝构造函数
  • 无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。当然函数参数可以改成非const:Array(Array& temp_array, bool move){...},这样也有问题,由于左值引用不能接右值,Array a = Array(Array(), true);这种调用方式就没法用了。

可以发现左值引用真是用的很不爽右值引用的出现解决了这个问题,在STL的很多容器中,都实现了以 右值引用为参数移动构造函数移动赋值重载函数,或者其他函数,最常见的如std::vector的push_backemplace_back。参数为左值引用意味着拷贝,为右值引用意味着移动。

class Array {
public:
    ......
 
    // 优雅
    Array(Array&& temp_array) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
public:
    int *data_;
    int size_;
};

如何判断一个对象是否是可以移动的?

在C++中,是否可以移动一个对象,取决于该对象的类是否定义了移动构造函数或移动赋值运算符。 以下是一些判断一个对象是否可以被移动的方法:

  1. 检查类的定义:如果一个类定义了移动构造函数或移动赋值运算符,那么这个类的对象就可以被移动。移动构造函数和移动赋值运算符的声明通常如下:

    class MyClass {
    public:
        MyClass(MyClass&& other); // 移动构造函数
        MyClass& operator=(MyClass&& other); // 移动赋值运算符
        // ...
    };
    

    注意,这两个函数的参数都是右值引用。

  2. 使用std::is_move_constructiblestd::is_move_assignable:这两个模板在<type_traits>头文件中定义,可以用来检查一个类型是否有可用的移动构造函数或移动赋值运算符:

    std::cout << std::is_move_constructible<MyClass>::value << std::endl; // 如果MyClass可移动,输出1,否则输出0
    std::cout << std::is_move_assignable<MyClass>::value << std::endl; // 如果MyClass可移动赋值,输出1,否则输出0
    
  3. 使用std::move_if_noexcept:这个模板函数可以用来判断是否可以无异常地移动一个对象。如果移动操作可能抛出异常,它会选择拷贝操作。这在一些容器操作中非常有用,例如std::vector的重新分配。

需要注意的是,即使一个对象可以被移动,也不意味着应该总是移动它。在某些情况下,例如当你知道一个对象将在移动操作后立即被销毁,或者你想避免昂贵的拷贝操作时,移动是有意义的。在其他情况下,移动可能会导致难以追踪的错误,例如,如果你错误地移动了一个仍然需要使用的对象。

3.2 实例:vector::push_back使用std::move提高性能

// std::move会调用到移动语义函数,避免了深拷贝。
int main() {
    std::string str1 = "aacasxs";
    std::vector<std::string> vec;
     
    vec.push_back(str1); // 传统方法,copy
    vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
    vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
    vec.emplace_back("axcsddcas"); // 当然可以直接接右值
}
 
// std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val); // 内部调用了emplace_back
 
void emplace_back (Args&&... args);

可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

moveable_objecta = moveable_objectb; 
改为: 
moveable_objecta = std::move(moveable_objectb);

还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):

std::unique_ptr<A> ptr_a = std::make_unique<A>();

std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移动赋值重载函数‘,参数是&& ,只能接右值,因此必须用std::move转换类型

std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过

std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。

4. 完美转发 std::forward

std::move一样,它的兄弟std::forward也充满了迷惑性,虽然名字含义是转发,但他并不会做转发,同样也是做类型转换.

与move相比,forward更强大,move只能转出来右值,forward都可以。

std::forward(u)有两个参数:T与 u。 a. 当T为左值引用类型时,u将被转换为T类型的左值; b. 否则u将被转换为T类型右值。

举个例子,有main,A,B三个函数,调用关系为:main->A->B,建议先看懂2.3节对左右值引用本身是左值还是右值的讨论再看这里:

void B(int&& ref_r) {
    ref_r = 1;
}
 
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
    B(ref_r);  // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    B(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
 
int main() {
    int a = 5;
    A(std::move(a));
}

例2:

void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
     
    change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
    change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
    // 可见,forward可以把值转换为左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}

上边的示例在日常编程中基本不会用到,std::forward最主要运于模版编程的参数转发中,想深入了解需要学习万能引用(T &&)引用折叠(eg:& && → ?)等知识,本文就不详细介绍这些了。

5. Reference

https://zhuanlan.zhihu.com/p/374392832

https://zhuanlan.zhihu.com/p/335994370

https://www.cnblogs.com/shadow-lr/p/Introduce_Std-move.html

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CRM系统针对销售管理有哪些功能?如何帮助销售效率增长?

从长远来看&#xff0c;有效的CRM管理系统可以帮助您的企业达到甚至超过收入目标。现代大多数企业都依靠CRM系统来管理其销售周期并增加收入。但是&#xff0c;当大多数人提到CRM时&#xff0c;他们指的是使能够改善业务关系并轻松管理不断团队的软件或工具。合格的CRM系统能够…

Bug小能手系列(python)_14: pd.concat得到的矩阵错误

pd.concat得到的不是自己想要的矩阵 0 引言1 错误原因2 解决思路3 具体代码4 总结 0 引言 今天在运行pd.concat (pd指的是pandas库)&#xff0c;需要将两个DataFrame数据&#xff08;数据分别为5*4的矩阵&#xff09;进行列合并时&#xff0c;突然发现得到的矩阵是10*8的&…

PyCharm社区版如何创建Django项目并运行

一、配置Django环境 1、使用PyCharm打开一个普通的Python项目 2、为该项目配置Django环境 &#xff08;1&#xff09;点击"File"-"Settings" &#xff08;2&#xff09;点击"Project:项目名"-"Python Interpreter"-"号" &…

为什么杭州的独角兽公司的技术专家都是阿里巴巴出来的?

在浙江杭州有一个不成为的规定&#xff0c;独角兽公司招聘技术人才的时候&#xff0c;尤其是阿里巴巴出来的技术专家&#xff0c;面试官都是争先恐后的面试&#xff0c;总是想第一时间把这个人拿到手&#xff0c;当然前提是这个技术专家不是水货。 猎头推荐人才的时候&#xf…

trilium笔记私有化部署

前言&#xff1a;一直用有道云笔记&#xff0c;随着笔记越多&#xff0c;每次搜索跟打开都感觉没那么顺畅。再考虑到数据安全问题&#xff0c;想着有没私有化部署的笔记&#xff0c;于是开始探索私有化部署笔记。 为知笔记&#xff1a;比较早之前&#xff0c;家里的NAS部署过为…

2023.12.10查找,线性探测法

二叉树的重构 集合实现对图的dfs,bfs复写 插入排序 霍夫曼树&#xff0c;霍夫曼编码 查找成功&#xff0c;查找失败的期望值计算 9.给定散列表大小为11&#xff0c;散列函数为H(Key)Key%11。按照线性探测冲突解决策略连续插入散列值相同的4个元素。问&#xff1a;此时该散…

ChatGpt使用技巧

通用类技巧 角色扮演 比如让ChatGpt扮演500强营销专家 告诉ChatGpt你的身份。初学者、或是有一定能力、知识的学习者等 限制ChatGpt回答长度 100~200字之间 让ChatGpt一步一步思考 他会预测下一个单词&#xff0c;根据prompt进行生成 明确你的要求和目的 说清楚问题&#x…

ES6(一部分)未完...

文章目录 ES61.ES6 let声明变量2.ES6 const声明常量3.变量解构赋值3-1解构对象3-2解构数组3-3字符串解构 4.模板字符串5.字符串扩展5-1 include函数5-2 repeat函数&#xff08;重复&#xff09; 6.数值扩展6-1二进制和八进制表示法6-2isFinite 与 isNaN方法6-3islnteger方法6-4…

记录汇川:H5U与Fctory IO测试9

主程序&#xff1a; 子程序&#xff1a; IO映射 子程序&#xff1a; 出料程序 子程序&#xff1a; 自动程序 Fctory IO配置&#xff1a; 实际动作如下&#xff1a; Fctory IO测试9

编码器与解码器LLM全解析:掌握NLP核心技术的关键!

让我们深入了解&#xff1a;基于编码器和基于解码器的模型有什么区别&#xff1f; 编码器与解码器风格的Transformer 从根本上说&#xff0c;编码器和解码器风格的架构都使用相同的自注意力层来编码词汇标记。然而&#xff0c;主要区别在于编码器旨在学习可以用于各种预测建模…

【AI视野·今日NLP 自然语言处理论文速览 第七十四期】Wed, 10 Jan 2024

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 10 Jan 2024 Totally 38 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Model Editing Can Hurt General Abilities of Large Language Models Authors Jia Chen Gu, Hao Xiang Xu, J…

全网首发!Yolov8_obb旋转框检测(DOTA1.0数据集)

一、YOLOv8环境搭建 &#xff08;1&#xff09;Pytorch的安装 如果你的环境没有部署请参考本人文章&#xff1a;NLP笔记&#xff08;2&#xff09;——PyTorch的详细安装_安装torchnlp-CSDN博客 &#xff08;2&#xff09;下载最新的Yolov8-obb代码&#xff1a; https://git…

如何使用PR制作抖音视频?抖音短视频创作素材剪辑模板PR项目工程文件

如何使用PR软件制作抖音视频作品&#xff1f;Premiere Pro 抖音短视频创作素材剪辑模板PR项目工程文件。 3种分辨率&#xff1a;10801920、10801350、10801080。 来自PR模板网&#xff1a;https://prmuban.com/37058.html

5分钟了解股票交易!上海股票开户交易佣金最低是多少?怎么开户费用最低?

股票交易是指通过证券市场买卖股票的活动。以下是股票交易的基本步骤&#xff1a; 开立证券账户&#xff1a;首先需要选择一家证券公司&#xff0c;向其提交相关材料开立证券账户&#xff0c;并完成账户开立手续。 研究和选择股票&#xff1a;在决定购买股票之前&#xff0c;建…

【hyperledger-fabric】部署Java应用远程访问智能合约

简介 首先是根据b站的视频 hyperledger-fabric【3】在 java 应用中访问合约 以及hyperledger-fabric【5】Java应用和私有数据&#xff0c;本文章主要讲述的是视频中我遇到的问题&#xff0c;以及相关知识点的总结。 遇到的问题 问题1&#xff1a;git clone下载下来的代码发现…

Halcon实例:提取图像的纹理特征

Halcon实例&#xff1a;提取图像的纹理特征 举例说明&#xff0c;输入的是一幅灰度图像&#xff0c;分别选取其中两个矩形区域的灰度图像&#xff0c;分析其灰度变化。首先选取灰度变化较为明显的矩形1&#xff0c;然后选取灰度变化比较平滑的矩形2&#xff0c;生成灰度共生矩…

SCA面面观 | 如何生成一份软件物料清单SBOM?

由于网络安全挑战和不断变化的威胁环境&#xff0c;使得软件供应链安全成为了一个重要议题。特别是近年来&#xff0c;软件供应链的复杂性和全球化程度的提升&#xff0c;第三方软件的安全性和可追溯性变得越来越重要。 为了应对这一挑战&#xff0c;从美国政府开始&#xff0c…

【算法Hot100系列】外观数列

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

2023年北邮渣硕的暑期秋招总结

背景 实验室一般是在研究生二年级的时候会放实习&#xff0c;在以后的日子就是自己完成毕业工作要求&#xff0c;基本上不再涉及实验室的活了&#xff0c;目前是一月份也是开始准备暑期实习的好时间。实验室每年这个时候都会有学长学姐组织暑期实习经验分享&#xff0c;本着不…

【抓包教程】BurpSuite联动雷电模拟器——安卓高版本抓包移动应用教程

前言 近期找到了最适合自己的高版本安卓版本移动应用抓HTTP协议数据包教程&#xff0c;解决了安卓低版本的问题&#xff0c;同时用最简单的办法抓到https的数据包&#xff0c;特此进行文字记录和视频记录。 前期准备 抓包工具&#xff1a;BurpSuite安卓模拟器&#xff1a;雷…