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管理的动态内存,就必须手动实现深拷贝(拷贝构造函数 + 赋值运算符重载),否则会触发浅拷贝的内存安全问题。

相关推荐
小肝一下4 分钟前
3. 单链表
c语言·数据结构·c++·算法·leetcode·链表·dijkstra
劈星斩月11 分钟前
监督学习算法 之 决策树
学习·算法·决策树
学计算机的计算基16 分钟前
操作系统内存管理全解:虚拟内存、页表、COW、malloc、OOM一篇搞定
java·笔记·算法
tachibana220 分钟前
hot100 前 K 个高频元素(347)
java·数据结构·算法·leetcode
AOwhisky22 分钟前
Python 学习笔记(第五期)——组合数据类型:列表、元组、集合与字典精讲——核心知识点自测与详解
开发语言·笔记·python·学习·云计算
小保CPP25 分钟前
OpenCV C++提取webp动图里的图片
c++·人工智能·opencv·计算机视觉
To_OC8 小时前
LC 131 分割回文串:刚学回溯时,我连怎么切字符串都想不明白
javascript·算法·leetcode
炸膛坦客9 小时前
单片机/C/C++八股:(二十六)IIC 专题(I²C)---- 上集
c语言·c++·单片机
旖-旎9 小时前
LeetCode 518:零钱兑换||(完全背包)—— 题解
c++·算法·leetcode·动态规划·背包问题
灯澜忆梦9 小时前
GO_并发编程---定时器
开发语言·后端·golang