0

0

C++内存管理基础中浅拷贝和深拷贝的实现方法

P粉602998670

P粉602998670

发布时间:2025-09-05 10:37:01

|

456人浏览过

|

来源于php中文网

原创

浅拷贝仅复制指针值导致多对象共享同一内存,析构时可能引发重复释放和悬空指针;深拷贝通过自定义拷贝构造函数和赋值运算符为指针成员分配新内存并复制内容,确保对象独立性,避免内存错误。

c++内存管理基础中浅拷贝和深拷贝的实现方法

在C++的内存管理中,理解浅拷贝和深拷贝是避免诸多内存错误的关键,简单来说,浅拷贝只是复制了对象成员的“值”,如果这些值是指针,那么新旧对象会共享同一块内存;而深拷贝则会为指针指向的资源也开辟新的内存空间,确保每个对象拥有独立的资源副本。

解决方案

要实现C++中的浅拷贝和深拷贝,我们通常需要关注类的成员变量,尤其是那些指向动态分配内存的指针。

浅拷贝(Shallow Copy)

C++的默认拷贝行为就是浅拷贝。当你没有为类定义拷贝构造函数或拷贝赋值运算符时,编译器会自动生成它们,这些默认生成的函数会逐个成员地复制(member-wise copy)。如果类中包含指向动态分配内存的指针,那么新对象和原对象的指针将指向同一块内存区域。

立即学习C++免费学习笔记(深入)”;

考虑一个简单的例子:

class MyString {
public:
    char* data;
    MyString(const char* s) {
        data = new char[strlen(s) + 1];
        strcpy(data, s);
    }
    ~MyString() {
        delete[] data;
    }
};

// ... 在main函数中
MyString s1("Hello");
MyString s2 = s1; // 默认拷贝构造,浅拷贝
// 此时 s1.data 和 s2.data 指向同一块内存

这里的

s2 = s1
导致
s1.data
s2.data
指向同一块内存。当
s1
s2
中的任何一个被销毁时,它会
delete[] data
,导致另一对象的
data
变成悬空指针。更糟糕的是,当两个对象都销毁时,同一块内存会被
delete[]
两次,这通常会导致程序崩溃。

深拷贝(Deep Copy)

为了解决浅拷贝带来的问题,我们需要实现深拷贝。深拷贝意味着当复制对象时,如果对象内部包含指向动态分配内存的指针,我们不仅复制指针本身,还要为指针指向的内容也分配新的内存,并将内容复制过去。这通常通过自定义拷贝构造函数和拷贝赋值运算符来完成。

#include 
#include  // For strlen and strcpy

class MyString {
public:
    char* data;
    size_t length;

    // 构造函数
    MyString(const char* s = "") {
        length = strlen(s);
        data = new char[length + 1];
        strcpy(data, s);
        std::cout << "Constructor called for: " << data << std::endl;
    }

    // 析构函数
    ~MyString() {
        std::cout << "Destructor called for: " << data << std::endl;
        delete[] data;
        data = nullptr; // 避免悬空指针
    }

    // 拷贝构造函数 (深拷贝实现)
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1]; // 分配新的内存
        strcpy(data, other.data);    // 复制内容
        std::cout << "Deep Copy Constructor called for: " << data << std::endl;
    }

    // 拷贝赋值运算符 (深拷贝实现)
    MyString& operator=(const MyString& other) {
        if (this == &other) { // 处理自我赋值
            return *this;
        }
        // 释放旧资源
        delete[] data;

        // 分配新资源并复制内容
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        std::cout << "Deep Copy Assignment Operator called for: " << data << std::endl;
        return *this;
    }

    // 获取字符串内容
    const char* c_str() const {
        return data;
    }
};

// 示例用法
// int main() {
//     MyString s1("Hello, World!");
//     MyString s2 = s1; // 调用拷贝构造函数
//     MyString s3("C++");
//     s3 = s1;          // 调用拷贝赋值运算符
//
//     std::cout << "s1: " << s1.c_str() << std::endl;
//     std::cout << "s2: " << s2.c_str() << std::endl;
//     std::cout << "s3: " << s3.c_str() << std::endl;
//
//     // 修改s1不会影响s2和s3,因为它们拥有独立的内存
//     // (如果MyString有修改方法,这里可以展示)
//
//     return 0;
// }

在这个

MyString
类的深拷贝实现中,
MyString(const MyString& other)
拷贝构造函数和
operator=(const MyString& other)
拷贝赋值运算符都确保了为
data
指针分配了新的内存,并复制了
other.data
的内容。这样,每个
MyString
对象都拥有自己独立的字符串数据,互不影响。

为什么C++默认的拷贝行为会引发内存问题?

