C++ 移动语义

news2024/11/20 12:44:34

从拷贝说起

我们知道,C++中有拷贝构造函数和拷贝赋值运算符。那既然是拷贝,听上去就是开销很大的操作。没错,所谓拷贝,就是申请一块新的内存空间,然后将数据复制到新的内存空间中。如果一个对象中都是一些基本类型的数据的话,由于数据量很小,那执行拷贝操作没啥毛病。但如果对象中涉及其他对象或指针数据的话,那么执行拷贝操作就可能会是一个很耗时的过程。

我们来看一个例子,该类中有一个string类型的成员函数,定义如下:

class MyClass
{
 public:
       MyClass(const std::string& s) :str(s)
       {
       };
 private:
     std::string str;
};

MyClass A{"hello"};

当我们新建一个该类的对象A,并传递参数“hello”时,对象A的成员变量str中会存储字符“hello”。而为了存储字符串,string类型会为其分配内存空间。因此,当前内存中的数据如图所示:

现在,当我们定义一个该类的新对象B,且把对象A赋值给对象B时,会发生什么?即,我们执行如下的语句:

MyClass B = A;

当拷贝发生时,为了让B对象中的成员变量str也能够存储字符串“hello”,string类型会为其分配内存空间,并将对象A的str中的数据复制过来,因此,经过拷贝操作后,此时内存中的数据如图所示:

需要移动语义的情况

既然拷贝操作没毛病,那么为什么要新增移动语义呢。因为在一些情况下,我们可能确实不需要拷贝操作,下面的例子。

class MyClass
{
  public:
      MyClass(const std::string& s):str(s)
      {
         
      };
  private:
      std:string str;
}
std:vector<MyClass> myclasses;
MyClass tmp{"hello"};
myclasses.push_pack(tmp);
myclasses.push_pack(tmp);

在上面的例子中,我们创建了一个容器以及一个MyClass对象tmp,我们将tmp对象添加到容器中2次,每次添加时,都会发生一次拷贝操作,最终内存中的数据如图所示:

现在问题来了,tmp对象在被添加到容器2次后,就不需要了,也就是说,它的生命周期即将结束,那么聪明的你一定想到,既然tmp对象不在需要了,那么将第2次将其添加到容器中的操作是不是就可以不执行拷贝操作了,而是让容器直接取tmp对象的数据继续用,没错,这时,就需要移动语义帅气登场了。

移动语义

所谓移动语义,就像其字面意思一样,即把数据从一个对象中转移到另一个对象中,从而避免拷贝操作所带来的性能损耗。

那么在上面的例子中,我们如何触发移动语义呢?很简单,我们只需要使用std::move函数即可。有关std::move函数,就是另一个话题了,这里我们不深入探讨。我们只需要知道,通过std::move函数,我们可以告知编译器,某个对象不再需要了,可以把它的数据转移给其他需要的对象用。

class Myclass
{
   public:
          MyClass(const std::string& s):str(s)
          {
             
          };
    //假设已经实现了移动语义
   private:
       std::string str;
}
std:vector<MyClass> myclasses;
MyClass tmp{"hello"};
myclasses.push_pack(tmp);
myclasses.push_pack(std::move(tmp));

由于我们还没讲到移动语义的实现,因此这里先假设MyClass类已经实现了移动语义。我们改动的是最后一行代码,由于我们不再需要tmp对象,因此通过使用std::move函数,我们让myClasses容器直接转移tmp对象的数据为已用,而不再需要执行拷贝操作了。

通过数据转移,我们避免了一次拷贝操作,最终内存中的数据如图所示:

至此,我们可以了解到,C++11引入移动语义可以在不需要拷贝函数操作的场合执行数据转移,从而极大的提升程序的运行性能。

左值引用与右值引用

在学习如何实现移动语义之前,我们需要先了解2个概念,即左值引用与右值引用。

为了支持移动语义,C++11引入了一种新的引用类型,称为“右值引用”,使用&&来声明。而我们最常用的&声明的引用,现在我们称为左值引用。

右值引用能够引用没有名称的临时对象以及使用std::move标记的对象

int val{0};
int && rRef0{ getTempValue()}; // ok 引用临时对象
int && rRef1{val};     //Error,不能引用左值
int&& rRef2{ std::move(val) };  // OK,引用使用std::move标记的对象

移动语义的实现需要用到右值引用。以下2中情况会让编译器将对象匹配右值引用:

1:一个语句执行完毕后会被自动销毁的临时对象。

2:由std::move标记的非const对象

区分拷贝操作与移动操作

我们回到上文的例子,对于myClasses容器的第一次push_back,我们期望执行的是拷贝操作,而对于myClasses容器的第二次push_back,由于之后我们不再需要tmp对象了,因此我们期望执行的是移动操作:


