C++:右值引用
右值与左值
在讲解右值引用之前,我们就需要先辨析一下左值
与右值
的区别。
左值
左值是一个表示数据的表达式,我们可以获取它的地址并且对其赋值,左值可以出现在赋值操作符=
的左边,但是右值不能。
int i = 0;
int* p = &i;
double d = 3.14;
变量i
,p
,d
都是左值,一方面来说,它们出现在了=
的左边,另一方面来说,我们可以对其取地址,并修改它的值。
const int ci = 0;
int const* cp = &i;
const double cd = 3.14;
变量ci
,cp
,cd
都是左值,它们出现在了=
的左边,我们可以对其取地址。但是由于具有const
属性,我们不能修改它。
因此, 左值
最显著的特征是可以取地址,但是不一定可以被修改。
右值
右值也是一个表达数据的表达式,比如字面常量
,表达式返回值
,函数返回值
等等,右值可以出现在赋值操作符=
的右边,但是不能出现在=
的左边,右值不能取地址。
double func()
{return 3.14;
}int x = 10;
int y = 20;
int z = x + y;
double d = func();
以上代码中,10
,20
,x + y
,func()
都是右值,它们出现在=
的右边。10
,20
对应了字面常量
;x + y
对应了表达式返回值
;func()
对应函数返回值
。这些都是右值,它们最显著的特点就是无法取地址。
右值引用语法
先回顾一下左值引用的语法:
int i = 0;
int* p = nullptr;int& ri = i;
int*& rp = p;
左值引用的语法是:type&
;右值引用的语法是:type&&
。
接下来我们尝试对刚刚的值进行右值引用:
int&& ri = 0;
int*&& rp = nullptr;
double&& rd = 3.14;
- 左值引用不能直接引用右值
- const左值引用 可以引用右值
用左值引用直接引用右值:
int& i = 5; // 非法
以const
引用的形式,那么就可以引用右值:
const int& i = 5; // 合法
- 右值引用不能直接引用左值
- 右值引用可以引用
move
后的左值
右值引用不能直接引用左值:
int i = 5;
int&& rri = i; // 非法
函数move
,其可以把一个左值强制转化为一个右值。
int i = 5;
int&& rri = move(i); // 合法
move(i)
之后,i
依然是左值,但是move(i)
这个表达式返回了一个右值的i
。
右值引用底层
既然存在右值引用这个语法,那么我们来看看右值引用到底干了些啥。
右值引用的工作主要有两种情况,一种是右值引用了常量,另外一种是右值引用了move
后的左值。
右值引用了常量:
当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据
看到一段代码:
int&& r = 5;
r = 10;
以上过程中,我们先用r右值引用了常量5,然后通过右值引用把5改为了10。
如果这个过程中,右值常量5存储在常量区,r右值引用后如果r指向常量区的5,会发生什么?此时我们的r = 10操作,就相当于把常量区的5修改为了10,从此以后整个程序中只要去常量区拷贝5都会变成拷贝10,这可就完蛋了。因此我们的右值引用常量,绝对不能直接引用常量区的数据!!
因此,右值引用常量时的真实操作是把常量区的数据拷贝到栈区中,然后这个引用指向这一块栈区内存。
- 当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,该数据可以修改
- 当
const
左值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,但是该数据是常量,不能修改
右值引用了move后的左值:
- 当右值引用了
move
后的左值,右值引用直接指向该左值
看到以下代码:
int i = 5;
int&& rri = move(i);rri = 10;cout << i << endl;
cout << rri << endl;
程序输出结果为:
10
10
我们可以通过修改右值引用来修改左值,或者说以通俗点的说法,此时右值引用就是这个左值的别名。确实是这样的,当右值引用了move
后的左值,其实和直接左值引用这个左值没有任何区别。
- 左值引用解决了传参时存在的拷贝问题
string add_string(string& s1, string& s2)
{string s = s1 + s2;return s;
}int main()
{string str;string hello = "Hello";string world = "world";str = add_string(hello, world);return 0;
}
以上代码中,add_string
函数需要接收两个string
类型的参数,此时我们使用传引用传参,就可以避免两个string
的拷贝消耗。
2.左值引用解决了一部分返回值的拷贝问题
string& say_hello()
{static string s = "hello world";return s;
}int main()
{string str1;string str2;str1 = say_hello();return 0;
}
以上代码中,函数say_hello生成了一个string,并把它返回给外部,如果我们直接返回,那么str1接收参数时,就会先拷贝构造出一个临时变量,然后临时变量再拷贝构造str1。这个过程发生了两次拷贝构造。但是返回值s指向的string是全局的,其出了函数依然存在,因此我们传引用返回,可以不用拷贝构造一个临时变量,直接拿返回值s去拷贝构造,节省了一次拷贝构造。
也就是说,左值引用通过传引用传参
和传引用返回
节省了拷贝。
右值引用,其实更多的是一种标记。
先来看看什么情况下会产生可以被右值引用的左值:
- 当一个左值被move后,可以被右值引用
- C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为
将亡值
右值的意思就是:这个变量的资源可以被迁移走
移动语义
为了讲解移动语义,先写一个简单的mystring
类:
class mystring
{
public://构造函数mystring(const char* str = ""){_str = new char[strlen(str) + 1];strcpy(_str, str);}//析构函数~mystring(){delete[] _str;}// 赋值重载mystring& operator=(const mystring& s){cout << "赋值重载" << endl;return *this;}// 拷贝构造mystring(const mystring& s){cout << "拷贝构造" << endl;}private:char* _str = nullptr;
};
mystring get_string()
{mystring str("hello");return str;
}int main()
{mystring s2 = get_string();return 0;
}
s2
通过函数get_string
来获得字符串,并构造自己。这个过程中,由于str
是局部变量,会发生拷贝构造临时变量,临时变量再拷贝构造s2
的过程。
但是由于str
是一个将亡值
,具有右值属性,我们可以写一个函数直接把它的资源转移走:
class mystring
{
public:// 移动构造mystring(mystring&& s){cout << "移动构造" << endl;std::swap(_str, s._str);}
};
函数主体部分,通过一个swap
函数把参数s
的_str
指针成员与自己的_str
成员进行交换。由于指针指向字符串数组,此时相当于把s
的字符串数组交换给自己,这样就完成了对右值引用的数据转移。
除了移动构造,我们还有原先的拷贝构造:
class mystring
{
public:// 移动构造mystring(mystring&& s){cout << "移动构造" << endl;std::swap(_str, s._str);}// 拷贝构造mystring(const mystring& s){cout << "拷贝构造" << endl;}
};
虽然说我们的左值引用,也可以达到这样的移动构造,但是有一个问题,并不是所有的对象,资源都是可以被转移走的。移动构造之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走。
就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义。
移动
:改语法实现了通过移走别人的资源,实现高效的创建对象,避免大量拷贝语义
:在这个过程中,右值引用只提供语义层面的功能,即许可一个对象资源被转移的右值语义
因为右值引用的出现,C++11后,类的默认成员函数从6
个变成了8
个。新增两个成员函数:移动构造
,移动赋值重载
。
//移动赋值重载
mystring& operator=(mystring&& s)
{std::swap(_str, s._str);return *this;
}// 移动构造
mystring(mystring&& s)
{std::swap(_str, s._str);
}
它们的特点是:参数为右值引用,函数体内部通过交换别人的指针到自己手上,实现高效的资源转移。
引用折叠
看到以下代码:
template <class T>
void func(T&& t)
{cout << "T&& 右值引用" << endl;
}template <class T>
void func(const T& t)
{cout << "const T& const左值引用" << endl;
}int main()
{int a = 5;func(a);//左值func(move(a));//右值return 0;
}
程序输出结果如下:
T&& 右值引用
T&& 右值引用
C++在模板中推出了引用折叠
,也叫做万能引用
,规则如下:
T& &&
推演为T&
T&& &&
推演为T&&
如果你希望当参数为左值引用和右值引用的时候,函数的功能是一样的,你就可以只写一个函数:
template <class T>
void func(T&& t)
{
}
此时,参数T&&
就已经是一个引用折叠了。现在我们来调用这个函数:
int a = 5;
func(a);
func(move(a));
第一次传参,func(a);,模板参数T的类型为int&,但是参数类型为int& &&,此时根据折叠引用规则:int& &&等于int&
第二次传参,func(move(a));,模板参数T的类型为int&&,但是参数类型为int&& &&,此时根据折叠引用规则:int&& &&等于int&&
我们刚才的模板,如果作用于int
类型,就可以推演出四套函数重载:
void func(int&){};
void func(const int&){};
void func(int&&){};
void func(const int&){};
完美转发
看到以下代码:
void fuc1(int& rri)
{cout << "func1 左值引用" << endl;
}void fuc1(int&& rri)
{cout << "func1 右值引用" << endl;
}int main()
{int i = 5;int&& rri = move(i);fuc1(rri);return 0;
}
输出结果:
func1 左值引用
右值引用后,右值引用指向的对象是右值属性,但是引用本身是左值属性
再来看到一个案例:
void func2(int& x)
{cout << "func2 左值引用" << endl;
}void func2(int&& x)
{cout << "func2 右值引用" << endl;
}template <class T>
void fuc1(T&& t)
{func2(t);
}int main()
{int i = 5;fuc1(i);//左值fuc1(move(i));//右值return 0;
}
由于在func1
中,我们经过了折叠引用这一步,T&&
这个参数类型是不确定的。
如果
T&&
是右值的话,传参后t
会变成左值,那么我们可以对其进行move
操作
如果T&&
是左值的话,传参后t
还是左值,我们无需对其进行操作
这个地方就不能粗暴的进行move
了,不然会把原本就是左值的参数,给move
成右值。为了解决这个情况,C++提供了一个函数模板forward
,称为完美转发
,其可以识别到参数的左右值类型,从而将其转化为原来的值。
我们只需要在引用折叠中这样进行调用:
template <class T>
void fuc1(T&& t)
{func2(forward<T>(t));
}
在forward
的模板参数中传入引用折叠的模板参数T
,那么forward<T>
就可以根据t
的类型自动返回其原始的左右值属性了。
相关文章:
C++:右值引用
右值与左值 在讲解右值引用之前,我们就需要先辨析一下左值与右值的区别。 左值 左值是一个表示数据的表达式,我们可以获取它的地址并且对其赋值,左值可以出现在赋值操作符的左边,但是右值不能。 int i 0; int* p &i; do…...
(算法)硬币问题
问题:有1元,5元,10元,50元,100元,500元的硬币各有C1,C5,C10.C50,C100,C500个。 现在要用这些硬币来支付A元,最小需要多少枚硬币? 该题使用递归算法,利用局部最优解来推导…...
如何隐藏 Ubuntu 顶部状态栏
如何隐藏 Ubuntu 顶部状态栏 Chapter1 如何隐藏 Ubuntu 顶部状态栏Chapter2 Ubuntu增大屏幕可用面积之——自动隐藏顶部状态栏Chapter3 Ubuntu18.04隐藏顶栏与侧栏 Chapter1 如何隐藏 Ubuntu 顶部状态栏 https://www.sysgeek.cn/hide-top-bar-ubuntu/ 准备工作:安…...

