C++ 右值引用 | 左值、右值、move、移动语义、引用限定符

文章目录

  • C++11为什么引入右值?
  • 区分左值引用、右值引用
  • move
  • 移动语义
    • 移动构造函数
    • 移动赋值运算符
    • 合成的移动操作
    • 小结
  • 引用限定符
    • 规定this是左值or右值
    • 引用限定符与重载


C++11为什么引入右值?

C++11引入了一个扩展内存的方法——移动而非拷贝,移动较之拷贝有两个优点:

  1. 效率更高: 在此之前,当数据结构申请的内存用尽时,一般是申请一块更大的内存,然后将旧内存中存储的元素拷贝到新内存中。但很多情况下,为了方便拷贝操作而建立的临时对象在拷贝完成后就被销毁了,因此不如直接将旧内存中的元素移动到新内存中,即省空间(临时对象也是要占内存的),还省时间(不用建立临时对象了)。
  2. IOunique_ptr 这样的类都包含不可被共享的资源(如指针或IO缓冲),因此,这些类不支持拷贝,仅支持移动。

PS:STLshared_ptr 既支持移动也支持拷贝。

而为了支持移动操作,就诞生了一种新的引用类型——右值引用(rvalue reference)

为了与左值引用进行划分,使用 & 时则代表是左值引用,而使用 && 则代表右值引用。

右值引用有一个重要的特性——只能绑定到一个将要销毁的对象。


区分左值引用、右值引用

左值

生成左值: 返回引用的函数、赋值、取下标、解引用、前置递增/递减运算符。

我们可以将一个 左值引用 绑定到这类表达式的结果上。

右值

生成右值: 返回非引用类型的函数、算术、关系、位、后置递增/递减运算符。

我们可以将一个 const的左值引用 或者一个 右值引用 绑定到这类表达式上。

举一些例子:

int i = 42;
int &r = i; // 正确:左值引用绑定变量
int &&rr = i; // 错误:不能将右值引用绑定到左值上
int &r2 = i * 42; // 错误:i*42是右值,不能将左值引用绑定到右值上
const int &r3 = i * 42; // 正确:可以将const左值引用绑定到右值上
int &&rr2 = i * 42; // 正确:右值引用可以绑定到算术结果上

详细来讲:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址,C++11认为其是左值。(const类型常量初始化时,编译器不给其开辟空间,当对该常量取地址时,编译器才为其开辟空间。)
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用,认为是左值。

总的来讲,即为:左值持久、右值短暂,左值有持久的状态,而右值要么是字面常量、要么是在表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象,我们得知:

  • 所引用的对象将要被销毁
  • 该对象没有其他用户

这两个特性意味着:可以自由地接管右值引用绑定的资源,而不必担心发生错误。

有趣的是,右值引用本身是一个变量,因此它是一个左值,也就是说,不能将右值引用绑定到一个右值引用类型的变量上:

int &&rr1 = 42; // 正确:字面常量是右值
int &&rr2 = rr1; // 错误:表达式rr1是左值

move

按照语法来说,右值引用应该只能引用右值,但我们可以通过move函数显式地将一个左值转换为对应的右值引用类型

#include<utility> //move的头文件
int &&rr1 = 42; // 右值引用
int &&rr2 = std::move(rr1); // rr1是左值,绑定到右值引rr2上

调用move就意味着:可以销毁一个移后源对象(rr1),也可以赋予它新值,但不能使用一个移后源对象(rr1)的值。

与大多数标准库名字的使用不同,对 move 我们不提供 using声明。换言之,我们直接调用 std::move 而不是 move。因为 STL 还有另一个 move,那个的作用就是将一个范围中的元素搬移到另一个位置。


移动语义

移动构造函数

  • 类似拷贝构造函数,移动构造函数的第一个参数是该类类型的引用,任何额外的参数都必须有默认实参。
  • 不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用

除了完成资源移动,移动构造函数还必须确保移后源对象是可销毁的。 一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。

