31、C++项目实战:智能指针实现

智能指针,说白了就是C++里帮你自动管理内存的“小管家”。

我刚开始写C++那会儿,最头疼的就是手动new和delete。项目一复杂,漏掉delete是常事,内存泄漏查起来简直要命。后来C++11引入了智能指针,我才算真正解脱。今天我们就自己动手,从零实现一个简单的智能指针。

为什么需要智能指针?

你想想看,传统指针有什么问题?

  • 忘记释放:new了不delete,内存泄漏
  • 重复释放:多个指针指向同一块内存,一个delete了,另一个再delete就崩了
  • 异常安全:函数中途抛出异常,delete语句没执行到

智能指针通过RAII(资源获取即初始化)机制,把内存管理绑定到对象的生命周期上。对象析构时自动释放内存,完美解决上述问题。

核心思想:智能指针是一个栈上的对象,它包装了一个原始指针。当智能指针对象离开作用域时,它的析构函数会自动delete所管理的指针。

智能指针的种类

标准库提供了三种智能指针,我们这次重点实现最常用的两种:

类型 所有权 特点
std::unique_ptr 独占所有权 不能拷贝,只能移动
std::shared_ptr 共享所有权 引用计数,最后一个释放时删除
std::weak_ptr 弱引用 配合shared_ptr,解决循环引用

我个人习惯是:能用unique_ptr就不用shared_ptr。为什么?因为引用计数有性能开销,而且容易搞出循环引用。我在项目中遇到过好几次,就是因为滥用shared_ptr导致内存泄漏,排查了两天才找到原因。

实现一个简易的unique_ptr

我们先从最简单的开始。unique_ptr的核心就是:禁止拷贝,允许移动

template <typename T>
class MyUniquePtr {
private:
    T* ptr_;

public:
    // 构造函数
    explicit MyUniquePtr(T* p = nullptr) : ptr_(p) {}

    // 析构函数
    ~MyUniquePtr() {
        delete ptr_;
    }

    // 禁止拷贝
    MyUniquePtr(const MyUniquePtr&) = delete;
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

    // 允许移动
    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }

    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr_;
            ptr_ = other.ptr_;
            other.ptr_ = nullptr;
        }
        return *this;
    }

    // 运算符重载
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }

    // 释放所有权
    T* release() {
        T* temp = ptr_;
        ptr_ = nullptr;
        return temp;
    }

    // 重置
    void reset(T* p = nullptr) {
        delete ptr_;
        ptr_ = p;
    }
};

小提示:移动语义是C++11引入的。移动构造函数把原指针置空,这样原对象析构时就不会重复释放了。这个技巧在实现智能指针时非常关键。

实现shared_ptr:引用计数的艺术

shared_ptr要复杂一些,因为它需要多个智能指针共享同一块内存。核心机制是引用计数

嗯,这里要注意:引用计数本身必须是线程安全的。我们先用一个简单的非线程安全版本,让你理解原理。

template <typename T>
class MySharedPtr {
private:
    T* ptr_;
    int* ref_count_;  // 引用计数

public:
    // 构造函数
    explicit MySharedPtr(T* p = nullptr) : ptr_(p), ref_count_(new int(1)) {}

    // 析构函数
    ~MySharedPtr() {
        release();
    }

    // 拷贝构造
    MySharedPtr(const MySharedPtr& other) 
        : ptr_(other.ptr_), ref_count_(other.ref_count_) {
        if (ref_count_) {
            (*ref_count_)++;
        }
    }

    // 拷贝赋值
    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            release();  // 释放当前资源
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            if (ref_count_) {
                (*ref_count_)++;
            }
        }
        return *this;
    }

    // 运算符重载
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }
    int use_count() const { return ref_count_ ? *ref_count_ : 0; }

private:
    void release() {
        if (ref_count_) {
            (*ref_count_)--;
            if (*ref_count_ == 0) {
                delete ptr_;
                delete ref_count_;
            }
        }
    }
};

避坑指南:我曾经在项目里犯过一个低级错误——把引用计数定义成静态成员变量。结果所有shared_ptr实例共享同一个计数,完全乱套了。记住:每个被管理的对象对应一个独立的引用计数。

核心逻辑流程图

下面这张图展示了shared_ptr的引用计数工作原理:

shared_ptr 引用计数工作原理 shared_ptr<T> sp1 ptr_ → 堆对象 shared_ptr<T> sp2 ptr_ → 堆对象 shared_ptr<T> sp3 ptr_ → 堆对象 T 对象 (堆上) 实际数据 引用计数块 ref_count = 3 shared_ptr 对象 管理的堆对象 引用计数(独立分配) 指针关联

从图中可以看到:三个shared_ptr共享同一个堆对象,引用计数为3。当某个shared_ptr析构时,引用计数减1。只有当引用计数变为0时,才会真正delete堆对象和引用计数块。

实战中的注意事项

实现智能指针时,有几个坑我踩过,分享给你:

  1. 不要用同一个原始指针构造多个shared_ptr:这样会导致多个独立的引用计数,析构时重复释放。应该用make_shared。
  2. 循环引用问题:两个对象互相持有shared_ptr,会导致内存泄漏。解决方案是用weak_ptr打破循环。
  3. 数组支持:标准库的unique_ptr支持数组特化(unique_ptr<T[]>),析构时调用delete[]。我们上面的实现没有处理,你可以自己加上。
  4. 自定义删除器:有时候你需要自定义释放逻辑(比如文件句柄的关闭),智能指针应该支持传入删除器。

个人经验:我建议你在实际项目中使用标准库的智能指针,而不是自己造的轮子。自己实现主要是为了理解原理。标准库的智能指针经过了大量测试和优化,性能更好,也更安全。

总结

智能指针是C++现代编程的基石。通过自己实现,你就能深刻理解RAII、引用计数、移动语义这些核心概念。

说白了,智能指针就是把内存管理这件麻烦事,交给编译器去操心。你只需要关注业务逻辑,剩下的交给智能指针就好。

嗯,代码量不大,但背后的设计思想很值得玩味。如果你能把这个实现吃透,C++的内存管理就算入门了。


公众号:蓝海资料掘金营,微信 deep3321