C++:深入理解C++11新特性:Chapter3:左值和右值

news/2024/7/24 13:51:07/

Chapter3:左值和右值

  • 1. 将右值绑定到 左值
  • 2. 将右值绑定到 常量左值引用
  • 3. 将右值绑定到右值引用
  • 总结:
  • 5. 左值,右值和右值引用
  • 6. 引用类型可以引用的的值类型
  • 7. 全能类型,常量左值引用用途
    • 7.1 拷贝构造函数
    • 7.2解决浅拷贝(深拷贝)
  • 8. 解决深拷贝问题

在C语言中,我们常常会提起左值(lvalue),右值(rvalue)这样的称呼,而在编译程序时,编译器有时也会报出错误信息中包含 左值,右值说法。不过左值、右值通常不是通过一个严谨的定义而为人所知。下面我通过这样一个例子,来引导大家认识: 左值,右值,左值引用,右值引用,常量左值引用

#include<iostream>struct Copyable{Copyable() {std::cout<< "copied...." << std::endl;}Copyable(const Copyable &copy){std::cout<< "copied" << std::endl;}
};Copyable ReturnRvalue()
{// 这是返回的 右值  return Copyable();
}// 1. 接收右值表达式
void AcceptValue(Copyable copy)
{}// 2. 右值引用减少对象开销,并延迟对象生命周期
//  直观意义:为临时变量续命,也就是为右值续命,因为右值在表达式结束后就消亡了,
// 如果想继续使用右值,那就会动用昂贵的拷贝构造函数。
void AcceptRef(Copyable && copy)
{}// 3. 常量左值引用减少对象开销,并延迟对象生命周期
void AcceptRef_2(const Copyable& copy){}int main()
{Copyable copy;std::cout << "Pass by value:" << std::endl;AcceptValue(ReturnRvalue());std::cout << "Passs by reference: " << std::endl;AcceptRef(ReturnRvalue());std::cout << "Passs by reference_2: " << std::endl;AcceptRef(ReturnRvalue());
}
// 打印结果: g++ -std=c++11 main.cpp -fno-elide-constructors// Copyable copy
construct.....       Pass by value:// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,一次拷贝函数作为AcceptValue函数实参
construct.....     
copied construct
copied constructPasss by reference:
// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,由于是引用传递,那么直接将此返回值作为AcceptRef函数实参
construct.....
copied construct// ReturnRvalue() 函数:调用一次构造函数构造Copyable ,一次拷贝构造函数作为ReturnRvalue函数返回值,由于是引用传递,那么直接将此返回值作为AcceptRef函数实参
Passs by reference_2:
construct.....
copied construct

上面的例子:我们用到了

  1. 函数形参为左值,然后将右值表达式作为实参绑定到左值
  2. 函数形参为右值引用,然后将右值作为实参绑定右值引用
  3. 函数形参为常量左值引用,然后将右值作为实参绑定到 常量左值引用

1. 将右值绑定到 左值

在这里插入图片描述
💚💚 这种绑定方式的特点:

  1. 函数 ReturnRvalue() 在运行结束后,返回值(右值临时变量)复制一份作为实参传递给 AcceptValue() 函数后就不会存活下去了。
    这样会导致:Copyable 这个对象构建两次。浪费内存。

2. 将右值绑定到 常量左值引用

在这里插入图片描述

💚💚

  1. AcceptRef使用了引用传递,在 以 ReturnRvalue 返回的右值为参数的时候,AcceptRef 就可以直接使用产生的临时值(并延长其生命周期)。
  2. 这个临时值的生命周期就和 AcceptRef() 生命周期一致

3. 将右值绑定到右值引用

在这里插入图片描述
💚💚

  1. AcceptRef使用了引用传递,在 以 ReturnRvalue 返回的右值为参数的时候,AcceptRef 就可以直接使用产生的临时值(并延长其生命周期)。
  2. 这个临时值的生命周期就和 AcceptRef() 生命周期一致。

总结:

🧡 通过上面三个小节,我们总结了 将右值绑定到 :左值,常量左值引用,右值引用的情况,下面以这个为例子,分析左值、右值含义,通常情况下,哪些是左值哪些是右值。
在这里插入图片描述

