2、移动构造函数:深拷贝与浅拷贝问题、移动构造函数的定义与实现、移动构造函数调用时机

2.1 深拷贝与浅拷贝——老生常谈的坑

先聊聊浅拷贝。说白了,就是编译器默认生成的拷贝构造函数,它只做成员变量的逐位复制。如果你的类里有指针成员,浅拷贝会让两个对象指向同一块内存。

我在项目中遇到过这样一个bug:一个日志类里有个 char* buffer,两个对象析构时都去 delete[] 同一块内存,程序直接崩了。嗯,这就是典型的浅拷贝灾难。

深拷贝就好多了。它会给新对象分配独立的内存,然后把数据一个一个复制过去。代码大概是这样的:

class String {
    char* data_;
    size_t size_;
public:
    // 深拷贝构造函数
    String(const String& other) 
        : size_(other.size_)
        , data_(new char[size_ + 1]) 
    {
        std::memcpy(data_, other.data_, size_ + 1);
    }
    
    ~String() { delete[] data_; }
};

深拷贝安全,但代价高。你想想看,每次拷贝都要 new 内存、memcpy,如果对象很大,性能损耗非常明显。

核心矛盾:深拷贝保证了安全,但浪费了性能。特别是那些临时对象,用完就销毁,何必还要深拷贝一份呢?

2.2 移动构造函数的定义与实现

移动构造函数就是来解决这个矛盾的。它的思路很简单:把临时对象的资源「偷」过来,再把临时对象置为空

定义方式如下:

class String {
    char* data_;
    size_t size_;
public:
    // 移动构造函数
    String(String&& other) noexcept
        : data_(other.data_)    // 偷走指针
        , size_(other.size_)
    {
        // 把源对象置为可析构的空状态
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    ~String() { delete[] data_; }
};

注意几个关键点:

  • 参数是 String&&,也就是右值引用
  • 函数体里没有 new 操作,只是指针的交换
  • 必须把源对象的指针置空,否则析构时会释放掉我们刚偷来的内存
  • 建议加上 noexcept,这样标准库容器在重新分配内存时才会优先使用移动构造

个人习惯:我写移动构造函数时,一定会把源对象置为「空但可析构」的状态。曾经有个同事忘了置空,结果两个对象析构时 double free,查了一下午才找到原因。

2.3 移动构造函数的调用时机

什么时候会调用移动构造函数?说白了,就是当实参是一个右值的时候。常见场景有:

  1. 返回局部对象:函数返回一个局部变量时,编译器会优先尝试移动构造
  2. std::move 显式转换:把左值转成右值引用,强制触发移动
  3. 临时对象传参:比如 func(String("hello"))
  4. 容器操作:vector 扩容时,如果元素有移动构造函数,就会用移动代替拷贝

看个例子:

String createString() {
    String s("hello");
    return s;  // 这里会尝试移动构造(RVO 优化前)
}

int main() {
    String s1("world");
    String s2 = std::move(s1);  // 显式触发移动构造
    String s3 = createString(); // 返回临时对象,触发移动构造
}

我曾经踩过的坑:在移动构造函数里抛了异常,结果源对象已经被修改了,析构时又出问题。所以移动构造函数一定要 noexcept,要么就别抛异常。标准库容器在保证强异常安全时,如果移动不是 noexcept,它会退化成拷贝。

2.4 知识体系总览

下面这张图帮你理清移动构造函数在整个资源管理中的位置:

移动构造函数知识体系 深拷贝 vs 浅拷贝 • 浅拷贝:指针复制,共享内存 • 深拷贝:分配新内存,完整复制 • 问题:深拷贝性能开销大 • 临时对象深拷贝浪费资源 移动构造函数 • 参数:T&& 右值引用 • 行为:偷取资源,置空源对象 • 要求:noexcept 保证 • 效率:O(1) 指针交换 调用时机 • 返回局部对象 • std::move 显式转换 • 临时对象传参 • 容器扩容/插入 核心要点总结 1. 浅拷贝不安全,深拷贝太慢——移动构造是折中方案 2. 移动构造只转移资源所有权,不分配新内存 3. 必须将源对象置为可析构的空状态,防止 double free 4. 加上 noexcept 让标准库容器放心使用移动语义

2.5 避坑指南

最后分享几个我实际工作中遇到的教训:

  • 别忘了处理自移动:虽然很少见,但 std::move(*this) 这种写法是合法的。移动构造函数里要判断一下源对象是不是自己。
  • 移动后对象仍可析构:源对象被移动后,它的析构函数仍然会被调用。所以必须保证它处于一个「安全」的状态,比如指针置空、大小置零。
  • 不要过度使用 std::move:有些人看到左值就加 move,反而会阻止编译器的 RVO(返回值优化)。让编译器自己决定,它比你聪明。

一句话总结:移动构造函数就是「偷」临时对象的资源,省掉深拷贝的开销。但偷完之后记得把源对象收拾干净,别留下烂摊子。


公众号:蓝海资料掘金营,微信 deep3321