手写一个智能指针:从 unique_ptr 到 shared_ptr 的引用计数原理

博主介绍:程序喵大人

C++ 内存管理的噩梦始于 new/delete 的手动配对,止于智能指针的自动化革新。从 RAII(资源获取即初始化)的核心理念,到 unique_ptr 的独占所有权,再到 shared_ptr 的引用计数共享机制,智能指针体系不仅解决了内存泄漏和悬空指针的顽疾,更通过类型系统明确了资源所有权语义。本文将深入剖析智能指针的实现原理,手写核心代码,助你彻底掌握这一现代 C++ 基石技术。

一、RAII:资源管理的哲学基石

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 最重要的设计理念之一。其核心思想简单而强大:资源的生命周期与对象的生命周期绑定。在对象构造时获取资源,在对象析构时自动释放资源,利用 C++ 栈对象的自动析构机制确保资源正确清理。

cpp 复制代码
class FileHandler {
public:
    FileHandler(const std::string& path) {
        fileHandle = fopen(path.c_str(), "r");  // 构造即获取资源
    }
    ~FileHandler() {
        if (fileHandle) fclose(fileHandle);     // 析构必释放资源
    }
private:
    FILE* fileHandle;
};

void processFile() {
    FileHandler file("data.txt");  // 自动打开
    // 使用文件...
}  // 离开作用域,自动关闭,即使发生异常

RAII 的三大核心特性:

自动释放,告别手动 delete / close;

异常安全,即使发生异常也能正确回收资源;

禁止拷贝、支持移动,资源只能有唯一所有者。

这种设计让资源管理变得可预测、可维护,是智能指针诞生的思想基础。

二、unique_ptr:独占所有权的轻量级守护者

unique_ptr 体现了独占所有权的清晰语义。在任何时候,只有一个 unique_ptr 可以指向一个给定的对象。它通过禁止拷贝构造函数和拷贝赋值运算符,只提供移动构造函数和移动赋值运算符来实现所有权的唯一性。

手写 unique_ptr 的核心实现
cpp 复制代码
template<typename T>
class MyUniquePtr {
private:
    T* ptr_;

public:
    MyUniquePtr() : ptr_(nullptr) {}
    explicit MyUniquePtr(T* ptr) : ptr_(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;
    }

    ~MyUniquePtr() {
        delete ptr_;
    }

    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* ptr = nullptr) {
        delete ptr_;
        ptr_ = ptr;
    }

    explicit operator bool() const {
        return ptr_ != nullptr;
    }
};

unique_ptr 的核心设计原则:

零开销抽象,性能几乎等同于裸指针;

异常安全,所有权转移过程不抛异常;

类型安全,在编译期防止错误使用。

这也是为什么在现代 C++ 中,unique_ptr 是默认首选的智能指针。

三、shared_ptr:引用计数的共享所有权模型

shared_ptr 允许多个指针共享同一个对象,通过引用计数机制管理对象生命周期。当最后一个 shared_ptr 被销毁时,对象才会被删除。其核心是控制块(Control Block)的设计。

引用计数控制块的内存布局

控制块是一个堆上的独立内存区域,通常包含以下内容:

  • 强引用计数(use_count)
  • 弱引用计数(weak_count)
  • 指向被管理对象的指针
  • 删除器(deleter)
cpp 复制代码
template<typename T>
class ControlBlock {
public:
    std::atomic<size_t> use_count{1};
    std::atomic<size_t> weak_count{0};
    T* ptr{nullptr};
    std::function<void(T*)> deleter;

    ControlBlock(T* p, const std::function<void(T*)>& del)
        : ptr(p), deleter(del ? del : [](T* p) { delete p; }) {}
};
手写 shared_ptr 的核心实现
cpp 复制代码
template<typename T>
class MySharedPtr {
private:
    T* ptr_;
    ControlBlock<T>* ctrl_block_;

    void increment_ref() {
        if (ctrl_block_) {
            ctrl_block_->use_count.fetch_add(1, std::memory_order_relaxed);
        }
    }

    void decrement_ref() {
        if (ctrl_block_) {
            if (ctrl_block_->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                ctrl_block_->deleter(ctrl_block_->ptr);
                ctrl_block_->ptr = nullptr;

                if (ctrl_block_->weak_count.load(std::memory_order_acquire) == 0) {
                    delete ctrl_block_;
                }
            }
        }
    }

public:
    MySharedPtr() : ptr_(nullptr), ctrl_block_(nullptr) {}

