【ChernoC++笔记】移动语义

发布时间 2023-07-10 21:46:30作者: rthete

【89】【Cherno C++】【中字】C++移动语义

❓为什么使用移动语义(moving semantics)?

很多时候,我们需要通过复制来传递对象:

  • 例如,把一个对象传递给一个函数,这个函数需要得到这个对象的所有权,我们需要在当前stack frame中构造一个一次性对象,然后复制到调用的函数中。
  • 同样的,想从函数返回一个对象,需要在函数中创建那个对象,然后返回,也就是需要复制。(可以通过*返回值优化来解决)

但是有些情况下,我们不想把一个对象通过复制传递。当要传递的对象需要堆分配内存,比如一个字符串,如果需要复制,必须创建一个全新的堆分配。如果我们只是移动对象而不是复制对象,将大大地提高性能。

▶️一个拷贝构造的例子

class String {
public:
    String() = default;
    String(const char* string) {
        printf("Created!\\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        // 将源字符串string移动到数据缓冲区m_Data
        memcpy(m_Data, string, m_Size);
    }

    // 拷贝构造函数
    String(const String& other) {
        printf("Copied!\\n");
        m_Size = other.m_Size; 
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }

    ~String() {
        printf("Destroyed!\\n");
        delete m_Data;  // char是简单类型,不需要delete[]析构
    }

    void Print() {
        for(uint32_t i = 0; i < m_Size; i++) {
            printf("%c", m_Data[i]);
        }
        printf("\\n");
    }

private:
    char* m_Data;   // 实际的字符串数据
    uint32_t m_Size;
};

class Entity {
public:
    Entity(const String& name) : m_Name(name) {}

    void PrintName() {
        m_Name.Print();
    }

private:
    String m_Name;
};

int main() {
    Entity entity(String("Cherno"));
    entity.PrintName(); 
}

运行代码打印如下:

Created!   // main函数中构造String实例
Copied!    // 拷贝构造entity中的String实例
Destroyed! // other实例析构
Cherno
Destroyed! // entity中的m_Name实例析构

可以看到,为了构造一个“Cherno”Entity对象,我们为它分配了两次内存:首先要在main函数的作用域创建了一个String对象(”Created!”),即第一次分配内存;再传递给Entity构造函数,使用拷贝构造函数再次分配空间,创建entity.m_Name这个String对象(”Copied!”)。

为什么不能在main函数中分配,然后直接移动到entity.m_Name中呢?➡️➡️移动语义

▶️利用移动语义来避免拷贝

  • 将Entity的构造函数传入参数改为一个右值引用,以传入临时值。并且需要将name显示转换为临时值,即使用(String&&)namestd::move(name),因为将一个临时变量绑定到右值引用上时,这个右值引用是一个左值
  • String类的move构造函数:不再需要new一个新的buffer,进行memcpy逐个复制数据。只需要给指针赋值,把新的字符串实例中创建的指针,指向other.m_Data指向的同一块数据。
  • 但是,旧的那个String实例(other)被删除后,会将它的数据带走。所以我们需要将other指向nullptr,即置空。当旧的String实例被销毁时,delete m_Data;实际上删除了nullptr。
  • 这么做实际上是接管了旧的String实例,重新连接了指针,即浅拷贝。而不是通过复制所有的数据并分配新内存来进行深拷贝
class String {
public:
    String() = default;
    String(const char* string) {
        printf("Created!\\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        // 将源字符串string移动到数据缓冲区m_Data
        memcpy(m_Data, string, m_Size);
    }

    // 拷贝构造函数
    String(const String& other) {
        printf("Copied!\\n");
        m_Size = other.m_Size; 
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }

    // move构造函数
    String(String&& other) noexcept {
        printf("Moved!\\n");
        m_Size = other.m_Size; 
        m_Data = other.m_Data;
        // 不再需要分配新的数据缓冲区
        // 偷取了other的资源,放回一个空对象,析构时只delete nullptr
        other.m_Data = nullptr;
        other.m_Size = 0;
    }

    ~String() {
        printf("Destroyed!\\n");
        delete m_Data;  // char是基本类型,不需要delete[]析构
    }

    void Print() {
        for(uint32_t i = 0; i < m_Size; i++) {
            printf("%c", m_Data[i]);
        }
        printf("\\n");
    }

private:
    char* m_Data;   // 实际的字符串数据
    uint32_t m_Size;
};

class Entity {
public:
    Entity(const String& name) : m_Name(name) {}

    // 传入右值,会调用该构造函数
    // Entity(String&& name) : m_Name(name) {}
    // 需要将name显示转换为临时值,因为将一个临时变量绑定到右值引用上时,这个右值引用是一个左值
    // Entity(String&& name) : m_Name((String&&)name) {}
    // 或者,使用std::move也可以做到
    Entity(String&& name) : m_Name(std::move(name)) {}
    
    void PrintName() {
        m_Name.Print();
    }

private:
    String m_Name;
};

int main() {
    // String("Cherno")是一个临时变量,调用Entity的move构造函数
    Entity entity(String("Cherno"));
    entity.PrintName(); 
    std::cin.get();
}

运行代码打印如下:

Created!
Moved! // 成功使用了move构造函数
Destroyed!
Cherno
Destroyed!