深拷贝(Deep Copy)和浅拷贝(Shallow Copy)

10 阅读7分钟

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是处理对象复制时非常重要的概念,尤其是在对象内部包含指针或引用指向动态分配的内存时。它们的主要区别在于如何处理这些内部资源。


1. 浅拷贝 (Shallow Copy)

情景: 你想把笔记本里的信息给你的朋友。

操作: 你没有把整个笔记本复制一份,而是只复制了笔记本的“目录” ,或者说,你只告诉了你的朋友:“这些信息在我的笔记本的第几页、第几行。”

结果:

  • 你的朋友也“拥有”了这些信息,但实际上,他只是知道去哪里看你的笔记本。

  • 问题来了:

    • 如果在你的笔记本里修改了某个信息(比如把“苹果”改成了“香蕉”),那么你的朋友下次按照他手里的目录去看的时候,他看到的也是“香蕉”。因为你们俩的“目录”都指向同一个实际的笔记本
    • 如果把你的笔记本丢了或者烧了,那么你的朋友手里的目录就彻底没用了,因为他再也找不到那些信息了。
    • 更糟糕的是,如果你的朋友也“丢弃”了他手里的目录(他以为他丢弃的是信息),而你又丢弃了你的笔记本,那么同一份信息就被“丢弃”了两次,这会引起混乱甚至错误。

总结: 浅拷贝就像是共享一个链接。大家看到的是同一个内容,内容变了,大家看到的都变。如果源头没了,链接就失效了。(类似于我们经常做的配钥匙,而不是复制房子)


  • 含义: 浅拷贝只复制对象本身的值,而不复制对象内部指向的动态分配的资源。它只是复制了指向这些资源的指针或引用
  • 工作方式: 当你进行浅拷贝时,新对象会拥有与原始对象相同的成员变量值。如果这些成员变量是基本类型(如 int, char, double),那么它们的值会被直接复制。但如果成员变量是指针或引用,那么新对象会复制这个指针或引用的地址,而不是它所指向的数据。
  • 结果: 原始对象和新对象会共享同一块动态分配的内存。
  • 问题:
    • 修改互相影响: 如果通过新对象修改了共享内存中的数据,原始对象也会看到这些修改,反之亦然。
    • 二次释放 (Double Free): 当原始对象和新对象都被销毁时,它们会尝试释放同一块内存两次,这会导致程序崩溃或未定义行为。

例子 (C++ 伪代码):

class MyClass {
public:
    int* data;
    MyClass(int val) {
        data = new int(val); // 动态分配内存
    }
    // 默认的拷贝构造函数或赋值运算符会执行浅拷贝
    // MyClass(const MyClass& other) {
    //     data = other.data; // 只是复制了指针地址
    // }
    ~MyClass() {
        delete data; // 析构时释放内存
    }
};

MyClass obj1(10); // obj1.data 指向一块内存,里面是 10
MyClass obj2 = obj1; // 浅拷贝,obj2.data 和 obj1.data 指向同一块内存

// 此时 obj1.data 和 obj2.data 都指向同一个地址
// 如果通过 obj2 修改数据:
*obj2.data = 20;
// 那么 *obj1.data 也会变成 20

// 当 obj1 和 obj2 销毁时,会尝试对同一块内存 delete 两次,导致错误。

2. 深拷贝 (Deep Copy)

情景: 你想把笔记本里的信息给你的朋友。

操作: 你不是只告诉朋友目录,而是把你的笔记本里的所有信息都一字不差地抄写了一份,然后用一个全新的笔记本把这些抄写出来的信息装好,再把这个新笔记本给了你的朋友。

结果:

  • 现在,你有一个笔记本,你的朋友也有一个笔记本。

  • 这两个笔记本里的信息内容一模一样,但它们是完全独立的两份信息。

  • 好处:

    • 如果在你的笔记本里修改了某个信息(比如把“苹果”改成了“香蕉”),你的朋友的笔记本里仍然是原来的“苹果”。你们互不影响。
    • 如果把你的笔记本丢了或者烧了,你的朋友的笔记本仍然完好无损,他可以继续看他的信息。
    • 你们各自管理自己的笔记本和信息,不会互相干扰,也不会出现“丢弃两次”的问题。