C++默认的拷贝行为,也就是我们常说的浅拷贝,其核心问题在于它只复制了对象成员的“值”。对于那些基本类型(如

int
,
double
)或者其他没有动态内存管理的类对象,这通常没什么问题。但一旦类中包含了指向堆上动态分配内存的指针(比如
char*
int*
),麻烦就来了。

想象一下,你有一个

MyString
对象
s1
,它的
data
指针指向了一块包含“Hello”的内存。当你用
s1
去初始化
s2
MyString s2 = s1;
)时,如果采用默认的浅拷贝,
s2.data
会直接复制
s1.data
的值,这意味着
s2.data
也指向了
s1.data
所指向的同一块“Hello”内存。此时,
s1
s2
实际上共享着同一份资源。

这种共享资源的方式会带来几个严重的后果:

  1. 重复释放(Double Free):当
    s1
    的生命周期结束,它的析构函数会被调用,
    delete[] s1.data
    会释放那块“Hello”内存。随后,当
    s2
    的生命周期也结束时,它的析构函数会再次尝试
    delete[] s2.data
    ,而
    s2.data
    仍然指向已经被释放的同一块内存。对一块已经释放的内存进行二次释放是未定义行为,通常会导致程序崩溃。
  2. 悬空指针(Dangling Pointer):在
    s1
    被销毁并释放内存后,
    s2.data
    仍然指向那块已经不再有效的内存区域。此时
    s2.data
    就成了一个悬空指针。任何通过
    s2.data
    访问内存的操作都可能导致程序崩溃或产生不可预测的结果。
  3. 意外修改:如果通过
    s1
    修改了
    data
    指向的内容,那么
    s2
    也会“看到”这些修改,反之亦然。这违背了对象独立性的原则,可能导致程序逻辑混乱。

所以,默认的浅拷贝行为对于管理动态内存的类来说,几乎总是一个陷阱。它假定所有成员都是独立的,但指针成员的“值”只是一个地址,真正的资源在地址后面,这才是需要独立复制的。

察言观数AskTable
察言观数AskTable

企业级AI数据表格智能体平台

下载

如何正确实现C++类的深拷贝:关键步骤与注意事项

正确实现深拷贝是C++中一个基础但又极其重要的技能,它确保了对象之间的数据独立性。这通常涉及到“三/五/零法则”(Rule of Three/Five/Zero),即如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,那么它很可能需要自定义所有这三个(或更多,考虑到C++11的移动语义)。

核心步骤:

  1. 析构函数 (

    ~ClassName()
    )

    • 这是深拷贝的基础。在析构函数中,必须释放所有由当前对象动态分配的内存资源。
    • 例如:
      delete[] data;
      之后,最好将
      data
      设置为
      nullptr
      ,以避免悬空指针,尽管对于即将销毁的对象来说,这更多是一种良好的编程习惯。
  2. 拷贝构造函数 (

    ClassName(const ClassName& other)
    )

    • 当一个新对象通过另一个同类型对象进行初始化时(例如
      MyString s2 = s1;
      MyString s2(s1);
      ),会调用拷贝构造函数。
    • 步骤:
      • 复制
        other
        对象的所有非指针成员(值类型成员)。
      • 对于
        other
        对象中的每一个动态分配的资源(通过指针持有),为当前新对象 分配新的内存
      • other
        对象对应资源的内容 复制 到当前新分配的内存中。
    • 示例:
      MyString(const MyString& other) {
          length = other.length;
          data = new char[length + 1]; // 分配新内存
          strcpy(data, other.data);    // 复制内容
      }
  3. 拷贝赋值运算符 (

    ClassName& operator=(const ClassName& other)
    )

    • 当一个已存在的对象被另一个同类型对象赋值时(例如
      s3 = s1;
      ),会调用拷贝赋值运算符。
    • 步骤:
      • 自我赋值检查:首先检查
        this == &other
        。如果两个对象是同一个,直接返回
        *this
        ,避免释放自己正在使用的资源。
      • 释放旧资源:当前对象可能已经持有一些动态资源,在接收新数据之前,必须先释放这些旧资源,防止内存泄漏。
      • 分配新资源:为
        other
        对象中的动态资源分配新的内存。
      • 复制内容:将
        other
        对象对应资源的内容复制到当前新分配的内存中。
      • *返回 `this
        **:允许链式赋值(
        a = b = c;`)。
    • 示例:
      MyString& operator=(const MyString& other) {
          if (this == &other) { // 自我赋值检查
              return *this;
          }
          delete[] data; // 释放旧资源
          length = other.length;
          data = new char[length + 1]; // 分配新内存
          strcpy(data, other.data);    // 复制内容
          return *this;
      }

