MST

星途 面试题库

面试题:C++右值引用与移动语义的性能考量

在C++中,右值引用和移动语义紧密相关。请详细阐述在一个包含复杂对象(例如,对象内部包含动态分配的内存)的类中,如何正确实现移动构造函数和移动赋值运算符,以利用右值引用达到性能优化?目的是避免不必要的深拷贝操作。同时说明在实现过程中可能会遇到的陷阱以及如何避免。
46.5万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

1. 移动构造函数的实现

假设我们有一个类 MyClass,内部包含动态分配的内存:

class MyClass {
private:
    int* data;
    int size;
public:
    // 构造函数
    MyClass(int s) : size(s) {
        data = new int[size];
    }
    // 析构函数
    ~MyClass() {
        delete[] data;
    }
    // 移动构造函数
    MyClass(MyClass&& other) noexcept : size(other.size), data(other.data) {
        other.size = 0;
        other.data = nullptr;
    }
};

在移动构造函数中:

  • 直接接管 other 对象的资源(datasize)。
  • other 对象的资源指针设为 nullptr 并将大小设为 0,这样 other 对象析构时不会释放已被接管的资源。
  • 使用 noexcept 说明该函数不会抛出异常,有助于编译器进行优化。

2. 移动赋值运算符的实现

class MyClass {
    // 其他成员...
public:
    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = other.data;
            other.size = 0;
            other.data = nullptr;
        }
        return *this;
    }
};

在移动赋值运算符中:

  • 首先检查是否是自赋值,如果是则直接返回。
  • 释放当前对象的旧资源(delete[] data)。
  • 接管 other 对象的资源并将 other 对象置为无效状态。
  • 同样使用 noexcept 表明不会抛出异常。

3. 可能遇到的陷阱及避免方法

  • 忘记释放旧资源:在移动赋值运算符中,如果忘记释放当前对象的旧资源(如 delete[] data),会导致内存泄漏。要始终记得在接管新资源前释放旧资源。
  • 未处理自赋值:在移动赋值运算符中,如果不检查自赋值情况,可能会导致对象自身资源被释放,进而出现未定义行为。通过 if (this != &other) 进行检查可避免这种情况。
  • 未标记为 noexcept:如果移动构造函数和移动赋值运算符没有标记为 noexcept,标准库在某些情况下(如容器的插入操作)可能不会使用移动语义,从而无法达到性能优化的目的。所以要确保标记为 noexcept,前提是函数确实不会抛出异常。
  • 资源所有权混乱:确保移动操作后,源对象处于有效但可析构的状态(通常将指针设为 nullptr 等),避免在后续操作中对已被移动的对象进行非法访问。