九和风头像
关注

深拷贝与浅拷贝

一、 概念

在C++中,拷贝构造函数用于用一个已存在的对象来初始化一个新对象。如果不显式定义,编译器会生成一个默认的拷贝构造函数。

  • 浅拷贝(Shallow Copy):简单的赋值拷贝操作。编译器默认实现的拷贝构造函数就是浅拷贝。

二、 浅拷贝带来的灾难:内存重复释放

1. 代码场景

假设我们有一个 Person 类,其中 m_Age 是整型,而 m_Height 是一个指针(为什么用指针?为了将其放入堆中,这样可以手动控制其生命周期,比如传入较大的数据)。

class Person {
public:
    int m_Age;
    int *m_Height; // 身高,指针,指向堆区

    // 有参构造函数
    Person(int age, int height) {
        m_Age = age;
        m_Height = new int(height); // 在堆区开辟空间
    }

    // 析构函数:将堆区开辟的数据做释放操作
    ~Person() {
        cout << "析构函数的调用" << endl;
        if (m_Height != NULL) {
            delete m_Height;
            m_Height = NULL;
        }
    }
};

void test01() {
    Person p1(18, 160);
    cout << "p1的年龄:" << p1.m_Age << " 身高:" << *p1.m_Height << endl;
    
    // 调用编译器默认的拷贝构造函数(浅拷贝)
    Person p2(p1); 
    cout << "p2的年龄:" << p2.m_Age << " 身高:" << *p2.m_Height << endl;
}

2. 内存图解剖析(结合手写笔记)

当执行 Person p2(p1); 发生浅拷贝时,内存布局如下:

  • 栈区

    • p1 在栈区(假设地址 0x0044),m_Age 为18,m_Height 指针存储的地址是 0x0011(指向堆区)。

    • p2 在栈区(假设地址 0x0041),m_Age 为18,m_Height 指针直接复制了 p1 的地址值 0x0011

  • 堆区

    • 地址 0x0011 处存放着 160

    • 此时,p1.m_Heightp2.m_Height 同时指向了同一块堆区内存

3. 问题爆发

test01 执行结束时,对象的析构顺序是先进后出(p2先析构,p1后析构):

  1. p2 先释放堆区内存:delete p2.m_Height; 此时 0x0011 的内存被释放。

  2. p1 随后释放堆区内存:delete p1.m_Height; 试图再次释放已经被释放的 0x0011

  3. 结果:引发 堆区内存重复释放(Double Free),程序崩溃(在笔记中被标注为“出现断点”)。

总结:浅拷贝带来的问题是堆区内存重复释放。解决方法:浅拷贝的问题要利用深拷贝解决。

三、 深拷贝的解决方案

为了解决浅拷贝带来的问题,我们必须自己实现拷贝构造函数,在堆区重新申请空间(深拷贝)。

1. 深拷贝构造函数实现

// 自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person &p) {
    cout << "拷贝构造函数的调用" << endl;
    m_Age = p.m_Age;
    
    // 浅拷贝:编译器默认实现,会导致重复释放
    // m_Height = p.m_Height; 
    
    // 深拷贝:重新在堆区申请空间
    m_Height = new int(*p.m_Height); // 解引用,将p指向的值赋给新开辟的空间
}

2. 内存变化图解

实现深拷贝后,再次执行 Person p2(p1);

  • 堆区会新开辟一块空间(假设地址 0x0022),并将 160 存入。

  • p2.m_Height 指向 0x0022

  • 此时 p1p2 的指针完全独立。析构时,p2 释放 0x0022p1 释放 0x0011,各自释放各自的内存,安全无副作用。

四、 完整代码与测试(补充)

结合笔记内容,完善的代码如下:

#include <iostream>
using namespace std;

class Person {
public:
    int m_Age;
    int *m_Height;

    // 默认构造函数
    Person() {
        cout << "默认构造函数的调用" << endl;
    }

    // 有参构造函数
    Person(int age, int height) {
        m_Age = age;
        m_Height = new int(height); // 指针在堆区申请内存
    }

    // 自定义拷贝构造函数(深拷贝)
    Person(const Person &p) {
        cout << "拷贝构造函数的调用" << endl;
        m_Age = p.m_Age;
        // m_Height = p.m_Height; // 浅拷贝,会导致堆区重复释放
        m_Height = new int(*p.m_Height); // 深拷贝:重新分配堆内存
    }

    // 析构函数
    ~Person() {
        cout << "析构函数的调用" << endl;
        if (m_Height != NULL) {
            delete m_Height;
            m_Height = NULL;
        }
    }
};

void test01() {
    Person p1(18, 160);
    cout << "p1的年龄:" << p1.m_Age << " 身高:" << *p1.m_Height << endl;

    Person p2(p1);
    cout << "p2的年龄:" << p2.m_Age << " 身高:" << *p2.m_Height << endl;
}

int main() {
    test01();
    return 0;
}

五、 面试高频考点与最佳实践

  1. 口诀如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。

  2. 三法则(Rule of Three):如果一个类需要自定义析构函数,那么它通常也需要自定义拷贝构造函数拷贝赋值运算符operator=)。因为需要析构通常意味着类管理了资源(如堆内存)。

  3. 现代C++建议

    • 在现代C++中,优先使用 std::stringstd::vector 等自带深拷贝语义的类,避免手动 new/delete

    • 如果必须动态分配资源,推荐使用 智能指针std::unique_ptrstd::shared_ptr),它们能自动管理内存,从根本上规避浅拷贝带来的悬空指针和内存泄漏问题。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_91257027/article/details/165364771

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--