首页 > 其他分享 >17、std::move和移动语义详解

17、std::move和移动语义详解

时间:2024-01-25 14:59:49浏览次数:27  
标签:std 17 右值 move 左值 移动 构造函数

概述

  std::move 是 C++ 标准库中的一个函数模板,用于将一个左值(左值引用)转化为右值引用,从而实现移动语义。移动语义是一种可以将资源(如内存)从一个对象转移到另一个对象的方式,而不是进行资源的复制。移动操作通常比复制操作更高效,对于大型的对象(如容器、字符串等)可以带来很大的性能优势。

左值与右值

在 C++ 中,左值是可以被取地址的表达式,而右值是临时的、不可取地址的表达式。 通常,左值是具有名称、有持久性的,而右值是临时性的、瞬时的。其具体区别如下:

  1. 左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体。
  2. 右值是在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
  3. 一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
  4. 左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。
  5. 右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。

如:

int a = 3;                         // a是左值,3是右值
std::string str = "hello world";   // str是左值,"hello world"是右值

使用 std::move 可以告诉编译器将一个对象视为右值,从而触发移动语义的操作。那么到底什么事移动语义呢?

std::move实现移动语义的代码应用

我们知道对于C++的类来说,通常有构造函数、赋值构造函数、拷贝构造函数等适用于在不同的条件下创建新对象,从C++11开始,还有了移动构造函数,它不同于拷贝构造函数,通常不会进行资源的复制(除非在自定义的移动构造函数中非要进行资源的复制,相信大家不会这么做^_^),当进行参数传递等一些操作时,巧妙利用std::move实现移动语义,就可以减少之前不必要的拷贝构造,从而大幅提高程序效率,下面看代码有助于加深理解。

首先实现一个MyClass类,具体说明请看注释:

#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int value)
            :ptr_(new int(value)) {    // 构造函数,存在开辟内存、复制资源的操作
        std::cout << "Default constructor called: MyClass(int value)" << std::endl;
    }
    MyClass(const MyClass& other)   // 拷贝构造函数,存在开辟内存、复制资源的操作
            : ptr_(new int(*other.ptr_)) {
        std::cout << "Copy constructor called: MyClass(const MyClass& other)" << std::endl;
    }
    MyClass(MyClass&& other) noexcept       // 移动构造函数,只是地址的复制,没有新开内存、资源复制
            : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
        std::cout << "Move constructor called: MyClass(MyClass&& other)" << std::endl;
    }
    MyClass& operator=(const MyClass& other) {  // 赋值构造函数,也存在开辟内存、复制资源的操作
        if (&other == this) {
            return *this;   // 自我赋值,直接返回
        }
        if (ptr_) {
            delete ptr_;                    // 释放原有内存
        }
        // 逐个赋值
        ptr_ = new int(*other.ptr_);
        return *this;
    }
    ~MyClass() {
        if (ptr_) {
            delete ptr_;
        }
        std::cout << "Destructor called." << std::endl;
    }
    int GetValue(void) { return *ptr_; }
    // 打印数据
    void PrintData() const {
        std::cout << "Data: " << *ptr_ << std::endl;
    }
private:
    int* ptr_;   // 相当于Class内部管理的资源
};

下面我们看一下如何通过std::move触发移动构造函数:

