想要抢先看后面的章节?打赏本文10元,即可获得带插图全本下载地址!
打赏完成记得私信我哦 :p

第12章 C++世界的几件新鲜事

“卖报啦卖报啦——”

“C++世界的新标准C++11新鲜出炉,大家都来看看C++世界的新鲜事啊!”

“右值引用心狠手辣想榨干C++的性能,智能指针聪明伶俐想解决C++内存管理的问题, <thread>正在做慈善、大量派发免费午餐啊……”

12.1 用右值引用榨干C++的性能

在性能方面,除了汇编语言和C语言外,C++大概是这个世界上公认的性能最佳的程序设计语言了。但是,C++标准委员会的那帮“老头们”并不就此感到满足,在C++的最新标准C++11中,他们添加了心狠手辣的右值引用这一新特性,想要榨干C++身上的最后一点性能。

12.1.1 什么是右值引用

在前面的程序中,程序中的数据要么表现为一个常量,比如具体的某个数字、某个字符串等,要么表现为某个特定类型的变量,可从来没听说过右值。难道数值也分左右?这里的右值是什么东西?右值引用又是什么?没错,数值也分左右。在C++语言中,按照能否放在赋值操作符“=”的左边或者右边,数值或变量被分成左值或者右值。通常,我们将那些能够放在等号左边的数值称为左值,比如某个变量,它既可以放在等号左边被赋值,同时也可以放在等号右边给另外的左值赋值。相对地,我们将那些只能放在等号右边的数值称为右值。比如某个数字常量,就只能放在等号右边给其它左值赋值而无法放在等号左边被赋值。C++中的右值主要指的是数字常量(例如,1、2.3等)和匿名变量(例如,函数的返回值变量、构造函数的返回的对象等)。而右值引用,顾名思义,也就是与这些右值相关联的引用了。在C++中,可以通过将“&&”符号放到数据类型的后面来定义一个相应类型的右值引用,相对应地,原来只使用一个“&”符号定义的引用被称为左值引用,简称引用。例如:

// 定义一个int类型的变量,这个变量可以放在等号左边被赋值,
// 所以是一个左值
int nInt = 1;
// 定义一个左值引用,将它指向一个左值
int& lrefInt = nInt;
// 定义一个右值引用,将它指向一个直接使用构造函数创建的右值int(0)int&& rrefInt =int(0);
// 显然我们无法将它放在等号左边对它赋值int(0) = 1; // 错误

我们知道,引用只是变量的一个“绰号”而已,它并不是独立存在的,而总是要跟某个具体的变量相关联。这里需要注意的是,左值引用和右值引用对变量的关联是一一对应专属的,换句话说,也就是左值引用只能关联到左值,而右值引用也只能关联到右值,否则,将会产生编译错误。例如:

// 正确:左值引用lrefInt1关联到左值变量nInt
int& lrefInt1 = nInt;
// 错误:左值引用lrefInt2不可以关联到右值int(0)int& lrefInt2 = int(0);
// 正确:右值引用rrefInt1关联到右值int(0)
int&& rrefInt1 = int(0);
// 错误:右值引用rrefInt2不可以关联到左值nInt
int&& rrefInt2 = nInt;

关联完成之后,左值引用和右值引用的使用就没有什么区别了,它们都可以当成普通数据变量进行左右值的操作。例如:

// 对右值引用rrefInt1赋值
rrefInt1 = 1;
// 利用右值引用对左值引用赋值
lrefInt1 = rrefInt1;

12.1.2 右值引用是如何提高性能的?

在C++中,最常见的右值就是函数(包括普通函数和构造函数)的返回值。当一个函数调用完成后,这些没有变量名的返回值通常会被赋值给等号左边的一个左值变量,在没有右值引用的时代,这其实是一个极其消耗性能浪费资源的过程。首先,需要释放左值变量原有的内存资源,然后根据返回值的大小重新申请内存资源,接着才是将返回值的数据复制到左值变量新申请的内存中,最后还要释放掉返回值的内存资源。经过这样四个步骤,才能最终完成一个函数返回值的赋值操作。繁琐的过程会消耗性能,只用做中间过渡的返回值会浪费资源。下面,我们来看一个实际的例子,用CreateBlock()函数创建一个用于管理内存的MemoryBlock对象,并将其保存到另外一个MemoryBlock类型变量中:

