【C++ 从基础到项目实战】C++(六):拷贝控制——浅拷贝与深拷贝,兼谈智能指针

📌 阅读时长:22分钟 | 关键词:C++、拷贝构造函数、浅拷贝、深拷贝、赋值运算符、智能指针、unique_ptr、shared_ptr

引言

上一篇文章我们学会了创建类、构造对象。但有一个容易踩坑的核心问题被有意跳过了:当一个对象被赋值给另一个,或者作为参数传递时,到底发生了什么? 如果你的类里有指针成员,浅拷贝会给你带来毁灭性的行为------两个对象共用一个资源,一个销毁了,另一个就变成悬挂指针。这篇文章就来把这个坑填平。

一、默认拷贝:自带的"浅拷贝"陷阱

编译器会自动生成一个拷贝构造函数 ,但它只做浅拷贝 ------逐字节复制,指针复制的是地址,不是地址指向的内容:

cpp 复制代码
class MyClass {
public:
    int *data;
    MyClass(int value) {
        data = new int(value);
    }

    // 编译器自动生成类似这样的拷贝构造函数:
    // MyClass(const MyClass &other) : data(other.data) {}  ← 浅拷贝!
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1;  // 浅拷贝:obj2.data 和 obj1.data 指向同一块内存

    std::cout << *obj1.data << std::endl;  // 10
    std::cout << *obj2.data << std::endl;  // 10

    // 💀 问题:两个对象指向同一块内存,析构时会 double delete!
}

浅拷贝的三大危害

问题 原因 后果
双重释放 两个指针指向同一块内存,析构时各自 delete 一次 程序崩溃
数据互相干扰 通过一个对象修改值,另一个"也变了" 逻辑错误
悬挂指针 一个对象先销毁释放了内存,另一个还在用 未定义行为

二、深拷贝:自己动手,丰衣足食

深拷贝 = 不仅拷贝指针本身,还要新分配一块内存,把内容一起拷过去:

cpp 复制代码
class MyClass {
public:
    int *data;

    MyClass(int value) {
        data = new int(value);
    }

    // 自定义深拷贝构造函数
    MyClass(const MyClass &other) {
        data = new int(*other.data);  // 新分配内存,拷贝值
    }

    ~MyClass() {
        delete data;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1;  // 深拷贝:各自拥有独立的内存

    *obj1.data = 20;
    std::cout << *obj2.data << std::endl;  // 仍然 = 10,互不影响 ✅
}

浅拷贝 vs 深拷贝图解

复制代码
浅拷贝:
  obj1.data ──→ [内存块: 10] ←── obj2.data  (两个指针指向同一块)

深拷贝:
  obj1.data ──→ [内存块: 10]
  obj2.data ──→ [内存块: 10]  (各自独立)

三、拷贝赋值运算符

除了拷贝构造(obj2 = obj1 在声明时),还有一种情况是赋值已有对象:

cpp 复制代码
class MyClass {
public:
    int *data;

    MyClass(int value) { data = new int(value); }

    // 深拷贝构造函数
    MyClass(const MyClass &other) { data = new int(*other.data); }

    // 深拷贝赋值运算符
    MyClass &operator=(const MyClass &other) {
        if (this != &other) {             // ⚠️ 防止自赋值
            delete data;                  // 先释放已有资源
            data = new int(*other.data);  // 再分配新资源并拷贝
        }
        return *this;
    }

    ~MyClass() { delete data; }
};

int main() {
    MyClass obj1(10), obj2(20);
    obj2 = obj1;  // 调用赋值运算符(深拷贝)
}

四、三五法则(Rule of Three)

如果一个类需要自定义析构函数 ,那么几乎一定也需要自定义拷贝构造函数拷贝赋值运算符

复制代码
Rule of Three(C++98):
 析构函数  +  拷贝构造函数  +  拷贝赋值运算符
                         ↓
            Rule of Five(C++11):
      再 + 移动构造函数 + 移动赋值运算符
cpp 复制代码
// 完整的三件套
class DataArray {
private:
    int *arr;
    int size;
public:
    DataArray(int s) : size(s), arr(new int[s]) {}

