4.深拷贝VS浅拷贝

核心定义

  • 浅拷贝 (Shallow Copy) :只复制对象本身和对象内的指针地址,不复制指针所指向的资源。

  • 深拷贝 (Deep Copy) :不仅复制对象本身,还 重新分配内存 ,完整地复制指针所指向的资源。 可以把它们比作 复印笔记 :

  • 浅拷贝 :只复印了笔记的 目录 。两份目录指向的都是 同一本原始笔记 。你修改了原始笔记,两份复印件看到的内容都变了。

  • 深拷贝 :把整本笔记 从头到尾完整地复印 了一遍。你现在有了两本完全独立、互不相干的笔记。

详细对比

代码层面(面试时可以口述这个逻辑)

想象一个简单的字符串类,它内部有一个 char* 指针指向动态分配的内存:

arduino 复制代码
class MyString {
public:
    MyString(const char* text) {
        p_data = new char[strlen
        (text) + 1];
        strcpy(p_data, text);
    }
    ~MyString() {
        delete[] p_data;
    }
    // 如果我们不写下面的拷贝构造函数,编
    译器会生成一个浅拷贝的版本
    // MyString(const MyString& 
    other) { p_data = other.
    p_data; } // 浅拷贝

    // 这是我们必须手动实现的深拷贝
    MyString(const MyString& other) 
    {
        // 1. 为新对象分配独立的内存
        p_data = new char[strlen
        (other.p_data) + 1];
        // 2. 将内容复制过来
        strcpy(p_data, other.
        p_data);
    }

private:
    char* p_data;
};

向面试官总结:

"深拷贝和浅拷贝的核心区别在于如何处理 堆上 的资源。

浅拷贝 是编译器默认的行为,它只是简单地复制指针的值,导致多个对象共享同一个资源。这会带来两个致命问题:一是数据修改会互相影响;二是对象析构时会重复释放同一块内存,导致程序崩溃。

为了解决这个问题,我们就必须实现 深拷贝 。这意味着在拷贝构造函数和拷贝赋值运算符中,我们要手动为新对象申请一块全新的内存,并将原始资源的内容完整地复制过去,从而确保每个对象都有自己独立的资源副本,互不干扰。"

加分项(体现你的知识广度):

"在现代C++中,我们更推荐使用 智能指针 (如 std::shared_ptr 和 std::unique_ptr )来管理动态资源。这样,我们就可以利用它们自带的拷贝和移动语义,让编译器自动处理好深拷贝和所有权问题,从而遵循' 零法则 (Rule of Zero) ',避免手动编写这些复杂的拷贝控制函数,让代码更安全、更简洁。"

深拷贝怎么写

在C++中,当你的类里包含了 动态分配的资源 (比如通过 new 创建的指针),为了实现深拷贝,你必须遵守一个被称为" 三法则 "(Rule of Three)的经典规则。这意味着你需要亲自实现三个特殊的成员函数:

  1. 析构函数 (Destructor)
  2. 拷贝构造函数 (Copy Constructor)
  3. 拷贝赋值运算符 (Copy Assignment Operator)

我们就以上一题的 MyString 类为例,一步步写出完整的深拷贝实现。

1. 基础类定义

首先,我们有一个管理动态字符串的类,它在构造时 new 一块内存,在析构时 delete 它。

c 复制代码
#include <cstring> // for strlen, 
strcpy
#include <iostream>

class MyString {
private:
    char* p_data; // 指向堆上内存的指
    针

public:
    // 构造函数: 从一个C风格字符串创建
    MyString
    MyString(const char* text = "") 
    {
        std::cout << "调用了构造函数" 
        << std::endl;
        p_data = new char[strlen
        (text) + 1];
        strcpy(p_data, text);
    }

    // 1. 析构函数: 释放资源
    ~MyString() {
        std::cout << "调用了析构函数, 
        释放了 " << (void*)p_data 
        << std::endl;
        delete[] p_data;
    }

    // ... 接下来实现拷贝控制函数 ...

    void print() const {
        std::cout << "[" << (void*)
        p_data << "] " << p_data << 
        std::endl;
    }
};

2. 实现拷贝构造函数

当用一个已有的对象来 创建 一个新对象时,拷贝构造函数会被调用。例如: MyString s2 = s1; 或 MyString s2(s1); 。

核心步骤:

    为新对象 p_data 分配一块 新的 内存。
    将源对象 p_data 指向的内容 复制 到新内存中。
c 复制代码
// ... 在MyString类中添加 ...

// 2. 拷贝构造函数 (深拷贝)
MyString(const MyString& other) {
    std::cout << "调用了拷贝构造函数 
    (深拷贝)" << std::endl;
    // 为新对象分配独立的内存
    p_data = new char[strlen(other.
    p_data) + 1];
    // 将源对象的内容复制过来
    strcpy(p_data, other.p_data);
}