class MyClass
{
public:
    MyClass(const std::string& s)
        : str{ s }
    {};

    // 假设已经实现了移动语义

private:
    std::string str;
};

std::vector<MyClass> myClasses;
MyClass tmp{ "hello" };
myClasses.push_back(tmp);  // 这里执行拷贝操作,将tmp中的数据拷贝给容器中的元素
myClasses.push_back(std::move(tmp));  // 这里执行移动操作,容器中的元素直接将tmp的数据转移给自己

现在我们已经知道,移动操作执行的是对象数据的转移,那么它一定是与拷贝操作不一样的。因此,为了能够将拷贝操作与移动操作区分执行,就需要用到我们上一节的主题:左值引用与右值引用。

因此,对于容器的push_back函数来说,它一定针对拷贝操作和移动操作有不同的重载实现,而重载用到的即是左值引用与右值引用。伪代码如下:


class vector
{
public:
    void push_back(const MyClass& value)  // const MyClass& 左值引用
    {
        // 执行拷贝操作
    }

    void push_back(MyClass&& value)  // MyClass&& 右值引用
    {
        // 执行移动操作
    }
};

通过传递左值引用或右值引用,我们就能够根据需要调用不同的push_back重载函数了。那么下一个问题来了,我们知道std::vector是模板类,可以用于任意类型。所以,std::vector不可能自己去实现拷贝操作或移动操作,因为它不知道自己会用在哪些类型上。因此,std::vector真正做的,是委托具体类型自己去执行拷贝操作与移动操作。

移动构造函数

当通过push_back向容器中添加一个新的元素时,如果是通过拷贝的方式,那么对应执行的会是容器元素类型的拷贝构造函数。关于拷贝构造函数,它是C++一直以来都包含的功能,相信大家已经很熟悉了,因此在这里就不展开了。

当通过push_back向容器中添加一个新的元素时,如果是通过移动的方式,那么对应执行的会是容器元素类型的“移动构造函数”(敲黑板,划重点)。

移动构造函数是C++11引入的一种新的构造函数,它接收右值引用。以我们前文的MyClass例子来说,为其定义移动构造函数:

class MyClass
{
public:
    // 移动构造函数
    MyClass(MyClass&& rValue) noexcept  // 关于noexcept我们稍后会介绍
        : str{ std::move(rValue.str) }  // 看这里,调用std::string类型的移动构造函数
    {}

    MyClass(const std::string& s)
        : str{ s }
    {}

private:
    std::string str;
};

在移动构造函数中,我们要做的就是转移成员数据。我们的MyClass有一个std::string类型的成员,该类型自身实现了移动语义,因此我们可以继续调用std::string类型的移动构造函数。

在有了移动构造函数之后,我们就可以在需要时通过它来创建新的对象,从而避免拷贝操作的开销。以如下代码为例:


MyClass tmp{ "hello" };
MyClass A{ std::move(tmp) };  // 调用移动构造函数

首先我们创建了一个tmp对象,接着我们通过tmp对象来创建A对象,此时传递给构造函数的参数为std::move(tmp)。还记得我们前文提及的编译器匹配右值引用的情况之一嘛,即由std::move标记的非const对象,因此编译器会调用执行移动构造函数,我们就完成了将tmp对象的数据转移到对象A上的操作:

自己手动实现移动语义

在前文的MyClass例子中,我们将移动操作交由std::string类型去完成。那如果我们的类有成员数据需要我们自己去实现数据转移的话,通常该怎么做呢?

我们来举个例子,假设我们定义的类型中包含了一个int类型的数据以及一个char*类型的指针:


class MyClass
{
public:
    MyClass()
        : val{ 998 }
    {
        name = new char[] { "Peter" };
    }

    ~MyClass()
  {
    if (nullptr != name)
    {
      delete[] name;
      name = nullptr;
    }
  }

private:
    int val;
    char* name;
};

MyClass A{};

当我们创建一个MyClass的对象时,它在内存的布局如图所示:

现在我们来为MyClass类型实现移动构造函数,代码如下所示:

class MyClass
{
public:
  MyClass()
    : val{ 998 }
  {
    name = new char[] { "Peter" };
  }

  // 实现移动构造函数
  MyClass(MyClass&& rValue) noexcept
    : val{ std::move(rValue.val) }  // 转移数据
  {
    rValue.val = 0;  // 清除被转移对象的数据

    name = rValue.name;  // 转移数据
    rValue.name = nullptr;  // 清除被转移对象的数据
  }

  ~MyClass()
  {
    if (nullptr != name)
    {
      delete[] name;
      name = nullptr;
    }
  }

private:
  int val;
  char* name;
};

