[算法与数据结构][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::vector的push_back函数原型:void push_back (const value_type& val); //如果没有const,vec.push_back(5)这样的代码就无法编译通过了。 //因为5是右值
右值引用:右值引用的标志是&&,可以指向右值,不可以指向左值。
int &&ref_a_right = 5; // okint 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)不会有性能提升!!!!
那么左值引用、右值引用本身是左值还是右值?
被声明出来的左值引用和右值引用都是左值,因为他们都是有地址的,也位于等号左边,这符合我们刚刚的定义。
右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。作为函数返回值的 && 是右值,直接声明出来的 && 是左值
左右值引用的区别:
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
- 作为函数形参时,右值引用更灵活。虽然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_back和emplace_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++中,是否可以移动一个对象,取决于该对象的类是否定义了移动构造函数或移动赋值运算符。 以下是一些判断一个对象是否可以被移动的方法:
检查类的定义:如果一个类定义了移动构造函数或移动赋值运算符,那么这个类的对象就可以被移动。移动构造函数和移动赋值运算符的声明通常如下:
class MyClass { public:MyClass(MyClass&& other); // 移动构造函数MyClass& operator=(MyClass&& other); // 移动赋值运算符// ... };注意,这两个函数的参数都是右值引用。
使用
std::is_move_constructible和std::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使用
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); // 传统方法,copyvec.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_backvoid 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
相关文章:
[算法与数据结构][c++]:左值、右值、左值引用、右值引用和std::move()
左值、右值、左值引用、右值引用和std::move 1. 什么是左值、右值2. 什么是左值引用、右值引用3. **右值引用和std::move的应用场景**3.1 实现移动语义3.2 **实例:vector::push_back使用std::move提高性能** **4. 完美转发 std::forward**5. Reference 写在前面&…...
【QT】day3
1.登陆界面 2.登陆失败 3.登陆成功弹窗 4.点击OK后跳转 #include "mainwindow.h" #include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this); }MainWindow::~MainWindow…...
c++ fork, execl 参数 logcat | grep
Linux进程编程(PS: exec族函数、system、popen函数)_linux popen函数会新建进程吗-CSDN博客 execvp函数详解_如何在C / C 中使用execvp()函数-CSDN博客 C语言的多进程fork()、函数exec*()、system()与popen()函数_c语言 多进程-CSDN博客 Linux---fork…...
QT:单例
单例的定义 官方定义:单例是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。 单例的写法 抓住3点: 构造函数私有化(确保只有一个实例)提供一个可以获取构造实例的接口(提供唯一的实…...
IPv6路由协议---IPv6动态路由(OSPFv3-4)
OSPFv3的链路状态通告LSA类型 链路状态通告是OSPFv3进行路由计算的关键依据,链路状态通告包含链路状态类型、链路状态ID、通告路由器三元组唯一地标识了一个LSA。 OSPFv3的LSA头仍然保持20字节,但是内容变化了。在LSA头中,OSPFv2的LS age、Advertising Router、LS Sequence…...
移动通信原理与关键技术学习(4)
1.小尺度衰落 Small-Scale Fading 由于收到的信号是由通过不同的多径到达的信号的总和,接收信号的增强有一定的减小。 小尺度衰落的特点: 信号强度在很小的传播距离或时间间隔内的快速变化;不同多径信号多普勒频移引起的随机调频ÿ…...
第二百五十八回
文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"模拟对话窗口的页面"相关的内容,本章回中将介绍如何创建一个可以输入内容的对话框.闲话休提,让我们一起Talk Flutter吧。 1. 概念…...
freesurfer-reconall后批量提取TIV(颅内总体积)
#提取TIV #singleline=$(grep Estimated Total Intracranial Volume /usr/local/freesurfer/subjects/bect-3d+bold-wangjingchen-4.9y-2/stats/aseg.sta...
【GO】如何用 Golang 的 os/exec 执行 pipe 替换文件
背景 主要记录一下怎么用 Golang 的 os/exec 去执行一个 cmd 的 pipeline,就是拿 cmdA 的输出作为 cmdB 的输入,这里记录了两种方法去替换文件里面的字符串。 pipe 那个逻辑在 demo1 里。 另外一种是直接读文件做替换,一不小心两个都放进来了…...
基于Spring-boot-websocket的聊天应用开发总结
目录 1.概述 1.1 Websocket 1.2 STOMP 1.3 源码 2.Springboot集成WS 2.1 添加依赖 2.2 ws配置 2.2.1 WebSocketMessageBrokerConfigurer 2.2.2 ChatController 2.2.3 ChatInRoomController 2.2.4 ChatToUserController 2.3 前端聊天配置 2.3.1 index.html和main.j…...
2023年度总结 - 职业生涯第一个十年
2023年只剩下最后一周,又到了一年一度该做年末总结的时候了。 回想起去年,还有人专门建立了一个关于年度总结文章汇总的仓库。读了很多篇别人写的,给了我很多的触动和感想。这里的每篇文章都是关于某个人这一整年的生活和工作的轨迹啊。即使你…...
setup 语法糖
只有vue3.2以上版本可以使用 优点: 更少的样板内容,更简洁的代码 能够使用纯 Typescript 声明props 和抛出事件 更好的运行时性能 更好的IDE类型推断性能 在sciprt标识上加上setup 顶层绑定都可以使用 不需要return ,可以直接使用 使用组件…...
Javaweb之Mybatis的基础操作的详细解析
1. Mybatis基础操作 学习完mybatis入门后,我们继续学习mybatis基础操作。 1.1 需求 需求说明 通过分析以上的页面原型和需求,我们确定了功能列表: 查询 根据主键ID查询 条件查询 新增 更新 删除 根据主键ID删除 根据主键ID批量删除 …...
知名开发者社区Stack Overflow发布《2023 年开发者调查报告》
Stack Overflow成立于2008年,最知名的是它的公共问答平台,每月有超过 1 亿人访问该平台来提问、学习和分享技术知识。是世界上最受欢迎的开发者社区之一。每年都会发布一份关于开发者的调查报告,来了解不断变化的开发人员现状、正在兴起或衰落…...
vue element plus Form 表单
表单包含 输入框, 单选框, 下拉选择, 多选框 等用户输入的组件。 使用表单,您可以收集、验证和提交数据。 TIP Form 组件已经从 2. x 的 Float 布局升级为 Flex 布局。 典型表单# 最基础的表单包括各种输入表单项,比如input、select、radio、checkbo…...
zmq_connect和zmq_poll
文章内容: 介绍函数zmq_connect和zmq_poll的使用 zmq_connect zmq_connect函数是ZeroMQ库中的一个函数,用于在C语言中创建一个与指定地址的ZeroMQ套接字的连接。该函数的原型如下: int zmq_connect(void *socket, const char *endpoint);其…...
TinyLog iOS v3.0接入文档
1.背景 为在线教育部提供高效、安全、易用的日志组件。 2.功能介绍 2.1 日志格式化 目前输出的日志格式如下: 日志级别/[YYYY-MM-DD HH:MM:SS MS] TinyLog-Tag: |线程| 代码文件名:行数|函数名|日志输出内容触发flush到文件的时机: 每15分钟定时触发…...
react-native 配置@符号绝对路径配置和绝对路径没有提示的问题
这里需要用到vscode的包 yarn add babel-plugin-module-resolver 找到根目录里的babel.config.js 在页面添加plugins配置 直接替换 module.exports {presets: [module:metro-react-native-babel-preset],plugins: [[module-resolver,{root: [./src],alias: {/utils: ./src/…...
element的Table表格组件树形数据与懒加载简单使用
目录 1. 代码实现2. 效果图3. 解决新增、删除、修改之后树节点不刷新问题。([参考文章](https://blog.csdn.net/weixin_41549971/article/details/135504471)) 1. 代码实现 <template><div><!-- lazy 是否懒加载子节点数据 --><!-…...
游戏、设计选什么内存条?光威龙武系列DDR5量大管饱
如果你是一位PC玩家或者创作者,日常工作娱乐中,确实少不了大容量高频内存的支持,这样可以获得更高的工作效率,光威龙武系列DDR5内存条无疑是理想之选。它可以为计算机提供强劲的性能表现和稳定的运行体验,让我们畅玩游…...
告别ZooKeeper!ClickHouse Keeper双机集群搭建全攻略(含常见报错解决方案)
ClickHouse Keeper双机集群实战指南:从零搭建到故障排查 1. 为什么选择ClickHouse Keeper替代ZooKeeper 在ClickHouse集群架构中,协调服务一直扮演着关键角色。传统方案依赖ZooKeeper实现分布式协调,但这种方式存在几个明显痛点: …...
OpenAI Triton项目中的相关技术对比:多面体编译与调度语言
OpenAI Triton项目中的相关技术对比:多面体编译与调度语言 【免费下载链接】triton Development repository for the Triton language and compiler 项目地址: https://gitcode.com/GitHub_Trending/tri/triton 引言 在深度学习编译器领域,OpenA…...
Java AI开发避坑!
文章目录一、当"龙虾"突然发狂二、解剖这场"史诗级翻车"第一刀:插件生态大迁徙第二刀:API 接口一锅端第三刀:安全沙箱锁死第四刀:目录结构洗牌三、Java 开发者的至暗时刻WebSocket 连接闪断MCP 适配器失效技能…...
告别Win11无边框窗口的‘残疾’体验:Qt自定义标题栏完美集成Snap Layout保姆级教程
现代Qt应用开发:Win11无边框窗口与Snap Layout深度整合实战 当微软推出Windows 11时,其标志性的Snap Layout功能彻底改变了多窗口管理体验。然而对于使用Qt框架开发无边框窗口应用的开发者来说,这却带来了一个棘手的问题——自定义标题栏与系…...
杰理之人声消除额外保留部分频率声音办法【篇】
将原始声音分为两份,一份走原先的人声消除,另一份走EQ调节 最后输出声音 原先人声消除效果(左-右) EQ调节后声音...
Qwen3.5-4B-Claude-Opus保姆级教程:Web界面响应延迟归因与优化路径
Qwen3.5-4B-Claude-Opus保姆级教程:Web界面响应延迟归因与优化路径 1. 模型与部署环境概览 Qwen3.5-4B-Claude-4.6-Opus-Reasoning-Distilled-GGUF是基于Qwen3.5-4B的推理蒸馏模型,特别强化了结构化分析、分步骤回答以及代码与逻辑类问题的处理能力。该…...
别再乱选了!Ansys EDA桌面版导入IBIS模型,Pin Import和Buffer Import到底怎么用?
Ansys EDA桌面版IBIS模型导入指南:Pin Import与Buffer Import深度解析 在信号完整性(SI)和电源完整性(PI)仿真领域,IBIS模型的使用一直是工程师们关注的焦点。作为行业标准的Ansys EDA工具链(原E-desktop)提供了强大的SIPI仿真能…...
如何用掩码生成蒸馏(MGD)提升小模型性能?实战ResNet-18到ImageNet分类
掩码生成蒸馏实战:如何让ResNet-18在ImageNet上提升1.8%准确率 在模型轻量化的浪潮中,知识蒸馏技术正经历着从简单模仿到特征重构的范式转变。当我们用ResNet-50这样的"大模型"指导ResNet-18等"小模型"训练时,传统方法往…...
Souliss嵌入式状态同步框架:轻量级去中心化智能家居通信实践
1. Souliss 智能家居网络框架深度解析:面向嵌入式工程师的底层通信架构实践指南Souliss 是一个专为资源受限嵌入式节点设计的轻量级、去中心化智能家居网络框架。其核心目标并非构建通用物联网平台,而是解决真实家庭场景中多协议共存、低功耗节点协同、边…...
深度学习迁移学习:从原理到实践
深度学习迁移学习:从原理到实践 1. 背景与动机 深度学习模型在各种任务上取得了显著的性能提升,但这些模型通常需要大量的标注数据和计算资源进行训练。在实际应用中,我们经常面临以下挑战: 数据不足:某些任务的标注数…...
