C++中深拷贝与浅拷贝的原理

复制代码
浅拷贝和深拷贝是针对包含指针/动态内存成员的类对象的两种拷贝方式,核心差异在于:是否为指针成员重新分配内存,还是仅复制指针本身
可以用一个一个比喻理解:
浅拷贝:你有一把钥匙(指针)开了一个房间(堆内存),浅拷贝只是复制了这把钥匙,两个钥匙都能开同一个房间;
深拷贝:不仅复制钥匙,还重新建了一个一模一样的房间,两把钥匙开各自的房间,互不影响。

一、浅拷贝

1、原理

浅拷贝是c++编译器默认升成的拷贝构造函数/赋值运算符的行为:

·对普通成员变量(如int、string):直接复制值

·对指针成员变量:仅复制指针的地址(而非指针指向的堆内存内容);

·最终结果:原对象和拷贝对象的指针指向同一块堆内存

2、代码示例(浅拷贝的问题)

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class String {
private:
    char* str; // 动态分配内存的指针成员
public:
    // 构造函数
    String(const char* s = "") {
        str = new char[strlen(s) + 1]; // 堆上分配内存
        strcpy(str, s);
    }

    // 析构函数:释放堆内存
    ~String() {
        delete[] str; // 释放指针指向的内存
    }

    // 打印字符串
    void print() const {
        cout << str << endl;
    }
};

int main() {
    String s1("Hello");
    String s2 = s1; // 浅拷贝:s2.str 和 s1.str 指向同一块内存

    s1.print(); // 输出 Hello
    s2.print(); // 输出 Hello

    // 程序结束时,s2先析构,释放了内存;s1析构时再次释放同一块内存,导致**双重释放**(程序崩溃)
    return 0;
}
问题分析:
s2 是 s1 的浅拷贝,两者的str指针指向同一块堆内存;
析构时,s2 先释放内存,s1 析构时尝试释放已释放的内存,触发内存错误(double free)。

二. 深拷贝(Deep Copy)

深拷贝需要手动实现拷贝构造函数和赋值运算符重载,其行为是:

不仅复制对象的成员变量,还会为指针成员重新分配独立的堆内存;

复制指针指向的内存内容到新分配的内存中;

最终两个对象拥有独立的内存空间,互不影响。

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class String {
private:
    char* str;
public:
    // 构造函数
    String(const char* s = "") {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }

    // 深拷贝构造函数(核心)
    String(const String& other) {
        // 1. 为当前对象分配独立的堆内存
        str = new char[strlen(other.str) + 1];
        // 2. 复制other指针指向的内容到新内存
        strcpy(str, other.str);
    }

    // 深拷贝赋值运算符重载(核心)
    String& operator=(const String& other) {
        // 防止自赋值(s1 = s1)
        if (this == &other) {
            return *this;
        }

        // 1. 释放当前对象已有的堆内存
        delete[] str;
        // 2. 分配新内存并复制内容
        str = new char[strlen(other.str) + 1];
        strcpy(str, other.str);

        return *this;
    }

    // 析构函数
    ~String() {
        delete[] str;
    }

    // 打印字符串
    void print() const {
        cout << str << endl;
    }

    // 修改字符串(验证独立性)
    void set(const char* s) {
        delete[] str;
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
};

int main() {
    String s1("Hello");
    String s2 = s1; // 深拷贝:s2有独立的内存

    s1.set("World"); // 修改s1的内容,s2不受影响
    s1.print(); // 输出 World
    s2.print(); // 输出 Hello

    return 0;
}
关键说明:
拷贝构造函数:先为当前对象分配内存,再复制内容;
赋值运算符重载:先释放自身内存→防止自赋值→分配新内存→复制内容;
最终 s1 和 s2 的str指向不同内存,修改其中一个不会影响另一个。

a)浅拷贝

1、浅拷贝的内存表现