    explicit MySharedPtr(T* ptr)
        : ptr_(ptr), ctrl_block_(new ControlBlock<T>(ptr, nullptr)) {}

    template<typename Deleter>
    MySharedPtr(T* ptr, Deleter del)
        : ptr_(ptr), ctrl_block_(new ControlBlock<T>(ptr, del)) {}

    MySharedPtr(const MySharedPtr& other)
        : ptr_(other.ptr_), ctrl_block_(other.ctrl_block_) {
        increment_ref();
    }

    MySharedPtr& operator=(const MySharedPtr& other) {
        if (this != &other) {
            decrement_ref();
            ptr_ = other.ptr_;
            ctrl_block_ = other.ctrl_block_;
            increment_ref();
        }
        return *this;
    }

    MySharedPtr(MySharedPtr&& other) noexcept
        : ptr_(other.ptr_), ctrl_block_(other.ctrl_block_) {
        other.ptr_ = nullptr;
        other.ctrl_block_ = nullptr;
    }

    ~MySharedPtr() {
        decrement_ref();
    }

    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }

    size_t use_count() const {
        return ctrl_block_ ? ctrl_block_->use_count.load(std::memory_order_relaxed) : 0;
    }

    T* get() const { return ptr_; }

    void reset(T* ptr = nullptr) {
        decrement_ref();
        if (ptr) {
            ptr_ = ptr;
            ctrl_block_ = new ControlBlock<T>(ptr, nullptr);
        } else {
            ptr_ = nullptr;
            ctrl_block_ = nullptr;
        }
    }
};

四、引用计数的线程安全性考量

shared_ptr 的引用计数在多线程环境下必须是线程安全的,标准库通过原子操作来保证这一点。

关键实现细节包括:

  1. 使用 fetch_add / fetch_sub 操作引用计数
  2. 增加引用时使用 memory_order_relaxed
  3. 减少引用并可能释放资源时使用 memory_order_acq_rel
cpp 复制代码
ctrl_block_->use_count.fetch_add(1, std::memory_order_relaxed);

if (ctrl_block_->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
    // 释放资源
}

相比互斥锁,原子操作的性能开销要小得多,是 shared_ptr 设计中的关键优化点。

五、循环引用问题的产生与解决

当多个对象通过 shared_ptr 相互持有对方时,会形成循环引用,导致引用计数永远无法归零,从而引发内存泄漏。

典型的循环引用场景
cpp 复制代码
class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

void circularReference() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
}
使用 weak_ptr 打破循环

weak_ptr 不增加强引用计数,只作为观察者存在。

cpp 复制代码
class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::weak_ptr<A> a_ptr;
};

void fixedCircularReference() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;

    if (auto a_locked = b->a_ptr.lock()) {
        // 对象仍然存在,可以安全访问
    }
}

六、unique_ptr 与 shared_ptr 的核心差异

对比维度 unique_ptr shared_ptr
所有权模型 独占所有权 共享所有权
拷贝语义 禁止拷贝,仅支持移动 支持拷贝
内存开销 单指针大小 指针 + 控制块
运行时开销 几乎为零 引用计数原子操作
线程安全 仅对象本身 引用计数线程安全
适用场景 明确唯一所有者 多对象共享资源

选择原则:

默认使用 unique_ptr;

确实需要共享时再使用 shared_ptr;

出现双向引用时引入 weak_ptr;

优先使用 make_unique / make_shared 以减少内存分配次数。

结语

智能指针是现代 C++ 内存管理的核心工具。RAII 提供了思想基础,unique_ptr 定义了清晰的独占所有权语义,shared_ptr 则通过引用计数实现了安全的共享模型,而 weak_ptr 负责解决循环引用这一经典难题。真正理解并掌握这些智能指针的实现原理,才能在工程实践中写出健壮、高性能、可维护的 C++ 代码。

码字不易,欢迎大家点赞,关注,评论,谢谢!

相关推荐
zhuqiyua5 小时前
第一次课程家庭作业
c++
5 小时前
java关于内部类
java·开发语言
好好沉淀5 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
只是懒得想了5 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
lsx2024065 小时前
FastAPI 交互式 API 文档
开发语言
VCR__5 小时前
python第三次作业
开发语言·python
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
wkd_0075 小时前
【Qt | QTableWidget】QTableWidget 类的详细解析与代码实践
开发语言·qt·qtablewidget·qt5.12.12·qt表格
东东5165 小时前
高校智能排课系统 (ssm+vue)
java·开发语言
余瑜鱼鱼鱼5 小时前
HashTable, HashMap, ConcurrentHashMap 之间的区别
java·开发语言