【C++】入门基础(引用、inline、nullptr)
目录 一.引用 1.引用的定义 2.引用的特性 3.引用的使用场景 4.const引用 5.引用和指针的区别 二.inline 三.nullptr 一.引用 1.引用的定义 引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会给引用变量开辟内存空间,…...
24/07/10数据结构(5.1213)链表OJ
继续练习题: 7.判断链表是不是回文结构 对于一个链表,设计一个时间复杂度O(n)空间复杂度O(1)的算法,判断是否为回文结果 给定一个链表的头指针A,返回一个bool值代表其是否为回文结构. 测试样例:1->2->2->1 返回:ture bool chkPalindrome(ListNode* A){ …...

C++ 入门基础:开启编程之旅
引言 C 是一种高效、灵活且功能强大的编程语言,广泛应用于系统软件、游戏开发、嵌入式系统、科学计算等多个领域。作为 C 语言的扩展,C 不仅继承了 C 语言的过程化编程特性,还增加了面向对象编程(OOP)的支持ÿ…...

据传 OpenAI秘密研发“Strawberry”项目
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...

简单的SQL字符型注入
目录 注入类型 判断字段数 确定回显点 查找数据库名 查找数据库表名 查询字段名 获取想要的数据 以sqli-labs靶场上的简单SQL注入为例 注入类型 判断是数字类型还是字符类型 常见的闭合方式 ?id1、?id1"、?id1)、?id1")等,大多都是单引号…...
HttpClient调用SpringBoot项目的文件上传接口实现文件上传
1.导入httpclient的jar包 这里导入了httpclient、httpmime11 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:sch…...