#include <iostream>
#include <cstring> // 为了使用内存复制函数memcpy()
using namespace std;// 用于管理内存的类
class MemoryBlock
{
public:// 构造函数,根据参数申请相应大小的内存资源MemoryBlock(const unsigned int nSize){cout<<"创建对象,申请内存资源"<<nSize<<"字节"<<endl;m_nSize = nSize; m_pData = new char[nSize];}// 析构函数,释放管理的内存资源~MemoryBlock(){cout<<"销毁对象";if(0 != m_nSize) // 如果拥有内存资源{cout<<",释放内存资源"<<m_nSize<<"字节";delete[] m_pData; // 释放内存资源m_nSize = 0; }cout<<endl;}// 赋值操作符,完成对象的复制// 这里的参数是一个左值引用MemoryBlock& operator = (const MemoryBlock& other){// 判断是否自己给自己赋值if(this == &other)return *this;// 第一步,释放已有内存资源cout<<"释放已有内存资源"<<m_nSize<<"字节"<<endl;delete[] m_pData;// 第二步,根据赋值对象的大小重新申请内存资源m_nSize = other.GetSize();cout<<"重新申请内存资源"<<m_nSize<<"字节"<<endl;m_pData = new char[m_nSize];// 第三步,复制数据cout<<"复制数据"<<m_nSize<<"字节"<<endl;memcpy(m_pData,other.GetData(),m_nSize);return *this;}
public:// 获取相关数据的成员函数unsigned int GetSize() const{return m_nSize;}char* GetData() const{return m_pData;}
private:
unsigned int m_nSize; // 内存块的大小char* m_pData; // 指向内存块的指针
};// 根据大小创建相应的MemoryBlock对象
MemoryBlock CreateBlock(const unsigned int nSize)
{// 创建相应大小的对象MemoryBlock mem(nSize);// 给内存中填满字符'A'char* p = mem.GetData();memset(mem.GetData(),'A',mem.GetSize());//p[0] = 'A';// 返回创建的对象return mem;
}
int main()
{// 用于保存函数返回值的block变量MemoryBlock block(256);// 用函数创建特定大小的MemoryBlock对象// 并赋值给block变量block = CreateBlock(1024);cout<<"创建的对象大小是"<<block.GetSize()<<"字节"<<endl;return 0;
}

在这段代码中,我们利用CreateBlock()函数创建了一个特定大小的MemoryBlock对象并将其保存到本地的一个block变量中。从表面上看,这是一个非常简单的动作,可是在背后,程序是经过千辛万苦的四个步骤才完成了这一“简单”动作。从程序的输出中,我们可以清楚地看到这四个步骤:

创建对象,申请内存资源256字节

创建对象,申请内存资源1024字节

释放已有内存资源256字节 ß第一步

重新申请内存资源1024字节 ß第二步

复制数据1024字节 ß第三步

销毁对象,释放内存资源1024字节 ß第四步

创建的对象大小是1024字节

销毁对象,释放内存资源1024字节

一个保存函数返回值的简单动作都需要这么复杂的四个步骤,再加上这类动作在C++程序中非常常见,更要命的是,这些步骤都是非常耗时的跟内存操作相关的(申请、复制、释放),这样一来,保存函数返回值的动作其性能自然高不起来。正是看到了这个性能缺陷,C++11提供了右值引用来解决这个问题。我们知道,函数的返回值其实是一个右值,通过在MemoryBlock类当中提供可以接受其右值引用为参数的移动构造函数和赋值操作符,我们就可以直接利用函数返回值这个右值作为我们的block变量:

