6. 深拷贝与浅拷贝:默认拷贝构造函数的陷阱、自定义拷贝构造函数实现深拷贝、资源管理

拷贝构造函数,说白了就是用一个对象去初始化另一个同类型的对象。听起来很简单对吧?但这里头的水,比你想象的要深得多。我见过太多人在这个坑里摔得鼻青脸肿,包括我自己刚入行那会儿。

6.1 默认拷贝构造函数:编译器替你做的“好事”

如果你没有显式定义拷贝构造函数,编译器会帮你生成一个默认的。这个默认版本做的事情很简单——逐字节复制。也就是把源对象的每个成员变量,原封不动地拷贝到目标对象中。

对于基本类型(int、double、指针等),这没问题。但一旦涉及到动态内存,麻烦就来了。

核心概念:浅拷贝(Shallow Copy)只复制指针的值,不复制指针指向的内存。深拷贝(Deep Copy)会复制指针指向的实际数据。

6.2 浅拷贝的陷阱:一个经典的崩溃案例

来看一段代码,我当年在实习时就写过类似的:

class StringBad {
private:
    char* str;
    int len;
public:
    StringBad(const char* s) {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }
    ~StringBad() {
        delete[] str;  // 释放内存
    }
};

int main() {
    StringBad s1("Hello");
    StringBad s2 = s1;  // 使用默认拷贝构造函数
    // 程序结束时,s1和s2的析构函数都会执行
    // 同一块内存被释放两次 —— 崩溃!
    return 0;
}

为什么会这样?默认拷贝构造函数把s1.str的地址直接复制给了s2.str。两个对象指向同一块堆内存。当它们离开作用域时,析构函数会先后释放同一块内存。第二次释放时,程序直接崩溃。

警告:浅拷贝导致的“双重释放”(double free)是C++中最常见的崩溃原因之一。我曾经在一个线上服务中排查了整整两天,最后发现就是这个问题。

6.3 自定义拷贝构造函数:实现深拷贝

解决方案很直接——我们自己写拷贝构造函数,让它做深拷贝:

class StringGood {
private:
    char* str;
    int len;
public:
    StringGood(const char* s) {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }
    
    // 自定义拷贝构造函数 —— 深拷贝
    StringGood(const StringGood& other) {
        len = other.len;
        str = new char[len + 1];  // 分配新内存
        strcpy(str, other.str);   // 复制数据
        // 现在每个对象都有自己的独立内存
    }
    
    ~StringGood() {
        delete[] str;
    }
};

深拷贝的核心就两句话:先分配自己的内存,再复制数据过去。这样每个对象都拥有独立的内存空间,析构时各删各的,互不干扰。

6.4 资源管理的三/五法则

说到资源管理,就不得不提C++的三/五法则(Rule of Three/Five)。这个法则我建议你刻在脑子里:

需要自定义的资源操作 必须同时定义的函数
析构函数 拷贝构造函数、拷贝赋值运算符
拷贝构造函数 析构函数、拷贝赋值运算符
拷贝赋值运算符 析构函数、拷贝构造函数

说白了,如果你需要手动管理资源(new/delete、文件句柄、互斥锁等),这三个函数就得一起上。C++11之后还加上了移动构造函数和移动赋值运算符,变成了五法则。

个人建议:能用标准库容器(std::string、std::vector)就别自己管理内存。它们已经实现了正确的拷贝语义,你直接用就行,省心省力。

6.5 知识体系总览

下面这张图把深拷贝与浅拷贝的核心逻辑串起来了,你可以对照着理解:

深拷贝 vs 浅拷贝 核心逻辑 浅拷贝(默认行为) 逐字节复制指针值 两个对象指向同一块内存 后果:双重释放 → 崩溃 深拷贝(自定义实现) 分配独立的新内存 复制数据到新内存 结果:各管各的内存,安全 三/五法则:有资源就要深拷贝 判断标准:类中是否有指针成员或动态分配的资源

6.6 避坑指南与最佳实践

我总结了几条实战经验,希望能帮你少走弯路:

  • 默认拷贝构造函数不是万能的——只要类里有指针成员,你就得警惕。我曾经接手过一个项目,整个代码库因为一个浅拷贝bug,导致内存泄漏了半年没人发现。
  • 拷贝赋值运算符也要深拷贝——很多人只关注拷贝构造函数,忘了赋值操作。记住,s2 = s1String s2 = s1是两回事,前者调用赋值运算符,后者调用拷贝构造函数。
  • 考虑使用智能指针——C++11的std::shared_ptrstd::unique_ptr能帮你自动管理资源。但要注意,shared_ptr的拷贝是共享所有权,不是深拷贝,语义不同。
  • 禁用拷贝语义——如果某个类不应该被拷贝(比如管理互斥锁的类),用= delete明确禁止:MyClass(const MyClass&) = delete;

一句话总结:浅拷贝复制指针,深拷贝复制数据。有动态资源的地方,就要有深拷贝。这是C++资源管理的基石,也是你从新手走向资深工程师的必经之路。


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