基于本文回答
0
评论

C++ 中如何实现深拷贝?

知识点图片

在 C++ 中,当类中包含指针成员动态分配的资源(如 new 分配的内存、文件句柄等)时,编译器默认生成的浅拷贝(Shallow Copy,只复制指针地址)会导致多个对象指向同一块内存,进而引发重复释放(Double Free)、内存泄漏或数据被意外修改等严重问题。

因此,我们需要实现深拷贝(Deep Copy):为新对象分配独立的内存,并将原对象内存中的数据复制过去。

实现深拷贝的核心是遵循 C++ 的“三法则”(Rule of Three),即如果你需要自定义以下任何一个,你通常需要自定义全部三个:

  1. 析构函数(释放资源)
  2. 拷贝构造函数(实现深拷贝)
  3. 拷贝赋值运算符(实现深拷贝并处理自我赋值)

以下是具体的实现方法和最佳实践:


1. 经典实现方法(手动管理内存)

我们以一个包含 int* 指针的类为例:

cpp
#include <iostream>

class DeepCopyDemo {
private:
    int* data;

public:
    // 1. 构造函数
    DeepCopyDemo(int value) {
        data = new int(value);
        std::cout << "构造函数调用\n";
    }

    // 2. 析构函数(释放内存)
    ~DeepCopyDemo() {
        delete data;
        std::cout << "析构函数调用\n";
    }

    // 3. 拷贝构造函数(深拷贝)
    // 重点:申请新内存,复制数据
    DeepCopyDemo(const DeepCopyDemo& other) {
        data = new int(*(other.data)); // 解引用并复制值
        std::cout << "拷贝构造函数调用(深拷贝)\n";
    }

    // 4. 拷贝赋值运算符(深拷贝)
    DeepCopyDemo& operator=(const DeepCopyDemo& other) {
        std::cout << "拷贝赋值运算符调用(深拷贝)\n";
        
        // 步骤1:检查自我赋值(非常重要!)
        if (this == &other) {
            return *this;
        }

        // 步骤2:释放原有的内存
        delete data;

        // 步骤3:申请新内存并复制数据
        data = new int(*(other.data));

        // 步骤4:返回自身的引用,支持连续赋值 a = b = c
        return *this;
    }

    // 辅助函数
    void setValue(int val) { *data = val; }
    void print() const { std::cout << "Value: " << *data << ", Address: " << data << "\n"; }
};

int main() {
    DeepCopyDemo obj1(10);
    DeepCopyDemo obj2 = obj1; // 调用拷贝构造函数

    DeepCopyDemo obj3(20);
    obj3 = obj1;              // 调用拷贝赋值运算符

    // 修改 obj1 不会影响 obj2 和 obj3,证明是深拷贝
    obj1.setValue(100);
    
    obj1.print();
    obj2.print();
    obj3.print();

    return 0;
}

2. 更优雅的实现:“拷贝与交换”惯用法(Copy-and-Swap Idiom)

上面实现的“拷贝赋值运算符”虽然正确,但如果在 new int 时抛出异常,会导致当前对象的 data 变成野指针。
为了保证强异常安全性,C++ 推荐使用 Copy-and-Swap 惯用法来实现拷贝赋值运算符:

cpp
#include <algorithm> // for std::swap

class SafeDeepCopy {
private:
    int* data;

public:
    SafeDeepCopy(int value) : data(new int(value)) {}
    ~SafeDeepCopy() { delete data; }
    SafeDeepCopy(const SafeDeepCopy& other) : data(new int(*(other.data))) {}

    // 友元 swap 函数
    friend void swap(SafeDeepCopy& first, SafeDeepCopy& second) noexcept {
        // 交换内部成员
        std::swap(first.data, second.data);
    }

    // 拷贝赋值运算符(注意参数是按值传递!这里会隐式调用拷贝构造函数)
    SafeDeepCopy& operator=(SafeDeepCopy other) {
        // 将当前对象的内容与局部对象 other 进行交换
        swap(*this, other);
        return *this;
        // other 离开作用域时,会自动调用析构函数,释放掉当前对象原有的旧内存
    }
};

优点:代码极其简洁,自动处理了自我赋值问题,且保证了强异常安全性(如果在按值传参时内存分配失败,当前对象不会被破坏)。


3. 现代 C++ 最佳实践:零法则(Rule of Zero)

在 Modern C++ (C++11 及以后) 中,手动写 newdelete 是极其不推荐的。如果你想避免写繁琐的深拷贝代码,最好的方法是使用标准库提供的资源管理类。

这被称为“零法则”:类的设计应该使其不需要自定义析构函数、拷贝构造函数和赋值运算符。

方案 A:使用 std::vectorstd::string

STL 容器自带深拷贝语义。如果你把裸指针换成 std::vector,编译器默认生成的拷贝函数就已经是深拷贝了!

cpp
#include <vector>

class ModernClass {
private:
    std::vector<int> data; // 替代裸指针或动态数组

public:
    ModernClass(int value) : data(1, value) {}
    
    // 不需要手动写析构、拷贝构造和赋值运算符!
    // 默认的浅拷贝行为对 std::vector 来说就是深拷贝其元素。
};

方案 B:结合智能指针(如果必须用指针)

如果你必须使用指针实现多态或特殊逻辑,并且需要深拷贝,std::unique_ptr 默认禁止拷贝,你需要手动编写拷贝构造函数;如果是 std::shared_ptr,默认拷贝只是增加引用计数(不是深拷贝)。

如果你使用 std::unique_ptr 并需要深拷贝:

cpp
#include <memory>

class SmartPointerDeepCopy {
private:
    std::unique_ptr<int> data;

public:
    SmartPointerDeepCopy(int value) : data(std::make_unique<int>(value)) {}
    
    // 拷贝构造函数(手动实现深拷贝)
    SmartPointerDeepCopy(const SmartPointerDeepCopy& other) {
        if (other.data) {
            data = std::make_unique<int>(*other.data);
        }
    }

    // 拷贝赋值运算符
    SmartPointerDeepCopy& operator=(const SmartPointerDeepCopy& other) {
        if (this != &other) {
            if (other.data) {
                data = std::make_unique<int>(*other.data);
            } else {
                data.reset();
            }
        }
        return *this;
    }
    
    // 不需要写析构函数,unique_ptr 会自动释放内存!
};

总结建议

  1. 首选:使用 std::vectorstd::string 等 RAII 容器,依靠编译器的默认行为,根本不需要自己写深拷贝(Rule of Zero)。
  2. 次选:如果必须自己管理资源,必须同时实现析构函数拷贝构造函数拷贝赋值运算符Rule of Three)。
  3. 进阶:使用 Copy-and-Swap 惯用法编写赋值运算符,以提高代码的异常安全性和鲁棒性。
右滑查看面试常问