MyClass A{};
MyClass B{ std::move(A) };  // 通过移动构造函数创建新对象B

还记得移动语义的精髓嘛?数据拿过来用就完事儿了。因此,在移动构造函数中,我们将传入对象A的数据转移给新创建的对象B。同时,还需要关注的重点在于,我们需要把传入对象A的数据清除,不然就会产生多个对象共享同一份数据的问题。被转移数据的对象会处于“有效但未定义(valid but unspecified)”的状态(后文会介绍)。

通过移动构造函数创建对象B之后,内存中的布局如图所示:

移动赋值运算符

与拷贝构造函数和拷贝赋值运算符一样,除了移动构造函数之外,C++11还引入了移动赋值运算符。移动赋值运算符也是接收右值引用,它的实现和移动构造函数基本一致。在移动赋值运算符中,我们也是从传入的对象中转移数据,并将该对象的数据清除:


class MyClass
{
public:
  MyClass()
    : val{ 998 }
  {
    name = new char[] { "Peter" };
  }

  MyClass(MyClass&& rValue) noexcept
    : val{ std::move(rValue.val) }
  {
    rValue.val = 0;

    name = rValue.name;
    rValue.name = nullptr;
  }

  // 移动赋值运算符
  MyClass& operator=(MyClass&& myClass) noexcept
  {
    val = myClass.val;
    myClass.val = 0;

    name = myClass.name;
    myClass.name = nullptr;

    return *this;
  }

  ~MyClass()
  {
    if (nullptr != name)
    {
      delete[] name;
      name = nullptr;
    }
  }

private:
  int val;
  char* name;
};

MyClass A{};
MyClass B{};
B = std::move(A);  // 使用移动赋值运算符将对象A赋值给对象B

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

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

相关文章

【Unity3D】绘制物体外框线条盒子

1 需求描述 点选物体、框选物体、绘制外边框 中介绍了物体投影到屏幕上的二维外框绘制方法&#xff0c;本文将介绍物体外框线条盒子绘制方法。 内框&#xff1a;选中物体后&#xff0c;绘制物体的内框&#xff08;紧贴物体、并与物体姿态一致的内框盒子&#xff09;外框&#…

Python与C++语法比较--字符串篇