注意事项:

  • 异常安全(Exception Safety):上述的拷贝赋值运算符在
    new char[length + 1]
    失败时,
    data
    可能已经被
    delete[]
    ,但新的内存分配失败,导致对象处于无效状态。更健壮的实现会采用 copy-and-swap idiom,它通过创建一个临时副本,然后交换资源来提供强大的异常安全保证。
    // 采用 copy-and-swap idiom 实现拷贝赋值运算符
    MyString& operator=(MyString other) { // 注意这里other是按值传递,会调用拷贝构造函数
        swap(*this, other); // 交换资源
        return *this;
    }
    // 还需要一个友元函数或成员函数来执行交换
    friend void swap(MyString& first, MyString& second) {
        using std::swap; // 允许ADL查找std::swap
        swap(first.data, second.data);
        swap(first.length, second.length);
    }

    这种方式在

    other
    构造时(按值传递)就完成了资源分配和复制,如果失败会抛出异常,不会影响当前对象。

  • 资源类型:深拷贝不仅适用于原始指针,也适用于
    std::vector
    std::string
    等容器,但这些标准库容器通常已经实现了深拷贝,所以你只需要确保你的类正确地拷贝了这些容器对象即可,无需手动管理它们的内部内存。
  • C++11的移动语义(Move Semantics):为了优化性能,避免不必要的深拷贝,C++11引入了移动构造函数和移动赋值运算符(Rule of Five)。它们通过“窃取”临时对象的资源来避免深拷贝,大大提高了效率。虽然不是深拷贝的直接实现,但在现代C++中,一个管理资源的类通常会同时实现这五个特殊成员函数(析构、拷贝构造、拷贝赋值、移动构造、移动赋值)。

浅拷贝在C++中是否有用武之地?何时可以安全使用它?

虽然深拷贝在处理动态内存时至关重要,但浅拷贝并非一无是处。在某些特定场景下,浅拷贝不仅安全,而且是更高效或更符合逻辑的选择。关键在于理解“所有权”的概念。

  1. 当类不包含任何动态分配的资源时

    • 如果一个类只包含基本类型成员(
      int
      ,
      double
      ,
      bool
      等)、枚举类型、或者其他本身就实现了深拷贝的类对象(如
      std::string
      ,
      std::vector
      ),那么默认的浅拷贝行为就足够了,因为它等同于深拷贝。
    • 例如:
      struct Point {
          int x;
          int y;
      };
      Point p1 = {10, 20};
      Point p2 = p1; // 浅拷贝,但因为没有动态资源,等同于深拷贝
    • 在这种情况下,让编译器自动生成拷贝构造函数和拷贝赋值运算符是最佳实践,这遵循了“零法则”(Rule of Zero)。
  2. 当对象不“拥有”其指向的资源,而是作为“视图”或“引用”存在时

    • 有时,一个类可能只是持有指向外部资源的指针或引用,它并不负责这些资源的生命周期管理。在这种情况下,复制这个指针或引用(浅拷贝)是完全合理的。
    • 例如,一个
      StringView
      类可能只持有指向
      char
      数组的指针和长度,它不
      new
      也不
      delete
      这块内存。它的作用是提供对现有字符串的只读访问。
      class StringView {
      public:
          const char* str;
          size_t len;
          StringView(const char* s, size_t l) : str(s), len(l) {}
          // 默认拷贝构造和赋值运算符就是浅拷贝,且是正确的
          // 因为StringView不拥有str指向的内存,不负责释放
      };
    • 在这种情况下,如果强制进行深拷贝,反而会创建不必要的内存副本,并引入新的内存管理问题。
  3. 性能优化(极少数情况)

    • 对于包含大量数据但很少修改的复杂对象,如果深拷贝的开销非常大,并且我们能确保在拷贝后不会修改原对象或副本,或者通过其他机制(如写时复制,Copy-on-Write)来管理,那么浅拷贝可能是一种性能上的考量。但这通常需要更复杂的内存管理策略来弥补浅拷贝的潜在风险。
    • 然而,这种场景非常罕见且容易出错,通常不推荐在没有充分理由和严密设计的情况下使用。

总结来说,浅拷贝是安全的,并且在不涉及动态内存管理或仅作为资源引用/视图时是正确的选择。它的风险主要在于,当类中存在指向堆内存的指针,并且这些指针代表了“所有权”时,默认的浅拷贝会破坏这种所有权模型,导致内存泄漏或重复释放。因此,关键在于明确你的类对资源是“拥有”还是“引用”。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

483

2023.08.02

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1503

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

233

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

87

2025.10.17

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

531

2023.09.20

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

320

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1503

2023.10.24

漫画免费在线观看地址大全
漫画免费在线观看地址大全

想找免费又资源丰富的漫画网站?本合集精选2025-2026年热门平台,涵盖国漫、日漫、韩漫等多类型作品,支持高清流畅阅读与离线缓存。阅读专题下面的文章了解更多详细内容。

1

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
C# 教程
C# 教程

共94课时 | 8万人学习

C 教程
C 教程

共75课时 | 4.3万人学习

C++教程
C++教程

共115课时 | 14.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号