// …
// 用于管理内存的类
class MemoryBlock
{
// …
public:
// 可以接收右值引用为参数的移动构造函数
MemoryBlock(MemoryBlock&& other){ cout<<"移动资源"<<other.m_nSize<<"字节"<<endl;// 将目标对象的内存资源指针直接指向源对象的内存资源// 表示将源对象内存资源的管理权移交给目标对象
m_pData = other.m_pData;m_nSize = other.m_nSize; // 复制相应的内存块大小// 将源对象的内存资源指针设置为nullptr
// 表示这块内存资源已经归目标对象所有
// 源对象不再拥有其管理权
other.m_pData = nullptr;other.m_nSize = 0; // 内存块大小设置为0}// 可以接收右值引用为参数的赋值操作符
MemoryBlock& operator = (MemoryBlock&& other){// 第一步,释放已有内存资源
cout<<"释放已有资源"<<m_nSize<<"字节"<<endl;delete[] m_pData;// 第二步,移动资源,也就是移交内存资源的管理权
cout<<"移动资源"<<other.m_nSize<<"字节"<<endl;m_pData = other.m_pData;m_nSize = other.m_nSize;other.m_pData = nullptr;other.m_nSize = 0;return *this;}
// …
}

从代码中我们可以看到,这里的移动构造函数和赋值操作符都以一个右值引用为参数。这就意味着这个参数所关联的右值对象在函数调用完成后就会被销毁,其管理的内存资源也会被释放。既然这个右值对象即将被销毁,而我们同时又要创建或者复制一个与之完全相同的对象,那么很自然地,我们会想到“废物再利用”,直接用这个即将被销毁的右值对象作为我们想要创建或复制的目标对象。内存资源依旧是那块内存资源,只不过其管理者由原来的作为参数的右值对象,换成了我们想要创建或复制的目标对象。整个过程如下图12-1所示:

图12-1 从函数返回值赋值

在这个过程中,没有内存资源的重新申请,也没有数据的复制,完全就像一场和平友好的内存资源管理权移交仪式,只是简单地让目标对象的内存指针指向右值对象的内存资源,从而将内存资源的管理权从右值对象移交(move)到了目标对象,以这种“低碳环保”的方式轻松地完成了目标对象的创建或者赋值。也正因为如此,为了与传统的可以接收左值引用(&)为参数的构造函数和赋值操作符相区别,这里的可以接收右值引用(&&)为参数的构造函数也被称为移动构造函数,相应地赋值操作符也被称为移动赋值操作符。从程序的输出中,我们也可以看到这个“移交”过程:

创建对象,申请内存资源256字节

创建对象,申请内存资源1024字节

释放已有资源256字节 ß第一步,释放已有内存资源

移动资源1024字节 ß第二步,移交内存资源的管理权

销毁对象

创建的对象大小是1024字节

销毁对象,释放内存资源1024字节

由此可见,为类提供可以接收右值引用为参数的移动构造函数和移动赋值操作符,是提高保存复制函数返回值这一常见动作性能的一个有效途径。对于标准库中的内容而言,比如vector容器,已经使用右值引用进行了改写,我们直接使用坐享其成就行了。对于我们自己创建的类,如果它会作为函数返回值被赋值给另外的左值对象时,为它提供移动构造函数和移动赋值操作符,则可以将函数返回值这个原先被“废弃”的右值再次利用起来,在一定程度上提高程序的性能,同时也是为创建“节约型社会”做出程序员的一点贡献。

错误未找到引用源_你好,C++(77)12.1 用右值引用榨干C++的性能相关推荐

  1. 的引用_左值、右值、左值引用、右值引用

    [导读]:本文主要详细介绍了左值.右值.左值引用.右值引用以及move.完美转发. 左值和右值 左值(left-values),缩写:lvalues 右值(right-values),缩写:rvalu ...

  2. java左值与右值问题_[C++11]左值、右值、左值引用、右值引用小结

    左值和右值 左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字. 比如 int a = b + c;,a 就是一个左值, ...