3. 实现拷贝赋值运算符

当用一个已有的对象去 赋值给 另一个已有的对象时,拷贝赋值运算符会被调用。例如: s2 = s1; 。

核心步骤:

  1. 检查自赋值 :这是最重要的第一步!防止 s1 = s1; 这种操作导致提前释放资源。
  2. 释放旧资源 : delete 掉当前对象 p_data 指向的旧内存。
  3. 分配新资源 : new 一块新内存。
  4. 复制内容 :将源对象的内容复制过来。
  5. 返回 *this :为了支持链式赋值,如 s3 = s2 = s1; 。
arduino 复制代码
// ... 在MyString类中添加 ...

// 3. 拷贝赋值运算符 (深拷贝)
MyString& operator=(const MyString& 
other) {
    std::cout << "调用了拷贝赋值运算符 
    (深拷贝)" << std::endl;
    
    // 1. 检查是否是自我赋值
    if (this != &other) {
        // 2. 释放当前对象已有的资源
        delete[] p_data;

        // 3. 分配新内存
        p_data = new char[strlen
        (other.p_data) + 1];

        // 4. 复制内容
        strcpy(p_data, other.
        p_data);
    }

    // 5. 返回当前对象的引用
    return *this;
}

完整的示例代码

把它们组合起来,就是一个完整的、实现了深拷贝的 MyString 类。

c 复制代码
#include <cstring>
#include <iostream>

class MyString {
private:
    char* p_data;

public:
    // 构造函数
    MyString(const char* text = "") 
    {
        std::cout << "调用了构造函数" 
        << std::endl;
        p_data = new char[strlen
        (text) + 1];
        strcpy(p_data, text);
    }

    // 1. 析构函数
    ~MyString() {
        std::cout << "调用了析构函数, 
        释放了 " << (void*)p_data 
        << std::endl;
        delete[] p_data;
    }

    // 2. 拷贝构造函数 (深拷贝)
    MyString(const MyString& other) 
    {
        std::cout << "调用了拷贝构造函
        数 (深拷贝)" << std::endl;
        p_data = new char[strlen
        (other.p_data) + 1];
        strcpy(p_data, other.
        p_data);
    }

    // 3. 拷贝赋值运算符 (深拷贝)
    MyString& operator=(const 
    MyString& other) {
        std::cout << "调用了拷贝赋值运
        算符 (深拷贝)" << std::endl;
        if (this != &other) {
            delete[] p_data;
            p_data = new char[strlen
            (other.p_data) + 1];
            strcpy(p_data, other.
            p_data);
        }
        return *this;
    }

    void print() const {
        std::cout << "[" << (void*)
        p_data << "] " << p_data << 
        std::endl;
    }
};

int main() {
    std::cout << "--- 创建 s1 ---" 
    << std::endl;
    MyString s1("Hello");
    s1.print();

    std::cout << "\n--- 创建 s2 (拷贝
    构造) ---" << std::endl;
    MyString s2 = s1; // 调用拷贝构造
    函数
    s1.print();
    s2.print(); // s1和s2的指针地址不
    同,证明是深拷贝

    std::cout << "\n--- 创建 s3 
    ---" << std::endl;
    MyString s3("World");
    s3.print();

    std::cout << "\n--- s3 赋值 (拷贝
    赋值) ---" << std::endl;
    s3 = s1; // 调用拷贝赋值运算符
    s1.print();
    s3.print(); // s1和s3的指针地址也
    不同

    std::cout << "\n--- 程序结束,自动
    调用析构 ---" << std::endl;
    return 0;
}

当你运行这段代码,你会清晰地看到构造、拷贝、赋值和析构函数的调用顺序,并且每个对象的指针地址都是独立的,这就完美地实现了深拷贝。

相关推荐
小兔兔吃萝卜9 分钟前
Spring 创建 Bean 的 8 种主要方式
java·后端·spring
大锦终26 分钟前
【算法】模拟专题
c++·算法
Java中文社群33 分钟前
26届双非上岸记!快手之战~
java·后端·面试
whitepure40 分钟前
万字详解Java中的面向对象(一)——设计原则
java·后端
autumnTop40 分钟前
为什么访问不了同事的服务器或者ping不通地址了?
前端·后端·程序员
方传旺42 分钟前
C++17 std::optional 深拷贝 vs 引用:unordered_map 查询大对象性能对比
c++
Dontla1 小时前
Makefile介绍(Makefile教程)(C/C++编译构建、自动化构建工具)
c语言·c++·自动化
用户6757049885021 小时前
SQL 判断是否“存在”?99% 的人还在写错!
后端