作为一个例子,我们为 IntVec类 定义移动构造函数,实现从一个 IntVec 到另一个 IntVec 的元素移动而非拷贝:
在这里插入图片描述

class IntVec // IntVec是对标准库vector类的模仿,仅存储int元素
{int *begin; // 指向已分配的内存中的首元素int *end; // 指向最后一个实际元素之后的位置int *cap; // 指向分配的内存末尾之后的位置
public:IntVec(IntVec &&a) noexcept // noexcept通知标准库不抛出任何异常: begin(a.begin), end(a.end), cap(a.cap) // 成员初始化器接管a中的资源{a.begin = a.end = a.cap =nullptr;// 令a进入可销毁状态,确保对其运行析构函数是安全的。}
}; 

工作流程:

  1. 移动构造函数不分配任何新内存,而是接管给定的 IntVec 中的内存。
  2. 接管之后,将给定对象中的指针都置为 nullptr
  3. 函数体执行完毕自动调用析构函数销毁移后源对象。

在第三点中,如果我们没有进行第二点,此时移后源对象仍指向被接管的内存,此时调用析构函数会释放掉刚刚移动的内存,因此三步一步都不能少。

关于 noexcept

  • 由于移动操作不分配任何资源,因此不会抛出异常,我们可以通知标准库,这样他就不会因为需要等待处理异常而浪费资源。
  • noexcept 是通知标准库的方式之一,出现在参数列表和初始化列表开始的冒号之间。

为什么移动操作不会抛出异常?

首先明确一定,是允许移动操作抛出异常的,但是这么做反而有坏处。

vectorpush_back 操作来讲,当执行尾插操作但是内存空间已经满了,需要重新分配内存空间,此时:

  • 如果重新分配过程使用了移动构造函数,且在移动了部分元素后抛出了一个异常,就会产生问题——旧空间中的移动源元素已经被改变了,而新空间中移动源元素尚未构造好。在此情况下,vector 将丢失自身的部分元素。
  • 如果 vector 使用了拷贝构造函数,当在新内存中构造元素时,旧内存中的元素保持不变。如果此时发生了异常,vector 可以释放新分配的(但还未成功构造的)内存并返回。vector 原有的元素仍然存在。

因此,对于移动操作来讲,不抛出异常反而能保证数据的完整性。


移动赋值运算符

和移动构造函数一样——不抛出异常,但仍要注意处理所有赋值运算符逃不过的劫难——自赋值问题。

