【C++】022、深拷贝浅拷贝
一、深拷贝&浅拷贝
深拷贝与浅拷贝的根本区别,不在于复制了多少字节,在于拷贝后,两个对象是否共享了同一个底层资源
浅拷贝:仅仅逐字节复制对象本身的内存布局,两个对象中的指针成员指向同一块动态内存。
深拷贝:递归复制指针所指向的全部底层数据,为副本独立分配一块新的动态内存。
二、代码解析与示意图
1、浅拷贝,编译器默认行为
原始对象 s1 (栈) 堆内存 (0x1000) ┌─────────────┐ ┌─────────────┐ │ char* m_ptr │──────│ H E L L O \0│ │ int m_len=5 │ └─────────────┘ └─────────────┘ 拷贝对象 s2 (栈) ┌─────────────┐ │ char* m_ptr │──────(指向同一块 0x1000) ❌ 共享了! │ int m_len=5 │ └─────────────┘2、深拷贝,手动处理
原始对象 s1 (栈) 堆内存 (0x1000) ┌─────────────┐ ┌─────────────┐ │ char* m_ptr │──────│ H E L L O \0│ │ int m_len=5 │ └─────────────┘ └─────────────┘ 拷贝对象 s2 (栈) 堆内存 (0x2000) ✅ 独立新家! ┌─────────────┐ ┌─────────────┐ │ char* m_ptr │──────│ H E L L O \0│ │ int m_len=5 │ └─────────────┘ └─────────────┘3、代码演示
#include <cstring> #include <iostream> class MyString { private: char* m_data; // 指向堆内存的指针 size_t m_len; public: // ---------- 构造函数 ---------- MyString(const char* str = "") : m_len(strlen(str)), m_data(new char[m_len + 1]) { strcpy(m_data, str); std::cout << "构造: " << m_data << std::endl; } // ---------- 析构函数 ---------- ~MyString() { delete[] m_data; // 释放堆内存 std::cout << "析构: 释放内存" << std::endl; } // ========================================== // ❌ 浅拷贝(编译器默认生成,如果不禁用会出大问题) // ========================================== // 默认拷贝构造:MyString(const MyString& other) // 默认拷贝赋值:MyString& operator=(const MyString& other) // 它们会执行:m_data = other.m_data; (逐字节复制指针值) // 后果:两个对象指向同一块内存,析构时双重释放(Double Free)-> 崩溃! // ========================================== // ✅ 深拷贝(必须手动实现) // ========================================== // 1. 拷贝构造函数(深拷贝) MyString(const MyString& other) : m_len(other.m_len), m_data(new char[m_len + 1]) { strcpy(m_data, other.m_data); // 复制数据到新内存 std::cout << "深拷贝构造: " << m_data << std::endl; } // 2. 拷贝赋值运算符(深拷贝 + 自我赋值检测) MyString& operator=(const MyString& other) { std::cout << "深拷贝赋值: " << other.m_data << std::endl; if (this == &other) { // 防止自我赋值:s = s; return *this; } // 步骤1:释放旧资源 delete[] m_data; // 步骤2:分配新内存并复制数据 m_len = other.m_len; m_data = new char[m_len + 1]; strcpy(m_data, other.m_data); return *this; } // 获取数据(打印用) const char* c_str() const { return m_data; } }; // ========== 测试代码 ========== int main() { MyString s1("Hello"); MyString s2(s1); // 调用深拷贝构造(独立内存) MyString s3("World"); s3 = s1; // 调用深拷贝赋值(s3 释放旧内存,申请新内存) std::cout << "s1: " << s1.c_str() << std::endl; std::cout << "s2: " << s2.c_str() << std::endl; std::cout << "s3: " << s3.c_str() << std::endl; return 0; // 三个对象析构,各自释放各自的内存,安全! }三、工程实战原则
1、三/五法则:
如果一个类需要自定义析构函数释放资源,那么一定需要自定义拷贝构造函数和拷贝赋值运算符,以及C11后的移动构造和移动赋值
反之,如果不需要析构函数,就不要自定义拷贝操作,直接让编译器生成默认的浅拷贝。
2、在业务代码中,使用string替换char*,用vector替换动态数组
因为他们内部已实现了深拷贝。
四、C11中的移动语义
C++中的移动语义既不是深拷贝,也不是浅拷贝,只是一种资源窃取
解析:
浅拷贝会导致两个对象共享资源,存在重复释放资源危险
深拷贝会复制一份数据(昂贵)
移动是把源对象的指针直接置空(nullptr),并把指针值偷给目标对象,移动后的源对象处于有效但未指定的状态,他的析构函数不会再释放资源。
