move移动语义详解

move移动语义

移动语义是C++11引入的一种机制,用于提高程序的性能和资源管理效率,特别是在涉及大数据对象的场景下。移动语义通过转移资源所有权,而不是复制资源,减少了不必要的拷贝操作

一、为什么需要移动语义?

当对象需要被复制时(如函数返回值或传参),通常会调用复制构造函数(copy constructor)。复制操作往往意味着需要分配新资源并将原资源的数据拷贝到新资源中;而如果不需要保留原对象的内容,我们可以**选择“移动”**而非“复制”,从而避免不必要的资源分配和数据拷贝。

二、什么是右值引用?

首先引入值类别的概念:C++11之前的值类别分为 左值(Lvalue)右值(Rvalue),而C++11引入了更细化的分类,包括 纯右值(Prvalue)将亡值(Xvalue)泛左值(Glvalue).

  • 左值:表示一个有持久地址的对象,可以对其取地址。

    • int x = 10;
  • 纯右值:表示一个临时值或不占用持久存储的值

    • 通常是字面量或计算结果,用于初始化或传递到函数。
    • x + 5;
  • 将亡值:特殊的右值,表示即将“被销毁”的对象,可以通过移动语义转移其资源

    • 通常出现在右值引用和std::move中。
    • std::move(x); //将亡值,x即将被移动
  • 泛左值:是左值和将亡值的统称,表示可以标识对象的表达式

    • 泛左值可以访问或修改对象。

    • x;           // 左值 -> 泛左值
      std::move(x); // 将亡值 -> 泛左值
      

所以得出右值引用的概念:右值引用就是右值引用,它和左值引用一样,算是一种引用。-> 右值引用只能被右值表达式初始化。

右值引用的作用:在右值引用(移动语义)出现之前,我们没有办法简单的区分,你到底是要复制,还是要移动?

将移动右值引用的行为称为“转移所有权”。

什么是移动:把原对象的指向资源的指针,赋给新的对象成员,也就是所谓的转移所有权,通常的实现是转移指向数据的那个指针给新对象

(即地址仍然是一样的,只是转移了所有权——转移实际数据的指针

#include 
#include 

struct Test{};

int main(){
 std::vectorv;
 v.emplace_back(Test{});
 std::cout << &v[0] << '\n';
 std::vector v2{ std::move(v) }; //移动语义,其实只是把v的指针转移所有权给了v2
 std::cout << &v2[0] << '\n';
} 

那么一个左值表达式,如何匹配到右值版本的函数? -> 使用std::move()

三、移动构造、赋值函数的实现

移动构造函数通过将原对象的资源所有权转移到新对象,从而避免昂贵的资源复制操作

实现细节:通过右值引用操作符将其它对象(other)的资源初始化构造对象(this)的资源,并在构造函数中清空原对象的资源

class MyClass {
private:
    int* data;
    size_t size;
public:
    // 构造函数
    MyClass(size_t s) : size(s), data(new int[s]) {}
    // 移动构造函数
    MyClass(MyClass&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;  // 清空原对象的资源,防止其析构函数释放资源
        other.size = 0;        // 确保原对象处于有效但最小化的状态
    }
    // 析构函数
    ~MyClass() { delete[] data; }
};

移动赋值运算符实现细节:防止自我赋值,转移并清空原对象(ohter)的资源

MyClass& operator=(MyClass&& other) noexcept {
    if (this != &other) {  // 防止自赋值
        delete[] data;     // 释放当前对象已有的资源
        data = other.data; // 转移资源所有权
        size = other.size;
        other.data = nullptr;  // 清空原对象
        other.size = 0;
    }
    return *this;
}

三、std::move的实现细节

std::move 是一个模板函数。本质是类型转换工具

作用:将左值显式转换为右值引用,提示编译器可以安全地“移动”资源。

move并不会真的移动资源,只是改变对象的值类别【资源的移动在移动构造/移动赋值中完成】)

template 
typename std::remove_reference::type&& move(T&& t) noexcept {
    return static_cast::type&&>(t);
}

四、何时使用移动语义

返回局部对象: 当函数返回局部对象时,使用移动语义可以避免复制。

其实在C++11之后,编译器有些功能就会隐性的调用move,如以下代码:我们既没有加引用,也没有使用右值引用;正常情况应该是复制一份传出去,但是在C++11里面会通过move给移动出去,而不调用拷贝构造。

MyClass createObject() {
    MyClass obj(100);
    return obj; // 返回时会调用移动构造函数
}

处理容器中的大对象: 在向容器插入对象时,使用移动语义可以提高性能:

std::vector vec;
std::string str = "A large string";
vec.push_back(std::move(str)); // 避免了字符串数据的复制

五、移动语义与引用的区别

引用和移动语义的功能虽然都能避免不必要的拷贝,但它们的本质、目的和适用场景完全不同。

区别:

  • 引用是对象的别名,无法转移资源的所有权:当你使用引用时,原对象的资源始终归原对象所有

    • 移动语义的核心是资源所有权的转移:通过移动构造函数或移动赋值运算符,原对象的资源可以被转移到新对象中
  • 引用不能优化返回值(临时对象)的传递:当函数返回一个局部临时对象时,无法直接使用引用,因为临时对象会在函数结束时销毁。

    • 移动语义则允许函数返回局部对象时,直接转移资源,无需创建拷贝。
  • 引用无法处理容器内部的资源转移,而移动语义可以显著提升性能

    •     std::vector vec;
          std::string s = "A long string with lots of content";
          vec.push_back(std::move(s)); // 调用了移动构造函数,避免了字符串复制
      
  • 引用无法避免临时对象的拷贝,而移动语义可以通过右值引用直接接管临时对象的资源

  • 多线程下不同线程访问同一个引用可能导致数据竞争和未定义行为,而移动语义通过资源的所有权转移,使得资源明确归属于某个线程,避免了多个线程共享同一个资源

你可能感兴趣的:(C++,c++,面试)