假设我们有一个包含动态内存的类对象,其内存布局如下(以 32 位系统为例):`对象 s1:

±---------+

| 指针 str | → 0x1000 (堆内存,存储 "Hello")

±---------+

浅拷贝生成 s2:

±---------+

| 指针 str | → 0x1000 (和s1指向同一块堆内存)

±---------+

  1. 浅拷贝的执行逻辑
    编译器生成的默认拷贝构造函数伪代码(逻辑等价):
cpp 复制代码
// 编译器自动生成的浅拷贝构造函数
String::String(const String& other) {
    // 直接复制指针的值(地址),而非指针指向的内容
    this->str = other.str; 
}
  1. 浅拷贝的核心问题根源
    内存所有权模糊:两个对象都认为自己拥有0x1000这块堆内存;
    析构时双重释放:第一个对象析构释放0x1000,第二个对象析构时再次释放已失效的内存,触发操作系统的内存保护机制(程序崩溃);
    修改相互影响:修改s1.str指向的内容,s2.str也会同步变化,违背对象的独立性原则。

b)深拷贝

深拷贝的本质是值复制(value copy)+ 独立内存分配,手动实现的拷贝逻辑会:

为目标对象分配全新的、独立的堆内存;

将源对象指针指向的数据内容复制到新内存中;

让目标对象的指针指向这个新内存,而非源对象的内存地址。

  1. 内存层面的表现

    对象 s1:

    ±---------+

    | 指针 str | → 0x1000 (堆内存,存储 "Hello")

    ±---------+

    深拷贝生成 s2:

    ±---------+

    | 指针 str | → 0x2000 (新分配的堆内存,复制了 "Hello")

    ±---------+

  2. 深拷贝的执行逻辑

    手动实现的深拷贝构造函数伪代码(逻辑等价):

cpp 复制代码
// 手动实现的深拷贝构造函数
String::String(const String& other) {
    // 步骤1:计算源对象数据的长度,为目标对象分配独立堆内存
    size_t len = strlen(other.str) + 1;
    this->str = new char[len]; // 新内存地址,比如0x2000
    
    // 步骤2:复制源对象指针指向的内容到新内存
    memcpy(this->str, other.str, len);
}
  1. 深拷贝解决问题的核心逻辑
    内存所有权唯一:每个对象拥有自己独立的堆内存,析构时只释放自己的内存,不会冲突;
    数据独立性:修改其中一个对象的堆内存数据,不会影响另一个对象,因为它们指向不同的内存块;
    避免野指针:赋值运算符重载时先释放自身旧内存,再分配新内存,杜绝内存泄漏和野指针

总结

浅拷贝是编译器默认的逐字节复制,仅复制指针地址,适用于无动态内存的简单类,但会导致多个对象共享堆内存,引发内存错误;

深拷贝需要手动实现,为指针成员重新分配独立内存并复制内容,保证对象独立性,是处理动态内存类的必选方案;

只要类中包含new/delete管理的动态内存,就必须手动实现深拷贝(拷贝构造函数 + 赋值运算符重载),否则会触发浅拷贝的内存安全问题。

相关推荐
发疯幼稚鬼2 小时前
图的存储与拓扑排序
数据结构·算法·排序算法·拓扑学
崇山峻岭之间2 小时前
Matlab学习记录16
开发语言·学习·matlab
Bruce_kaizy2 小时前
c++图论——生成树之Kruskal&Prim算法
c++·算法·图论
在屏幕前出油2 小时前
Python面向对象编程基础——类、实例对象与内存空间
开发语言·python
C++业余爱好者2 小时前
Hibernate 框架超详细说明
java·开发语言
wuk9982 小时前
基于MATLAB/Simulink实现交流异步电动机矢量控制的仿真
开发语言·matlab
零度@2 小时前
30条Java性能优化清单
java·开发语言
期待のcode2 小时前
Java的包装类
java·开发语言
aloha_7892 小时前
python基础面经八股
开发语言·python