17、std::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)报错!!!

 

posted @ 2024-01-25 14:43  zwj鹿港小镇  阅读(225)  评论(0编辑  收藏  举报