int main (void)
{ MyClass obj1(10); // 调用默认构造函数 MyClass obj2 = std::move(obj1); // 调用移动构造函数 MyClass obj3(30); // 调用默认构造函数 MyClass obj4(std::move(obj3)); // 调用移动构造函数 return 0; }

运行后得到如下结果:

可以直观地看到利用obj1创建obj2时以及利用obj3创建obj4时,均调用了移动构造函数而不是赋值构造函数或拷贝构造函数。

接下来,我们以std::vector<MyClass>的相关操作为例,看一下std::move在工程中的具体应用:

int main() {
    std::vector<MyClass> vec;
    // 不使用移动语义
    MyClass obj5(10);            // 调用默认构造函数
    vec.push_back(obj5);         // 调用复制构造函数

    // 使用移动语义
    MyClass obj6(20);            // 调用默认构造函数
    vec.push_back(std::move(obj6));                    // 调用拷贝+移动构造函数
    for (auto &obj : vec) {
        obj.PrintData();
    }

    return 0;
}

编译后运行结果如下:

由上可知:首先,我们定义一个std::vector<MyClass>对象,并准备向其中push新元素。

传统做法是不使用移动语义的,这样会先调用默认构造函数创建新对象obj1,再通过拷贝构造函数将obj1的资源复制到vector新元素中,在拷贝构造函数中会涉及到开辟内存、资源复制等操作;

当使用了移动语义之后,我们首先通过默认构造函数创建了对象obj2,然后通过std::move直接将obj2转换为右值传递给vector,将obj2的所有权转移给vector中的新元素,从运行结果也可以看出由于std::vector本身的实现机制,在所有权转移过程中调用了两次移动构造函数,但是均不会涉及内存开辟、资源复制等操作,提高了代码效率。

std::move实现移动语义的优点

可以将对象从左值变为右值,避免拷贝构造,只是将对象状态或者所有权从一个对象转移到另一个对象,没有涉及内存的搬迁或者内存拷贝,从而极大地提高代码效率。

但需要注意,使用 std::move 后原对象(如上面的obj6)的状态是不确定的,不应再对其进行操作,否则程序运行时可能出现Segmentation fault (core dumped)报错!!!

 

标签:std,17,右值,move,左值,移动,构造函数
From: https://www.cnblogs.com/zwj-199306231519/p/17987126

相关文章

  • 16、std::forward与完美转发详解
    概述std::forward是C++11中引入的一个函数模板,用于实现完美转发(PerfectForwarding)。它的作用是根据传入的参数,决定将参数以左值引用还是右值引用的方式进行转发。然而,完美转发是为了解决传递参数时的临时对象(右值)被强制转换为左值的问题。在C++03中,可以使用泛型引用来......
  • 寒假生活指导17
    <template><divclass="carbon-quota-page"><!--页面标题--><h1>碳额度查询</h1><!--查询表单区域--><el-formv-if="!loading":model="form"ref="queryForm"la......
  • Codeforces Round 170 (Div. 1)A. Learning Languages并查集
    如果两个人会的语言中有共同语言那么他们之间就可以交流,并且如果a和b可以交流,b和c可以交流,那么a和c也可以交流,具有传递性,就容易联想到并查集,我们将人和语言看成元素,一个人会几种语言的话,就将这些语言和这个人所在的集合合并,最后求一下人一共在几个连通块中,连通块的个数-1就是答案,......
  • G.Snake Move
    赛后独立思考了不算长的一段时间,然后就通过了;赛时没看这道题有些可惜,算是个教训吧发现两个性质之后,这道题就非常简单了:1.只有初始位置的贪吃蛇才可能会对最优路径产生影响2.令贪吃蛇长度-1的操作等价于它停在原地不动点击查看代码#include<bits/stdc++.h>usingnamespac......
  • std::function类的使用示例
    std::function是C++标准库中的一个模板类,用于封装可调用的目标,比如函数、函数指针、成员函数指针、Lambda表达式等,使得它们可以像普通函数一样被调用。这种灵活性使得std::function在许多场景下都非常有用。以下是std::function的一般用法:1.封装函数指针1.1不带参数和返回值......
  • Error Code: 1171. All parts of a PRIMARY KEY must be NOT NULL
    今天建表时候发现报错了:CREATETABLEt3(c1intDEFAULTNULL,c2intDEFAULTNULL,c3intNOTNULL,c4intDEFAULTNULL,PRIMARYKEY(c1,c2,c3))ENGINE=InnoDBDEFAULTCHARSET=utf8mb3ErrorCode:1171.AllpartsofaPRIMARYKEYmustbeNOTNULL;ifyounee......
  • 洛谷题单指南-模拟和高精度-P1786 帮贡排序
    原题链接:https://www.luogu.com.cn/problem/P1786题意解读:此题比较简单,模拟+排序即可解决。需要注意的是,当帮贡或者等级相同时,都要保持原来的顺序,因此需要记录每个人的编号,便于排序。话不多说,直接上代码。100分代码:#include<bits/stdc++.h>usingnamespacestd;constint......
  • 2024最新iOS17.3微信分身详解分享
    微信是目前最流行的社交软件之一,拥有庞大的用户群体。然而,对于一些需要同时使用多个微信账号的用户来说,使用官方版微信就显得有些不方便。iOS分身微信软件可以解决这个问题,它可以让用户在同一台设备上同时登录多个微信账号,从而实现工作生活两不误。iOS分身微信软件的优势iOS分身微......
  • remove 移除数据
    //云端代码constdb=uniCloud.database()exports.main=async(event,context)=>{constcollection=db.collection(event.name)constdocList=awaitcollection.where(event.data).get()if(!docList.data||docList.data.length===0){......
  • ARC170C Prefix Mex Sequence
    题意简述有长度为\(n\)的\(s_i=0/1\),求满足下列条件的长度为\(n\)的序列\(a\)的个数,对\(998244353\)取模:\(\foralli,0\lea_i\lem\)当\(s_i=0\)时,\(a_i\not=\operatorname{mex}(a_1,a_2,\cdots,a_{i-1})\)当\(s_i=1\)时,\(a_i=\operatorname{mex}(a_1,a_2,\......