  3. [C++11新特性](24)列表初始化,右值引用,可变参数模板,lambda表达式,包装器

    文章目录 列表初始化 {}初始化 initializer_list auto.nullptr.范围for decltype STL的变化 右值引用 简介 移动构造与移动赋值 完美转发 新的类功能 可变 ...

  4. C++右值引用与转移和完美转发

    C++右值引用与转移和完美转发 1.右值引用 1.1右值 lvalue 是 loactor value 的缩写,rvalue 是 read value 的缩写 左值是指存储在内存中.有明确存储地址(可 ...

  5. C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现

    目录 一.右值引用 (一).何为右值 (二).右值引用 (三).右值和左值的互相传递 ①左值->右值引用 ②右值->左值引用 (四).右值引用的自身属性 二.移动构造和移动赋值 (一).移 ...

  6. (P36-P39)右值和右值引用、右值引用的作用以及使用、未定引用类型的推导、右值引用的传递

    文章目录 1.右值 2. 右值引用 3.性能优化 4.&& 的特性 5.右值引用的传递 1.右值 C++11 增加了一个新的类型,称为右值引用( R-value reference), ...

  7. C++ 对象移动(右值引用()、移动构造函数、移动赋值运算符、引用限定函数)

    原文:对象移动(右值引用(&&).移动构造函数.移动赋值运算符.引用限定函数) 一.对象移动概述 C++11标准引入了"对象移动"的概念 对象移动的特性是:可以移动 ...

  8. 移动语义-右值引用-完美转发-万字长文让你一探究竟

    C++ 右值引用 block://6984617523950616580?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38 ...

  9. c++右值引用以及使用

    前几天看了一篇文章<4行代码看看右值引用> 觉得写得不错,但是觉得右值引用的内容还有很多可以去挖掘学习,所以总结了一下,希望能对右值引用有一个更加深层次的认识 一.几个基本概念 1.1左值 ...

  10. C++左值、右值、左值引用、右值引用

    左值(lvalue)和右值(rvalue) 左值(lvalue):locator value,存储在内存中.有明确的的地址(可寻址)的数据 能够取地址,有名字的值就是左值 //左值引用 int a=1 ...

最新文章

  1. Python--协程(gevent模块)
  2. 成交量与股价关系的深度剖析 (一)
  3. HibernateEHCache –Hibernate二级缓存
  4. 我的 CSDN 两周年创作纪念日
  5. 计算机类学术期刊SCI/EI期刊核心期刊有哪些?
  6. 蜗牛星际安装U-NAS
  7. mybatisplus的逻辑删除
  8. Excel:文本数字转换成数字的三种方法(转)
  9. EF中的Guid主键
  10. Rest-assured使用
  11. Java 根据贷款年限对应的不同利率计算月供
  12. 【附源码】计算机毕业设计java中小学在线考试系统设计与实现
  13. 电信专家王煜全:手机监管面临三大困境
  14. 【Excel2019(十七):数学函数】【Round函数+Roundup函数+Rounddown函数+Int函数+Mod函数】
  15. JavaScript高级程序设计(第4版)学习随笔【第三章】
  16. C语言中sort函数的用处
  17. 如何设计好一条推送通知
  18. (六) 数据结构 - 快速排序
  19. 4G的想象:移动视频行业将迎来大爆发
  20. shell脚本语法基础汇总

热门文章

  1. AS中几个较好的插件
  2. Oracle数据库性能优化的艺术pdf
  3. POJ - 3250 Bad Hair Day 单调栈
  4. js判断是否是正整数,js判断是否是数字
  5. 【学堂在线数据挖掘:理论方法笔记】第l六天(3.31)
  6. 智能优化算法:磷虾群算法-附代码
  7. 【LeetCode】【字符串】题号:*551. 学生出勤记录 I
  8. ENVI入门系列教程---一、数据预处理---4.2 自动采集控制点的RPC正射校正
  9. 《剑指offer》面试题60——把二叉树打印成多行(C++)
  10. 专题一——递归与递推