    DataArray(const DataArray &o) : size(o.size), arr(new int[o.size]) {   // 拷贝构造
        std::copy(o.arr, o.arr + size, arr);
    }

    DataArray &operator=(const DataArray &o) {                             // 拷贝赋值
        if (this != &o) {
            delete[] arr;
            size = o.size;
            arr = new int[size];
            std::copy(o.arr, o.arr + size, arr);
        }
        return *this;
    }

    ~DataArray() { delete[] arr; }                                         // 析构
};

五、智能指针:告别手动 delete

C++11 引入智能指针,自动管理内存,从根本上避免浅拷贝/忘记 delete 的坑。

5.1 unique_ptr:独占所有权

unique_ptr 不可复制,只能移动,确保只有一个指针拥有对象:

cpp 复制代码
#include <memory>

int main() {
    auto p1 = std::make_unique<int>(10);   // C++14 推荐写法

    // std::unique_ptr<int> p2 = p1;       // ❌ 不可复制!
    auto p2 = std::move(p1);               // ✅ 所有权转移
    // p1 现在为空

    if (p1) std::cout << *p1;              // 不会执行
    else    std::cout << "p1 已空" << std::endl;

    std::cout << *p2 << std::endl;         // 10
    // p2 离开作用域自动 delete
}

5.2 shared_ptr:共享所有权 + 引用计数

多个 shared_ptr 可共享同一块内存,最后一个释放时才 delete:

cpp 复制代码
#include <memory>

int main() {
    auto p1 = std::make_shared<int>(10);
    {
        auto p2 = p1;                  // 引用计数 1→2
        std::cout << *p2 << std::endl; // 10
    }                                  // p2 离开,引用计数 2→1

    std::cout << *p1 << std::endl;     // 10(内存还在)
}                                      // p1 离开,引用计数 1→0,自动 delete

5.3 裸指针 vs 智能指针

特性 裸指针 T* unique_ptr shared_ptr
所有权 无约束 独占 共享
复制 ❌(只能移动) ✅(引用计数+1)
自动释放 ❌ 需手动 delete
循环引用 ⚠️ 需 weak_ptr 解决
性能开销 极小 引用计数有额外开销

💡 日常开发原则:能用 unique_ptr 就别用 shared_ptr,能用智能指针就别用裸指针

小结

序号 知识点 一句话总结
1 浅拷贝 只拷地址不拷内容,两个对象共用内存→双重释放
2 深拷贝 新分配内存+拷贝内容,各自独立
3 拷贝赋值 operator= 实现深赋值,注意自赋值检查
4 三五法则 自定义析构时,记得也自定义拷贝构造和赋值
5 unique_ptr 独占所有权,不可复制,移动转移
6 shared_ptr 共享所有权,引用计数为0时自动释放

下一篇文章,我们将进入面向对象最强大的特性------继承与多态:如何复用代码、如何用虚函数实现"同一个接口,不同的行为"。


本文是「C++ 从基础到项目实战」系列的第 6 篇。关注我,不错过后续更新。

相关推荐
luck_bor1 小时前
IO流知识点笔记
java·开发语言·笔记
程序大视界1 小时前
【Python系列课程】Pandas(四):数据统计与排序——describe、sort_values、sample
开发语言·python·pandas
KWTXX1 小时前
使用matlab官网的skills调用claude-待完成
开发语言·matlab
Cthy_hy1 小时前
Python算法竞赛:排列组合核心用法
开发语言·python·算法
大圣编程2 小时前
面向对象深度理解
java·开发语言·算法
爱喝水的鱼丶2 小时前
SAP-ABAP:SAP 简单报表输出开发系列(共6篇) 第四篇:SAP 报表异常处理机制:数据校验与消息提示规范落地
开发语言·数据库·学习·算法·sap·abap
影寂ldy2 小时前
C# const 常量 / readonly 只读 / static readonly
java·开发语言·c#
代码中介商2 小时前
C++四大设计模式:单例、工厂、观察者、策略
java·c++·设计模式