[leetcode]kth-smallest-element-in-a-sorted-matrix 有序矩阵中第k小元素
. - 力扣(LeetCode) class Solution { public:bool check(vector<vector<int>>& matrix, int mid, int k, int n) {int i n - 1;int j 0;int num 0;while (i > 0 && j < n) {if (matrix[i][j] < mid) {num i 1;j;…...

【经典面试题】是否形成有环链表
1.环形链表oj 2. oj解法 利用快慢指针: /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/typedef struct ListNode ListNode; bool hasCycle(struct ListNode *head) {ListNode* slow head, *fast…...

Flask 用 Redis 缓存键值对-实例
Flask 使用起 Redis 来简直就是手到擒来,比 MySQL 简单多了,不需要那么多配置,实际代码就这么多,直接复制就能用。除了提供简单实用的实例以外,本文后面还会简单介绍一下 Redis 的安装与使用,初学者也能一看…...

我的世界1.21多种服务端开服教程,原版/Forge/Fabric/Paper/Mohist...,Minecraft开服教程
Minecraft(MC)1.21版多种服务端开服教程,我的世界1.21服务器搭建教程,MC原版/Forge/Fabric/Paper/Mohist服务端搭建教程,我的世界MOD/插件服开服教程。 本教程使用 Linux系统MCSManager 面板来搭建Minecraft服务器。 …...

docker安装nginx并配置https
参考 docker安装nginx并配置https-腾讯云开发者社区-腾讯云 (tencent.com) 证书的生成 参见:SpringBoot项目配置HTTPS接口的安全访问(openssl配置)_配置接口访问-CSDN博客 步骤 1: 拉取Nginx镜像 docker pull nginx 好使的镜像如下&#x…...

永磁同步电机控制算法--基于 SVM 的无磁链环 DTC
永磁同步电机无磁链环 DTC 通过控制定子磁链交轴分量来直接控制转矩,不再要求控制磁链幅值恒定,省去了传统 DTC 中的磁链环,不仅转矩响应更快,有效抑制了转矩脉动,而且提高了电机功率因数。但无磁链环 DTC 方案仍采用传…...
如何避免在 Docker 容器中遇到 MAC 地址冲突和 IP 地址冲突的问题
在 Docker 容器中遇到 MAC 地址冲突和 IP 地址冲突的问题时,通常是由于 Docker 在分配网络资源时出现了一些问题。虽然这种情况并不常见,但仍有可能发生。以下是一些原因和可能的解决方案: 原因分析 Docker 版本问题:某些 Docke…...
arm64架构下源码编译安装kafka —— 筑梦之路
一般来说,直接使用官方提供的二进制文件即可,没有必要使用源码编译安装的方式,而对于有特殊用途的,选择源码编译安装无疑是更好地选择。比如修改源码实现想要的功能,mirrormaker2保持topic名称不变。 git clone https…...

LabVIEW前面板占满整个屏幕(转)
希望在运行一个LabVIEW程序时,它的前面板能够占据整个屏幕,且不显示Windows的任务栏或其他任何的LabVIEW菜单选项。怎样才能实现这一功能? 您可以通过手动配置或编程的方式实现该功能。 手动配置VI属性 您可以通过以下操作,将…...
Promise.all、any、race和allSettled的相同点与不同点与应用场景
在JavaScript中,Promise对象是一种处理异步操作的方式。它允许我们以一种更优雅的方式来处理异步代码,而不是使用回调函数。在Promise中,有一些方法可以帮助我们更好地管理多个Promise实例,这些方法包括Promise.all、Promise.any、…...
Ubuntu下如何设置程序include搜索路径及链接路径
添加库的include及lib路径 linux下系统默认路径为 /usr/include, /usr/local/include, gcc在编译程序时会按照当前目录路径->系统默认路径->系统环境变量的路径方式去查找,所以当我们想调用的库未安装在系统默认路径时,我们可以通过手动添加环境变…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...

技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...

基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...

【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...