总结: 深拷贝就像是复制一份实体文件。大家都有自己的一份,互不影响。


  • 含义: 深拷贝不仅复制对象本身的值,还会为对象内部指向的动态分配的资源重新分配内存,并将原始对象中的数据复制到新分配的内存中。
  • 工作方式: 当你进行深拷贝时,新对象会为所有动态分配的资源创建独立的副本。这意味着如果原始对象有一个指针指向一块内存,新对象会分配一块新的内存,并将原始指针指向的数据复制到这块新内存中,然后新对象的指针指向这块新内存。
  • 结果: 原始对象和新对象拥有完全独立的资源。
  • 优点:
    • 独立性: 修改其中一个对象不会影响另一个对象。
    • 安全: 不会发生二次释放的问题,因为每个对象都管理自己的资源。

例子 (C++ 伪代码):

class MyClass {
public:
    int* data;
    MyClass(int val) {
        data = new int(val);
    }
    // 深拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int(*other.data); // 为新对象重新分配内存,并复制数据
    }
    // 深拷贝赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) { // 防止自赋值
            delete data; // 释放旧资源
            data = new int(*other.data); // 分配新资源并复制数据
        }
        return *this;
    }
    ~MyClass() {
        delete data;
    }
};

MyClass obj1(10); // obj1.data 指向一块内存,里面是 10
MyClass obj2 = obj1; // 深拷贝,obj2.data 指向另一块内存,里面也是 10

// 此时 obj1.data 和 obj2.data 指向不同的地址
// 如果通过 obj2 修改数据:
*obj2.data = 20;
// 那么 *obj1.data 仍然是 10,不受影响。

// 当 obj1 和 obj2 销毁时,它们各自释放自己的内存,不会冲突。

区别总结

特性浅拷贝 (Shallow Copy)深拷贝 (Deep Copy)
复制内容复制对象本身的值和内部指针/引用的地址复制对象本身的值,并为内部指针/引用指向的资源重新分配内存并复制数据
资源共享原始对象和新对象共享动态分配的资源。原始对象和新对象拥有独立的动态分配资源。
修改影响修改其中一个对象会影响另一个对象。修改其中一个对象不会影响另一个对象。
内存释放容易导致二次释放问题。安全,每个对象管理自己的资源。
实现方式默认的拷贝构造函数/赋值运算符通常是浅拷贝。需要手动实现拷贝构造函数和赋值运算符,以处理动态资源。
效率相对高效,因为只复制指针地址。相对低效,因为需要分配新内存并复制所有数据。

何时使用

  • 浅拷贝: 当对象不包含指向动态分配内存的指针或引用时(例如,只包含基本类型或不涉及资源管理的复合类型),或者你明确希望两个对象共享同一份数据时。
  • 深拷贝: 当对象包含指向动态分配内存的指针或引用,并且你需要确保新对象拥有自己独立的数据副本时。这是处理资源管理(如文件句柄、网络连接、动态数组等)的类通常需要的方式。

在 C++ 中,如果你的类管理着动态内存或其他资源,通常需要遵循“三/五/零法则”(Rule of Three/Five/Zero),即如果需要自定义析构函数,那么通常也需要自定义拷贝构造函数和拷贝赋值运算符(或者移动构造函数和移动赋值运算符),以确保正确的深拷贝行为,避免资源泄漏和二次释放问题。


简单来说:

  • 浅拷贝:只复制“地址”或“引用”,就像复制了一份地图。两份地图都指向同一个地方。这个地方变了,两份地图看到的结果都变。如果这个地方被毁了,两份地图都失效。
  • 深拷贝:复制了“地址”指向的实际内容,就像复制了一份一模一样的地方。两份地图指向的是两个独立但内容相同的地方。一个地方变了,另一个地方不受影响。一个地方被毁了,另一个地方依然存在。