IntVec& IntVec::operator=(IntVec &&rhs) noexcept{if(this != &rhs){ // 处理非自赋值free(); // 释放已有资源begin = rhs.begin; // 从 rhs 接管资源end = rhs.end;cap = rhs.cap;// 将 rhs 置于可析构状态rhs.begin = rhs.end = rhs.cap = nullptr;}return *this;
}

这种写法其实是最常用也最简单的自赋值处理方法,像之前讲的 用临时量存右侧运算对象swap实现自赋值 。巧妙则巧妙,但是写起来一定要很小心,远不如直接 if-else 来的方便。


合成的移动操作

如果我们不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作。但与拷贝操作不同,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作

只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个 非static数据成员 都可以移动时,编译器才会为它合成移动操作。

编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员:
在这里插入图片描述


小结

在移动操作之后,移后源对象必须保持有效的、可析构的状态。

移后源对象仍然保持有效

我们可以对它执行诸如 emptysize 这些操作。但是,我们不知道将会得到什么结果。我们可能猜测一个移后源对象是空的,但结果并不一定如我们猜测的那样。换言之,我们可以重新用它,但是我们不知道用之前它是什么状态。


同时存在拷贝控制操作和移动操作时的匹配规则

  • 拷贝构造函数接受一个 const 类型名& 的左值引用类型;
  • 移动构造函数接受一个 类型名&& 右值引用类型。

因此,左值只能匹配拷贝构造函数,但是右值却都可以匹配,只是调用拷贝操作时需要进行一次到 const 的转换,而移动操作精确匹配,因此,右值会使用移动操作。


swap实现一个赋值运算符既是拷贝操作也是移动操作

  • 移动赋值运算符接受一个 类型名&& 右值引用类型;
  • 拷贝赋值运算符接受一个 const 类型名& 的左值引用类型。

因此,我们可以在已经定义好移动构造函数的基础上,借助 swap函数 实现一个形参为 类型名 的赋值运算符:

class IntVec
{
public:IntVec(IntVec &&a) noexcept: begin(a.begin), end(a.end), cap(a.cap){a.begin = a.end = a.cap =nullptr;}IntVec& operator=(IntVec a){swap(*this, a);return *this;}
}; 

具体思想我们在上一篇博客的swap实现自赋值中讲过一次,这里简单再提一下。

  • 首先 swap函数 是类自己重载的,而不是标准库中的 swap函数,目的是避免浪费内存。
  • 一定要确保类已经定义好了移动构造函数,否则,像我们之前说过的那样,在有拷贝操作的情况下,类不会合成移动操作,则该赋值运算符只实现了拷贝操作而没有实现移动操作。
  • 该赋值运算符最终实现的操作由传入的实参类型决定:左值拷贝、右值移动

举个例子:

// 假定 v1、v2 都是 IntVec 对象
v1 = v2; // v2是左值,拷贝构造函数来拷贝v2
v1 = std::move(v2); // 移动构造函数移动v2

匹配详情就不多说了,在上文的匹配规则中讲的很详细了,这里主要想体现的是:不管使用的是拷贝构造函数还是移动构造函数,赋值运算符都可以将他们的结果作为实参来执行。换言之,配合上 swap函数赋值运算符 同时支持 移动操作拷贝操作


为什么拷贝操作的形参通常是 const X& 而不是 X&?移动操作的形参通常是 X&& 而不是 const X&&?

  • 当我们希望使用 将亡值 时,通常传递一个右值引用。为了在移动后释放源对象持有的资源,实参不能是 const 的。
  • 从一个对象进行拷贝的操作不应该改变该对象。因此,通常不需要定义一个接受一个 (普通的)X& 参数的版本。

引用限定符

规定this是左值or右值

有时会看到这样的代码:

string s1 = "hello", s2 = "world";
s1 + s2 = "!";

此处我们对两个 string 的连接结果——一个右值,进行了赋值。

在旧标准中,我们没有办法阻止这种使用方式。为了维持向后兼容性,新标准库类仍然允许向右值赋值。但是,我们有时需要阻止这种用法。在此情况下,我们希望强制左侧运算对象(即,this指向的对象)是一个左值。

我们指出 this 的左值/右值属性的方式与定义 const 成员函数相同,即,在参数列表后放置一个引用限定符(reference qualifier)

class IntVec
{
public:IntVec& operator=(IntVec a) & // 只能向可修改的左值赋值{swap(*this, a);return *this;}
}; 

引用限定符可以是 &&&,分别指出 this 可以指向一个左值或右值。类似 const 限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。

一个函数可以同时用 const引用限定。在此情况下,引用限定符必须跟随在const限定符之后:

class IntVec
{
public:IntVec& operator=(IntVec a) const &}; 

引用限定符与重载

就像一个成员函数可以根据是否有 const 来区分其重载版本一样,引用限定符也可以区分重载版本。

举个例子:
在这里插入图片描述

编译器会根据调用 sorted 的对象的左值/右值属性来确定使用哪个 sorted 版本:
在这里插入图片描述

  • 当我们定义 const成员函数 时,可以定义两个版本,唯一的差别是一个版本有 const限定 而另一个没有。
  • 引用限定的函数则不一样。如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加。

在这里插入图片描述

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

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

相关文章

且谈关于最近软件测试的面试

前段时间有新的产品需要招人&#xff0c;安排和参加了好几次面试&#xff0c;下面就谈谈具体的面试问题&#xff0c;在面试他人的同时也面试自己。 面试问题是参与面试同事各自设计的&#xff0c;我也不清楚其他同事的题目&#xff0c;就谈谈自己设计的其中2道题。 过去面试总是…

C++ 多态 | 虚函数、抽象类、虚函数表

文章目录多态虚函数重写重定义&#xff08;参数不同&#xff09;协变&#xff08;返回值不同&#xff09;析构函数重写&#xff08;函数名不同&#xff09;final和override重载、重写、重定义抽象类多态的原理虚函数常见问题解析虚函数表多态 一种事物&#xff0c;多种形态。换…

C++ 运算符重载(一) | 输入/输出,相等/不等,复合赋值,下标,自增/自减,成员访问运算符

文章目录输出运算符<<输入运算符>>相等/不等运算符复合赋值运算符下标运算符自增/自减运算符成员访问运算符输出运算符<< 通常情况下&#xff0c;输出运算符的第一个形参是一个 非常量ostream对象的引用 。之所以 ostream 是非常量是因为向流写入内容会改变…

C++ 重载函数调用运算符 | 再探lambda,函数对象,可调用对象

文章目录重载函数调用运算符lambdalambda等价于函数对象lambda等价于类标准库函数对象可调用对象与function可调用对象function函数重载与function重载函数调用运算符 函数调用运算符必须是成员函数。 一个类可以定义多个不同版本的调用运算符&#xff0c;互相之间应该在参数数…

C++ 运算符重载(二) | 类型转换运算符,二义性问题

文章目录类型转换运算符概念避免过度使用类型转换函数解决上述问题的方法转换为 bool显式的类型转换运算符类型转换二义性重载函数与类型转换结合导致的二义性重载运算符与类型转换结合导致的二义性类型转换运算符 概念 类型转换运算符&#xff08;conversion operator&#…

分布式理论:CAP、BASE | 分布式存储与一致性哈希

文章目录分布式理论CAP定理BASE理论分布式存储与一致性哈希简单哈希一致性哈希虚拟节点分布式理论 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系统中的所有数据副本&#xff0c;在同一时刻是否一致&#xff08;所有节点访问同一份最新的数据副…

分布式系统概念 | 分布式事务:2PC、3PC、本地消息表

文章目录分布式事务2PC&#xff08;二阶段提交协议&#xff09;执行流程优缺点3PC&#xff08;三阶段提交协议&#xff09;执行流程优缺点本地消息表&#xff08;异步确保&#xff09;分布式事务 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分…

数据结构算法 | 单调栈

文章目录算法概述题目下一个更大的元素 I思路代码下一个更大元素 II思路代码132 模式思路代码接雨水思路算法概述 当题目出现 「找到最近一个比其大的元素」 的字眼时&#xff0c;自然会想到 「单调栈」 。——三叶姐 单调栈以严格递增or递减的规则将无序的数列进行选择性排序…

最长下降子序列

文章目录题目解法DP暴搜思路代码实现贪心二分思路代码实现题目 给出一组数据 nums&#xff0c;求出其最长下降子序列&#xff08;子序列允许不连续&#xff09;的长度。&#xff08;类似于lc的最长递增子序列&#xff09; 示例&#xff1a; 输入&#xff1a; 6 // 数组元素个…

Linux 服务器程序规范、服务器日志、用户、进程间的关系

文章目录服务器程序规范日志rsyslogd 守护进程syslog函数openlog函数setlogmask函数closelog函数用户进程间的关系进程组会话系统资源限制改变工作目录和根目录服务器程序后台化服务器程序规范 Linux 服务器程序一般以后台进程&#xff08;守护进程[daemon]&#xff09;形式运…

IO模型 :阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路复用IO

文章目录IO模型阻塞IO非阻塞IO信号驱动IO多路复用IO异步IOIO模型 根据各自的特性不同&#xff0c;IO模型被分为阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路复用IO五类。 最主要的两个区别就是阻塞与非阻塞&#xff0c;同步与异步。 阻塞与非阻塞 阻塞与非阻塞最主要的区别就…

Tomcat服务器集群与负载均衡实现

一、前言 在单一的服务器上执行WEB应用程序有一些重大的问题&#xff0c;当网站成功建成并开始接受大量请求时&#xff0c;单一服务器终究无法满足需要处理的负荷量&#xff0c;所以就有点显得有点力不从心了。另外一个常见的问题是会产生单点故障&#xff0c;如果该服务器坏掉…

Linux服务器 | 事件处理模式:Reactor模式、Proactor模式

文章目录Reactor模式Proactor模式同步I/O模型模拟Proactor模式两者的优缺点ReactorProactor同步I/O模型通常用于实现 Reactor 模式&#xff0c;异步I/O模型通常用于实现 Proactor 模式。&#xff08;不是绝对的&#xff0c;同步I/O也可模拟出 Proactor 模式&#xff09; React…

Linux服务器 | 服务器模型与三个模块、两种并发模式:半同步/半异步、领导者/追随者

文章目录两种服务器模型及三个模块C/S模型P2P模型I/O处理单元、逻辑单元、存储单元并发同步与异步半同步/半异步模式变体&#xff1a;半同步/半反应堆模式改进&#xff1a;高效的半同步/半异步模式领导者/追随者模式组件 &#xff1a;句柄集、线程集、事件处理器工作流程两种服…

字符串匹配之KMP(KnuthMorrisPratt)算法(图解)

文章目录最长相等前后缀next数组概念代码实现图解GetNext中的回溯改进代码实现代码复杂度分析最长相等前后缀 给出一个字符串 ababa 前缀集合&#xff1a;{a, ab, aba, abab} 后缀集合&#xff1a;{a, ba, aba, baba} 相等前后缀 即上面用同样颜色标识出来的集合元素&#…

Android入门(一) | Android Studio的配置与使用

文章目录安装配置Android Studio使用Android Studio模拟器更改Android SDK的路径Hello World&#xff01;安装配置Android Studio 从这一步开始&#xff1a; 一直点 next 即可&#xff0c;直到存储路径的选择上&#xff0c;可以放到非 C 盘&#xff0c;这里我放到 D 盘了&am…

Android 入门(四) | Intent 实现 Activity 切换

文章目录Intent显式 Intent定义两个 xml 文件android:orientationmatch_parent 和 wrap_contentIntent函数定义两个 Activity隐式 Intent更多隐式 Intent 的用法用隐式 Intent 打开系统浏览器自建 Activity 以响应打开网页的 Intent向下一个活动传递数据返回数据给上一个活动In…

Android入门(二) | 项目目录及主要文件作用分析

文章目录项目目录分析app目录分析AndroidManifest.xml 分析MainActivity.kt 分析build.gradle 分析最外层目录下的 build.gradleapp 目录下的 build.gradle项目目录分析 我们来看一下 src/main/res 下的一些文件&#xff1a; .gradle 和 .idea &#xff1a;这两个目录下放置…

Android入门(三) | Android 的日志工具 Logcat

文章目录日志工具类 android.util.LogLogcat 中的过滤器日志工具类 android.util.Log Log 从属日志工具类 android.util.Log &#xff0c;该类提供了五个方法供我们打印日志&#xff1a; Log.v() &#xff1a;用于打印那些最为琐碎的、意义最小的日志信息。对应级别 verbose&…

Android入门(五) | Activity 的生命周期

文章目录Activity 的状态及生命周期实现管理生命周期FirstActivitySecondActivityDialogActivity运行结果旧活动被回收了还能返回吗&#xff1f;Activity 的状态及生命周期 Android 的应用程序运用 栈&#xff08;Back Stack&#xff09; 的思想来管理 Activity&#xff1a; …