5. 左值,右值和右值引用

在这里插入图片描述

6. 引用类型可以引用的的值类型

在这里插入图片描述

在这里插入图片描述

7. 全能类型,常量左值引用用途

7.1 拷贝构造函数

对C++程序员来说,编写C++程序有一条必须注意的规则,就是在类中红包含了一个指针成员的话,那么就要特别注意 拷贝函数的编写,因为一不小心,就会出现内存泄漏。

#include<iostream>
using namespace std;class HasPtrMem {
public:// 默认构造函数HasPtrMem():d(new int(0)) {}~HasPtrMem(){delete d;}int *d;};int main()
{HasPtrMem a;HasPtrMem b(a);cout<< *a.d << endl;cout<< *b.d << endl;
}
// 打印结果
free(): double free detected

在这里插入图片描述
💚💚 根据上图我们作如下分析

  1. 由于没有提供拷贝函数,C++会默认提供一个拷贝函数 (这个编译器源码编译后可以看出)
  2. 默认的拷贝函数类似于 memcpy按位拷贝,这样会造成一个问题:a.d 和 b.d 都指向一个内存地址
  3. 那么当main函数结束后,a和b 对象纷纷调用析构函数,当对象a 析构完毕之后,b.d 就变成了一个悬挂指针,不能指向有效的内存地址
    如果在不小心的情况下,对此指针做解引用,那么就势必会引起严重的错误。
    这个问题在C++中非常经典,这样的拷贝构造方式在C++上被称为 “浅拷贝” ,在位声明定义拷贝构造函数的情况下,C++会为每个类生成一个 浅拷贝构造函数。

💚💚 解决浅拷贝带来的问题 :自定义拷贝函数,实现深拷贝
我们为 HasPtrMem添加一个拷贝构造函数,拷贝构造函数从堆中分配内存,并将分配的新内存交还给 d,又用 *(h.d) 进行初始化,通过这样的方法很好的避免了 悬挂指针的困扰
在这里插入图片描述

7.2解决浅拷贝(深拷贝)

在章节7.1 中,拷贝构造函数为指针成员分配新的内存再进行内容拷贝的做法在C++编程中是不可违背的。
但是这个里面会有一个问题,这个问题就是:临时对象a 以及 对象的成员 int* d 指针指向的堆内容,但是又没有使用到,这就是一种浪费。
看下面这个例子

#include<iostream>
using namespace std;
class HasPtrMem {
public:HasPtrMem():d(new int(0)){cout<< "Construct: "<< ++ n_cstr<< endl;}HasPtrMem(const HasPtrMem& h):d(new int(*h.d)){cout<< "copy construct: " << ++n_cptr << endl;}~HasPtrMem(){cout<< "Destruct: " << ++ n_dstr<< endl;}int *d;static int n_cstr;static int n_cptr;static int n_dstr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr= 0;
int HasPtrMem::n_cptr= 0;
HasPtrMem GetTemp() {return HasPtrMem();}int main()
{HasPtrMem a = GetTemp();
}
// 编译选项  g++ -std=c++11 main_3.18.cpp -fno-elide-constructors
Construct: 1
copy construct: 1
Destruct: 1
copy construct: 2
Destruct: 2
Destruct: 3

💚💚 根据上面的打印可知:HasPtrMem 类构造函数调用了一次,拷贝构造函数调用了两次。析构函数调用了3次,为什么会出现这个情况了 。下面,我通过一张图来说明。
在这里插入图片描述

上图显示:GetTemp() 函数需要经历 两次拷贝函数,才可以让 对象 a 使用,由于拷贝函数进行了深拷贝,虽然解决了 指针悬挂问题,但是拷贝函数会 新建 堆内存(new int()) , 这是非常昂贵了。

  1. 可以想象一下如果类 HasPtrMem 成员 的指针指向非常大的堆内存,那么拷贝构造的过程就会非常昂贵。
  2. 更加令人堪忧的是,临时变量的产生和销毁对程序员是不可见的,并不会影响程序的运行,即使是性能有所下降,也不容易察觉。

8. 解决深拷贝问题

在第七节中,我们知道深拷贝带来临时对象复制,如果 一个类中存在指针变量,且指向非常大内存,那么在拷贝过程中必然会耗费内存,如果解决这个问题了,C++11 引入了右值和 move 语义,可以极大的提高性能,详细分析见我的下一篇文章。
move语义


http://www.ppmy.cn/news/70458.html

相关文章

chatGPT生成的:前端学习导航

MDN Web 文档&#xff1a;提供关于 HTML、CSS、JavaScript 等前端技术的详细文档和指南。W3Schools&#xff1a;提供在线教程&#xff0c;覆盖了 HTML、CSS、JavaScript 和其他前端技术的基础知识。freeCodeCamp&#xff1a;一个开源的学习平台&#xff0c;提供免费的编程课程和…

红黑树(RBTree)

红黑树的基本性质 &#xff08;1&#xff09;红黑树是每个结点都带有颜色属性的二叉查找树&#xff0c;颜色或红色或黑色。在二叉搜索树强制一般要求以外&#xff0c;对于任何有效的红黑树我们增加了如下的额外要求: 性质1. 结点是红色或黑色。 性质2. 根结点是黑色。 性质…

包管理工具:pnpm | 京东云技术团队

作者&#xff1a;京东零售 杨秀竹 pnpm 是什么 pnpm&#xff08; performant npm &#xff09;指的是高性能的 npm&#xff0c;与 npm 和 yarn 一样是一款包管理工具&#xff0c;其根据自身独特的包管理方法解决了 npm、yarn 内部潜在的安全及性能问题&#xff0c;在多数情况…

SQL 常用函数总结(二)

字符串处理函数 1. CONCAT() 函数功能&#xff1a;将两个或多个字符串合并成一个字符串。 函数语法&#xff1a; CONCAT(string1, string2, ...)string1、string2 等的数量可以是零个或多个&#xff0c;分别表示需要合并的字符串。 使用示例&#xff1a; 假设现在有一个名…

软件测试工程师的职业发展方向

一、软件测试工程师大致有4个发展方向: 1 资深软件测试工程师 一般情况&#xff0c;软件测试工程师可分为测试工程师、高级测试工程师和资深测试工程师三个等级。 达到这个水平比较困难&#xff0c;这需要了解很多知识&#xff0c;例如C语言&#xff0c;JAVA语言&#xff0c…

多尺度样本熵

多尺度样本熵及其MATLAB实现方法 随着人们对信号处理技术的不断深入研究和发展&#xff0c;在信号非线性、非高斯的情况下&#xff0c;熵的概念成为一种很重要的测量信号复杂度的度量方式。多尺度熵是指在多个尺度范围内测量信号复杂度的一种方法。本文将介绍多尺度样本熵的概…

什么是前端宏任务,什么又是前端微任务呢?一文读懂前端微任务宏任务。

在前端中&#xff0c;宏任务和微任务是异步任务的两种不同类型。 前端有很多中异步任务类型。 可以分为三类&#xff1a; 宏任务 定时器任务用户交互事件任务&#xff08;鼠标事件、键盘事件&#xff09;网络请求任务I/O操作任务&#xff08;读写文件&#xff09; 微任务 Pro…

TPC 网络通信基础(二)

文件下载利用 tcp原理 Ubuntu 20.04 python3.7 三个python文件 客户端.py 服务器.py 文件.py 客户端充当用户 服务器充当提供下载的服务端 客户端代码&#xff1a; import socketdef main():# 创建套接字tcp_socket socket.socket(socket.AF_INET,socket.SOCKET_…

String StringBuilder常用方法总结

在java中String类不可变的&#xff0c;创建一个String对象后不能更改它的值。所以如果需要对原字符串进行一些改动操作&#xff0c;就需要用StringBuilder类或者StringBuffer类&#xff0c;StringBuilder比StringBuffer更快一些&#xff0c;缺点是StringBuilder不是线程安全的&…

JAVA面试-语法基础- A01

语法基础 面向对象封装继承多态 面向对象 面向对象特性 封装 利用抽象数据类型将数据和基于数据的操作封装在一起&#xff0c;使其构成一个不可分隔的独立实体&#xff0c;数据被保护在抽象数据类型的内部&#xff0c;尽可能的隐藏内部的细节&#xff0c;只保留一些对外的接口…

前端通信-服务端发送事件: SSE(Server-Sent Events)

在日常开发中&#xff0c;我们经常会遇到需要实时获取数据的情况&#xff0c;之前实现这种相似的功能通常都是用ajax长轮询&#xff0c;在HTML5规范中定义了新的通信方式&#xff0c;WebSocket和SSE。websocket相对SSE更常用一些&#xff0c;本文着重来介绍SSE的应用。 SSE AP…

滨州申请专利需要准备哪些材料?

如果你想保护你的一些发明和设计&#xff0c;你可以申请专利。申请专利时&#xff0c;需要提前了解程序和相关流程。那么&#xff0c;申请专利需要准备哪些材料呢&#xff1f;让我们一起仔细看看。 首先&#xff0c;申请专利需要准备哪些材料&#xff1f; (1)外观专利&#xff…

C++学习 Day14

目录 1. 泛型编程 2. 函数模板 2.1 函数模板概念 2.2 函数模板格式 2.3 函数模板的原理 2.4 函数模板的实例化 2.5 模板参数的匹配原则 3. 类模板 3.1 类模板的定义格式 3.2 类模板的实例化 1. 泛型编程 如何实现一个通用的交换函数呢&#xff1f; void Swap(int&a…

德尔玛IPO首日破发,市值蒸发超4亿

今日&#xff08;5月18日&#xff09;&#xff0c;小米“代工厂”广东德尔玛科技股份有限公司&#xff08;下称“德尔玛”&#xff0c;301332.SZ&#xff09;正式在深交所挂牌上市。 德尔玛此次IPO募资净额为12.31亿元&#xff0c;开盘价为14.81元/股&#xff0c;与发行价持平…

【Linux内核解析-linux-5.14.10】文件系统知识点以及解答(建议收藏)

什么是Linux文件系统&#xff1f; 答&#xff1a;Linux文件系统是一种用于管理和组织计算机上数据的方法。它提供了一个层次结构&#xff0c;使用户能够轻松地访问他们的数据&#xff0c;并且支持对数据进行备份、恢复和保护。 Linux中有哪些常见的文件系统类型&#xff1f; 答…

使用 SpringBoot 访问 MySQL 数据库

一、目标 创建一个 MySQL 数据库&#xff0c;构建一个 Spring 应用程序&#xff0c;并将其连接到新创建的数据库。 二、准备工作 1、最喜欢的文本编辑器或 IDE 2、Java 17或更高版本 3、Gradle 7.5或Maven 3.5 三、初始化项目 1、 导航到https://start.spring.io。该服务…

HTTPTomcatServlet学习

HTTP&Tomcat 今日目标&#xff1a; 了解JavaWeb开发的技术栈理解HTTP协议和HTTP请求与响应数据的格式掌握Tomcat的使用掌握在IDEA中使用Tomcat插件理解Servlet的执行流程和生命周期掌握Servlet的使用和相关配置 1. Web概述 1.1 Web和JavaWeb的概念 Web是全球广域网&…

freeswitch的2833和inband对接方案

概述 freeswitch支持三种模式的DTMF传输方式&#xff0c;分别时inband、INFO、2833。 在传统的PSTN网络中&#xff0c;所有的DTMF码都是inband模式&#xff0c;所以VOIP网络和PSTN网络对接中&#xff0c;需要将DTMF码做格式转换&#xff0c;通常是2833和inband之间的转换。 …

Makefile 简易教程

如果你是命令行重度使用者&#xff0c;学习 Makefile 将可以大大提高你的开发效率&#xff0c;下面简单介绍一下 Makefile 的知识和使用方式。 Makefile 是一种包含一组指令来编译和构建软件项目的文件。 Makefile 文件通常包含一组规则和依赖关系&#xff0c;以指定如何将源…

Netty核心组件模块(一)

1.Bootstrap和ServerBootstrap 1>.Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类; 2>.常见的方法有: ①.public ServerBootstr…