tags: C Python 写在前面 刷lc从Python转向C, 不只是语法层面, 还要改变很多的API, 这次记录一下C和Python在字符串方面的一些区别, 供参考. 基本区别 Python字符串是不可变类型, 而C的String为容器(本质上是一个类的别名, 说容器有点不合适, 因为容器内的元素类型已经被指…

Kotlin中的Lambda编程

文章目录1.集合的创建与遍历2.集合的函数式API3.Java函数式API的使用1.集合的创建与遍历 传统意义上的集合主要是List和Set&#xff0c;再广泛一点的话&#xff0c;像Map这样的键值对数据结构也可以包含进来。List&#xff0c;Set和Map再Java中都是接口&#xff0c;List主要的…

Java设计模式-命令模式Command

介绍 命令模式&#xff08;Command Pattern&#xff09;&#xff1a;在软件设计中&#xff0c;我们经常需要向某些对象发送请求&#xff0c;但是并不知道请求的接收 者是谁&#xff0c;也不知道被请求的操作是哪个&#xff0c; 我们只需在程序运行时指定具体的请求接收者即可&…

数影周报:TikTok因在线跟踪被罚500万欧,Windows 7退出历史舞台

本周看点&#xff1a;TikTok因在线跟踪被法国罚款500万欧元 &#xff1b;思科已裁员近700 人&#xff1b;Windows 7退出历史舞台&#xff1b;亚马逊向所有卖家开放Buy with Prime服务&#xff1b;“全路程”完成2亿元C轮融资......数据安全那些事TikTok因在线跟踪被法国罚款500…

Android13 wifi无线调试adb连接设置

在进行adb调试的时候&#xff0c;有时候需要使用wifi连接&#xff0c;或者wifi连接较为方便&#xff0c;早些的Android上&#xff0c;需要设置端口等操作&#xff0c;adb tcpip 6666参考android wifi adb 调试 - 简书 (jianshu.com)好几步操作&#xff0c;在Android13上&#x…

Deque 的理解 STL中stack与queue为什么选择使用deque为底层模板容器

目录 一、Deque的引入 二、Deque是什么&#xff1f; 三、deque的遍历方式&#xff1f;deque的缺陷&#xff1f; 四、它为什么能更贴合与stack与queue&#xff1f; 五、STL中vector与list的底层实现 一、Deque的引入 Stack、Queue在之前的博客中我也是分别使用了更容易处理…

【蓝桥杯】历届真题 杨辉三角形 (省赛)Java

【问题描述】 下面的图形是著名的杨辉三角形: 如果我们按从上到下、从左到右的顺序把所有数排成一列&#xff0c;可以得到如下数列: 1,1&#xff0c;1&#xff0c;1&#xff0c;2,1&#xff0c;1&#xff0c;3,3&#xff0c;1&#xff0c;1,4&#xff0c;6,4&#xff0c;1&…

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 (六)多功能数据处理器

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 &#xff08;六&#xff09;多功能数据处理器 &#x1f508;声明&#xff1a; &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN博客 &#x1f9e8;未经作者允许&#xff0c;禁止转载 &#x1f511;系列专栏&#xff1a;牛客…

react基础Day02-受控组件评论案例propscontext

React组件 目标 能够知道受控组件是什么能够写出受控组件了解非受控组件 表单处理 受控组件&#xff08;★★★&#xff09; HTML中的表单元素是可输入的&#xff0c;也就是有自己的可变状态而React中可变状态通常保存在state中&#xff0c;并且只能通过setState() 方法来…

[acwing周赛复盘] 第 86 场周赛20230114

[acwing周赛复盘] 第 86 场周赛20230114 一、本周周赛总结二、 4794. 健身1. 题目描述2. 思路分析3. 代码实现三、4795. 安全区域1. 题目描述2. 思路分析3. 代码实现四、4796. 删除序列1. 题目描述2. 思路分析3. 代码实现六、参考链接一、本周周赛总结 去吃羊蝎子了&#xff0…

基于汽车知识图谱的汽车问答多轮对话系统 详细教程

结果: 1 技术路线 该技术路线主要将KBQA分为三部分,实体识别与实体链接,关系识别,sparql查询,其中每个部分分为一到多种方法实现。具体的处理流程图如下:

大脑的记忆

AI神经网络中的记忆 当前AI发展进入一个瓶颈,大家都意识到还是要继续在人脑中获取AI方向的指引。当然也有科学家说物理世界与心理世界并非一一对应,人类的智能也没必要与物理世界一一对应,甚至本质上都可以是不同的,所以没必要研究大脑认知和大脑的机制,更不需要分子级别…

IDEA structure窗口各标志及功能

文章目录图标对象类型访问权限其他修饰符工具栏图标 对象类型 class 类 interface 接口 enum 枚举 interface 注解 class initializer 代码块 method 方法 field 字段/属性 anonymous class 匿名类 lambda lambda表达式 propertie 访问器&#xff08;get方法&#xff0…

【Java面试】Queue接口

文章目录BlockingQueue中有哪些方法&#xff0c;为什么这样设计&#xff1f;BlockingQueue是怎么实现的&#xff1f;BlockingQueue中有哪些方法&#xff0c;为什么这样设计&#xff1f; 先看一眼结构&#xff0c;再看具体的分析 为了应对不同的业务场景&#xff0c;Blockin…

拉伯证券|业绩猛增超13倍,主力连续抢筹,这只股收获4连板

成绩陡增股获主力接连抢筹 春节日益接近&#xff0c;A股成交活跃度有所下滑&#xff0c;不过有一些股票节前继续取得主力喜爱。证券时报•数据宝核算&#xff0c;到1月12日收盘&#xff0c;沪深两市共54只个股接连5日或5日以上主力资金净流入。 主力资金净流入继续周期最长的是…

人工智能学习07--pytorch03--tensorboard(下载tensorboard、opencv)

transform 主要是对input图像进行变换&#xff08;统一尺寸、对图像中的数据进行类的转换&#xff09; TensorBoard很有用 如&#xff1a;通过loss的变化过程&#xff0c;来看loss的变化是否复合预想。也可以通过loss来选择模型。 TensorBoard&#xff0c;虽然他是TensorFlo…

排序综合(C++版)

目录 排序综合 一、问题描述 二、运行环境说明 三、代码段 四、效果展示 排序综合 备注&#xff1a;大二&#xff08;上&#xff09;数据结构课程设计B题 一、问题描述 给定N…

Python asyncio异步编程简单实现

今天继续给大家介绍Python相关知识&#xff0c;本文主要内容是Python asyncio异步编程简单实现。 一、asyncio事件循环简介 asyncio引入了事件循环的概念。事件循环是一个死循环&#xff0c;还循环会检测并执行某些代码。在Python中&#xff0c;引入了asyncio模块后&#xff…

动态内存管理:学习笔记9

目录 一.前言 二.动态内存函数 1.malloc和free 2.calloc函数 3. realloc函数(动态内存空间调整函数) 情形一&#xff1a;扩容时&#xff0c;原内存地址处可以容纳调整后的动态内存 情形二&#xff1a;扩容时&#xff0c;原内存地址无法容纳调整后的动态内存 三.C/C程序…