【C++ Primer Plus】C++11 深入理解右值、右值引用和完美转发
1. 右值引用和移动语义
1.1 左值和右值
- 左值 local value:存储在内存中、有明确存储地址(可寻址)的数据(x、y、z)
- 右值 read value:不一定可以寻址,例如存储于寄存器中的数据;通常字面量都是右值,除了字符串常量(1、3)
int x = 1; int y = 3; int z = x + y;
对于x++和++x虽然都是自增操作,但x++编译器首先生成一份临时值,然后对x自增,最后返回临时内容,所以x++是右值;++x是对x递增后返回自身,所以++x是左值
x++; // 右值引用 ++x; // 左值引用 int *p1 = &x++; // 右值引用所以编译失败 int *p2 = &++x; // 左值引用可以编译成功
1.2 左值引用和右值引用
- 左值引用:必须引用一个左值。
- 常量左值引用:可以引用左值或右值。因为右值的生命周期被延长了,但这种引用存在一个问题,常量左值引用导致无法修改对象内容。
int &x1 = 7; // 非常量左值引用 编译错误 const int &x = 11; // 语句结束后,11的生命周期被延长了 const int x = 11; // 语句结束后,11立刻被销毁
- 右值引用:引用右值且只能引用右值的方法
int &&k = 11; // 右值引用(延长右值的生命周期)
对于数字的表示可能不太清晰,因为数字本身就有些虚无缥缈,下面用一个类的例子来更好解释右值引用的优势,可以减少复制构造来优化性能(但实际上编译器会帮我们优化)
- 优化前:MyClass&& mc = make_myclass(); 调用后
- ctor: 调用mc的构造函数
- copy ctor:返回值时调用mc的拷贝函数
- dtor: 返回值后将mc销毁调用析构函数(注意这里的复制构造函数会存在问题:如果是浅复制,此时销毁的对象会把堆区内存销毁导致新的对象空引用,所以还是强调必须重写复制构造函数)
- 由于右值引用,延长了右值的生命周期
- dtor: main函数结束再调用一次析构函数
- 编译器优化后:MyClass&& mc = make_myclass(); 调用后
- ctor: 调用构造函数
- dtor: 调用析构函数
- MyClass mc = make_myclass(); 该函数调用后
- ctor: 调用mc的构造函数
- copy ctor: 返回值 调用mc的拷贝函数
- dtor: 返回值后销毁mc 调用析构函数
- copy ctor: 为了构造mc2 调用构造函数
- dtor: 销毁返回值临时变量 调用析构函数
- dtor: main函数结束后再调用一次析构函数
#include <iostream> using namespace std; class MyClass { public: char* pc; MyClass(); MyClass(const MyClass& myclass); ~MyClass() { cout << "dtor" << endl; delete pc; } void show() { cout << "Show: " << pc << endl; } }; MyClass::MyClass() { cout << "ctor" << endl; pc = new char[10]; for (int i = 0; i < 10; i++) pc[i] = 'c'; } MyClass::MyClass(const MyClass& myclass) { cout << "copy ctor" << endl; pc = myclass.pc; for (int i = 0; i < 10; i++) pc[i] = myclass.pc[i]; } MyClass make_myclass() { MyClass mc; // 1.构造函数 3.析构函数(此时如果是浅复制,新的对象指向的内存也将为空 return mc; // 2.拷贝函数 } int main() { MyClass&& mc = make_myclass(); // 返回值的生命周期被延长 mc.show(); cout << endl; MyClass mc2 = make_myclass(); // 再次调用拷贝构造函数 mc2.show(); }
1.3 移动语义
上面其实已经用到了移动语义,移动语义主要就是解决C++复制构造对性能的影响。但也存在问题,例如移动构造函数运行过程中发生了异常,这会造成源对象和目标对象都不完整。这里再用一个例子说明,该Useless类内有一个元素个数为 n 的 char 数组,静态变量 ct 记录了对象个数。
- Useless one(20, 'o'); 调用 int char 构造函数
- Useless one(20, 'c'); 调用 int char 构造函数
- Useless three(one + two);
- one + two 调用 operator+ 运算符重载,在内部调用 int 构造函数构造了对象 temp
- 返回值后调用移动语义构造函数,夺走 temp 里指针指向的内容并把它的指针设置为空,这样它在销毁时不会把堆区内存清空
- 临时对象 temp 被销毁
class Useless { public: int n; // 元素个数 char* pc; // 数据指针 static int ct; // 对象个数 void ShowObject()const; Useless(int k); Useless(int k, char ch); Useless(Useless&& f); // 移动构造 ~Useless(); Useless operator+(const Useless& f)const; void ShowData() const; }; int Useless::ct = 0; Useless::Useless(int k) :n(k) { printf("int 参数的构造函数; 对象个数为: %d\n", ++ct); pc = new char[n]; ShowObject(); } Useless::Useless(int k, char ch) :n(k) { printf("int char参数的构造函数; 对象个数为: %d\n", ++ct); pc = new char[n]; for (int i = 0; i < n; i++){ pc[i] = ch; } ShowObject(); } Useless::Useless(Useless&& f) :n(f.n) { printf("移动构造函数; 对象个数为: %d\n", ++ct); pc = f.pc; f.pc = nullptr; f.n = 0; ShowObject(); } Useless::~Useless() { printf("析构函数调用; 元素个数为: %d\n", --ct); ShowObject(); delete[] pc; } Useless Useless::operator+(const Useless& f)const { printf("进入 operator+\n"); Useless temp = Useless(n + f.n); for (int i = 0; i < n; i++) temp.pc[i] = pc[i]; for (int i = n; i < temp.n; i++) temp.pc[i] = f.pc[i - n]; printf("离开 operator+\n"); return temp; } void Useless::ShowObject() const { printf("元素个数: %d, 数据地址: %x\n", n, (void*)pc); } void Useless::ShowData()const { if (n == 0) printf("元素个数为空\n"); else for (int i = 0; i < n; i++) printf("%c ", pc[i]); printf("\n"); } int main() { Useless one(20, 'o'); // int char 构造函数 对象个数1 printf("\n"); Useless two(20, 'c'); // int char 构造函数 对象个数2 printf("\n"); Useless three(one + two); // 1. operator+ 调用 int 构造函数 对象个数3; 2. operator+ 返回右值, 调用移动构造函数(减少了复制的次数) 对象个数4; 3.临时对象被销毁 对象个数3 printf("\n"); printf("object one: \n"); one.ShowData(); printf("object two: \n"); two.ShowData(); printf("object three: \n"); three.ShowData(); printf("\n"); }
1.4 强制移动
移动构造函数和移动赋值运算符都必须使用右值,但如果让他们使用左值就需要一些特殊处理
Useless choices[10]; Useless best; int pick = 5; best = chioces[pick]; // 由于这里是左值,所以会调用普通的复制赋值运算符
可以使用C++11头文件utility中提供的move函数来实现将左值转换为右值,但是注意右值的字段会被夺走,并且必须定义了移动赋值运算符或移动构造函数
#include <iostream> #include <utility> #include <vector> #include <string> int main() { std::string str = "Hello"; std::vector<std::string> v; //调用常规的拷贝构造函数,新建字符数组,拷贝数据 v.push_back(str); std::cout << "After copy, str is \"" << str << "\"\n"; //调用移动构造函数,掏空str,掏空后,最好不要使用str v.push_back(std::move(str)); std::cout << "After move, str is \"" << str << "\"\n"; std::cout << v[0] << ", " << v[1] << "\n"; }
2. 万能引用
很多时候我们希望传递的是一个引用而非通过拷贝构造传递,这可以提高程序效率;但仅仅通过fn(className& c)来传递引用会导致不能传递右值,fn(const className& c)又会导致传递进来的参数不能被修改,所以提出了万能引用的概念
2.1 引用折叠
万能引用实际上就是发生了类型推导,如果源对象是一个左值,则推导出左值引用;如果源对象是一个右值,则推导出右值引用。
void foo(int &&i) {} // i为右值引用 template<class T> void bar(T &&t) {} // t为万能引用 template<class T> void bar(vector<T> &&t) {} // 非万能引用,必须是直接的T int get_val() {return 5;} int &&x = get_val(); // x 为右值引用 auto &&y = get_val(); // y 为万能引用
C++11 通过一套引用叠加推导的规则来实现万能引用——引用折叠,可以注意到实际类型是左值引用,则最终类型一定是左值引用;只有引用类型是一个非引用类型或者右值引用,最后推导出来的才是一个右值引用
通过下面几行代码理解引用折叠,首先是C++11规定的展开时的定义
- 实参类型为T的左值, 则模板T展开为T& int => int&(T)
- 此时Test形参的类型为 T& &&,经过折叠后为 T& 左值引用
- 此时static_cast<T&>(t) 将t转为左值引用,所以调用左值引用的函数
- 实参类型为T的右值, 则模板T展开为T int => int(T)
- 此时Test形参的类型为 T &&,所以为右值引用
- 此时static_cast<T&&>(t) 将t转为右值引用,所以调用右值引用的函数
#include <iostream> void process(int& i) { std::cout << "左值引用" << std::endl; } void process(int&& i) { std::cout << "右值引用" << std::endl; } template<class T> void Test(T&& t) { process(static_cast<T&&>(t)); } int main() { int a = 1; Test(a); // C++11规定 实参类型为T的左值, 则模板T展开为T& int => int&(T) Test(1); // C++11规定 实参类型为T的右值, 则模板T展开为T int => int(T) }
2.2 完美转发
通过 std::forward<T>() 可以实现完美转发,不论左值还是右值都可以通过引用的方式传参,提高程序运行的效率。下面给出了一个完美转发的例子,打印了 T 的实际类型,并通过修改 t 的值实现了修改 a 的值(传入左值即左值引用),同样如果传入类的右值一样是右值引用。
#include <iostream> template<class T> void show_Type(T&& t) { std::cout << "is int&: " << std::is_same_v<T, int&> << std::endl; std::cout << "is int : " << std::is_same_v<T, int> << std::endl; t = 10; } template<class T> void perfect_forwarding(T&& t) { show_Type(std::forward<T>(t)); } int main() { int a = 5; perfect_forwarding(5); // 该参数在不同函数间始终以右值引用方式传递 perfect_forwarding(a); // 该参数在不同函数间始终以左值引用方式传递 std::cout << a; // 由于以引用的方式传递, 这里内存中的数值也一样会修改 }
相关文章:

【HTTP协议】简述HTTP协议的概念和特点
HTTP(Hypertext Transfer Protocol)是一种用于在Web上进行数据通信的协议。它是基于客户端-服务器模型的,其中客户端发送请求,服务器返回响应。

零基础搭建本地Nextcloud私有云结合内网穿透实现远程访问
文章浏览阅读753次,点赞53次,收藏47次。本文主要讲解如何搭建本地Nextcloud私有云结合内网穿透实现远程访问

centos7下执行yum命令报错
文章浏览阅读323次,点赞11次,收藏7次。在Linux系统中,安装nginx时候,需要先安装环境。Nginx是使用C语言开发,安装nginx需要先从官网上将源码下载,然后编译,编译需要gcc环境,但是在安装gcc环境的时候,执行命令报错。

鸿蒙开发软件用什么编程语言?
鸿蒙经过几年的迭代,抛弃了Java,基于TS出了一个官方推荐的ArkTS语言,甩开了JVM,提升效率,同时支持自己研发的一些现代化特性,没有版权的问题,现在唯一的问题就是各大公司愿不愿意为它去适配生态了,还好的是,目前各大互联网公司已经开始适配了。

spring cloud Eureka注册中心和Nacos注册中心
文章浏览阅读103次。代码方式:在order-service中的OrderApplication类中,定义一个新的IRule:@Bean配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则注意。

通过.NET Core+Vue3 实现SignalR即时通讯功能
.NET Core 和 Vue3 结合使用 SignalR 可以实现强大的实时通讯功能,允许实时双向通信。在这个示例中,我们将详细说明如何创建一个简单的聊天应用程序,演示如何使用 .NET Core SignalR 后端和 Vue3 前端来实现实时通讯功能。 步骤1:准备工作 确保你已经安装了以下工

Python爬虫遇到重定向URL问题时如何解决?
文章浏览阅读652次,点赞14次,收藏6次。重定向是指当用户请求一个URL时,服务器返回一个中断请求的URL的响应。这种情况通常发生在网站对URL进行了修改或者重定向到其他页面的情况下。其中,如果处理不当开发,可能会导致爬虫无法获取所需的数据,从而影响爬虫的效果。在Python爬虫开发中,处理重定向URL问题是非常的。我们使用可以请求库来处理重定向,通过查看重定向后的重要URL和重定向历史来了解重定向的情况,从而确保爬虫能够正确获取所需的数据。

C# 实现微信退款及对帐
文章浏览阅读1.6k次,点赞77次,收藏84次。本次我们以微信支付进行举例,在考生注册账号、编写简历、报名职位、被初审核通过等一系列基础的条件的具备下,可以进入支付考务费的环节(笔试费用),我们会为其生成一个支付二维码,考生支付后(无论成功与否),都会记录其支付结果状态。以上提供的代码仅供参考,在实际的应用中,我们还可以根据业务需要编写其它功能,如下载微信官方对帐单,导入到应用系统中,与业务数据进行对帐,以排查争议数据;退款申请成功后,仅为申请状态,需要通过查询退款情况以确定是否完成,该功能可以在考生方进行实现,考生可随时查询自己的对帐情况。

uniapp开发App从开发到上架全过程
当我们的APP开发完毕,最终交付的时候,必然要经历的一个环节,就是APP上架,国内APP上架一般为IOS端appstore上架,安卓端应用商店比较多,最常见的应用商店有华为应用商店、小米应用商店、OPPO应用商店、VIVO应用商店、应用宝应用商店等。 在开始上架 前,需要准备好相应的材料,安卓端

vue实现动态路由菜单!!!
文章浏览阅读244次,点赞2次,收藏2次。递归处理后端响应的菜单树,后依次通过addRoute方法往静态父路由,添加动态子路由,添加完使用el-menu渲染并添加router属性实现路由菜单模式。

GaussDB数据库SQL系列-触发器
文章浏览阅读680次,点赞37次,收藏33次。GaussDB数据库中的触发器是一种强大的工具,可用于自动化数据处理、数据验证、日志记录等任务。通过使用触发器,您可以提高数据一致性、减少数据冗余、实施业务规则并增强数据安全性。本文介绍了GaussDB数据库中触发器的基本概念、创建步骤和示例。希望能够帮助您更好地了解和使用GaussDB中的触发器功能。

本地Nginx服务搭建结合内网穿透实现多个Windows Web站点公网访问
文章浏览阅读1.1k次,点赞96次,收藏91次。访问http://127.0.0.1:9200/登录cpolar web UI管理界面,点击左侧仪表盘的隧道管理——隧道列表,找到所要配置的隧道,点击右侧的编辑。接下来,我们通过强大的且稳定的内网穿透工具cpolar,将本地nginx服务暴露至公网环境,以实现穿透多个站点端口需求,无需公网IP,也不用设置路由器。提示更新隧道成功,点击左侧仪表盘的状态——在线隧道列表,可以看到公网地址已经更新为保留成功的二级子域名,将其复制下来。修改隧道信息,将保留成功的二级子域名配置到隧道中。

swagger注解属性设置导致的报错:For input string: ““
文章浏览阅读61次。使用swagger的时候,用 @ApiModelProperty 修饰的类的属性如果没设置的话默认赋值为“”,当Integer类型的属性没设置example的话就会报错。

go的HTTP网络编程
文章浏览阅读258次,点赞8次,收藏2次。本文教大家用go实现http网络编程

Flask 实现Token认证机制
在Flask框架中,实现Token认证机制并不是一件复杂的事情。除了使用官方提供的`flask_httpauth`模块或者第三方模块`flask-jwt`,我们还可以考虑自己实现一个简易版的Token认证工具。自定义Token认证机制的本质是生成一个令牌(Token),并在用户每次请求时验证这个令牌

elasticsearc DSL查询文档
文章浏览阅读68次。精确查询常见的有哪些?term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段range查询:根据数值范围查询,可以是数值、日期的范围。

什么是分布式锁?Redis实现分布式锁详解
文章浏览阅读151次,点赞4次,收藏3次。在分布式系统中,涉及多个主机访问同一块资源,此时就需要锁来做互斥控制,避免出现类似线程安全问题。而Java中的synchronized只是对当前进程中的线程有效,多个主机实际上是多个进程,那么它就无能为力了,此时就需要分布式锁。

MySQL运行在docker容器中会损失多少性能
前言 自从使用docker以来,就经常听说MySQL数据库最好别运行在容器中,性能会损失很多。一些之前没使用过容器的同事,对数据库运行在容器中也是忌讳莫深,甚至只要数据库跑在容器中出现性能问题时,首先就把问题推到容器上。 那么到底会损失多少,性能损失会很多吗? 为此我装了两个MySQL,版本都是8.

【Docker】Docker与Kubernetes:区别与优势对比
一种革新性的容器技术一、Docker与Kubernetes简介二、架构和部署模型1. Docker 部署模型2. 构建 Docker 镜像3. 运行容器4. 编排工具三、可移植性和可扩展性1. 可移植性(Portability):2. 可扩展性(Scalability):四、管理和编排能力五、生态系统和社区支持

微信小程序完整实现微信支付功能(SpringBoot和小程序)
然后到提供前端调用支付路由的类,WechatController类,注意我这里路由拼接的有/wechat/pay/notify,这个要和之前配置yml文件的支付回调函数一样,要不然不行。不久前给公司实现支付功能,折腾了一阵子,终于实现了,微信支付对于小白来说真的很困难,特别是没有接触过企业级别开发的大学生更不用说,因此尝试写一篇我如何从小白实现微信小程序支付功能的吧,使用的后端是SpringBoot。效果如下,这里因为我的手机不能截图支付页面,所以用的开发者工具支付的效果,都是一样的。4.前端(小程序端)

IT行业哪个方向比较好就业?
文章浏览阅读12次。在IT行业中,就业前景好的方向有很多,以下是一些比较热门的:

通过内网穿透本地MariaDB数据库,实现在公网环境下使用navicat图形化工具
文章浏览阅读113次,点赞50次,收藏40次。cpolar安装成功后,双击打开cpolar【或者在浏览器上访问本地9200端口 127.0.0.1:9200】,使用cpolar邮箱账号登录 web UI管理界面,如果还没有注册cpolar账号的话,点击免费注册,会跳到cpolar官网注册一个账号就可以了.在浏览器上访问9200端口,http://127.0.0.1:9200/,登录cpolar web ui管理界面,点击左侧仪表盘的隧道管理——隧道列表,找到mariaDB隧道,点击右侧的编辑。修改隧道信息,将保留成功的固定tcp地址配置到隧道中。

MySQL数据库索引以及使用唯一索引实现幂等性
一次和多次请求某一个资源对于资源本身应该具有同样的结果任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

PVE 下虚拟机 Ubuntu 无法进入恢复模式的解决方案——提取原有系统文件
问题说明 某天重启虚拟机 Ubuntu,发现虚拟机只有容器IP,桥接的接口在虚拟机显示状态为 DOWN: 想重启进入恢复模式,却发现恢复模式一直花屏,无法使用: 没有办法了,只能想办法提取原有系统内原有文件。 解决方案 定位虚拟机编号: 找到虚拟机主硬盘: SSH 登录宿主机,执行以下命令 ls -

Nginx 核心配置文 nginx.conf介绍
文章浏览阅读38次。我们都知道浏览器中可以显示的内容有HTML、XML、GIF等种类繁多的文件、媒体等资源,浏览器为了区分这些资源,就需要使用MIME Type。所以说MIME Type是网络资源的媒体类型。Nginx作为web服务器,也需要能够识别前端请求的资源类型。在Nginx的配置文件中,默认有两行配置:用来配置Nginx响应前端请求默认的MIME类型。语法默认值位置在default_type之前还有一句。

如何快速本地搭建悟空CRM结合内网穿透工具高效远程办公
如何快速本地搭建悟空CRM结合内网穿透工具高效远程办公。

Kafka 集群如何实现数据同步?
哈喽大家好,我是咸鱼 最近这段时间比较忙,将近一周没更新文章,再不更新我那为数不多的粉丝量就要库库往下掉了 T﹏T 刚好最近在学 Kafka,于是决定写篇跟 Kafka 相关的文章(文中有不对的地方欢迎大家指出) 考虑到有些小伙伴可能是第一次接触 Kafka ,所以先简单介绍一下什么是 Kafka

使用 Hexo 搭建个人博客并部署到云服务器
目录1 整体流程2. 本地环境准备2.1 安装 Node.js 和 Git2.2 安装 Hexo3. 服务端环境准备3.1 Nginx 环境配置3.1.1 安装 Nginx3.1.2 更改 Nginx 配置文件3.2 Node.js 环境配置3.3 Git 环境配置3.3.1 安装 Git3.3.2

过滤器模式 rust和java的实现
文章浏览阅读301次。我们将创建一个 Person 对象、Criteria 接口和实现了该接口的实体类,来过滤 Person 对象的列表。我们制作一个Person实体类,Criteria为标准条件,CriteriaMale等为实现的具体判断器,是需要为person类使用meetCriteria方法便可以进行不同条件的判断。我们制作一个Person实体类,Criteria为标准条件,CriteriaMale等为实现的具体判断器,是需要为person类使用meetCriteria方法便可以进行不同条件的判断。

python最流行的适合计算积分和微分方程的库
SciPy有一个子模块scipy.integrate,包含多种数值积分方法,如牛顿哥特法(quad)、梯形法(trapz)、辛普森法(simps)等