C++ 智能指针的底层实现原理有哪些?

来源:Reactjs教程作者:花满楼头衔:网络博主
导读:本期聚焦于花满楼创作的《C++ 智能指针的底层实现原理有哪些?》,敬请观看详情。引用计数并不是简单放在对象内部的一个整数,shared_ptr 的安全共享依赖一个独立控制块,控制块里同时保存强引用计数、弱引用计数和删除器。理解这一点,才能解释为什么从一个裸指针构造多个 shared_ptr 会重复释放。unique_ptr 则走另一条路,通过禁止拷贝和空基类优化做到几乎零开销;weak_ptr 通过观察控制块而不是直接访问对象,既能探测对象是否存活,又能打断循环引用。本文从引用计数、控制块布局、拷贝移动语义、weak_ptr 的 lock 机制和常见误用场景入手,拆解这三类智能指针的实现思路,并给出简化的类模板代码,帮助读者看清析构、赋值和线程安全背后的真实细节。

智能指针要解决的核心问题只有一个:对象的所有权到底归谁,以及由谁在什么时机释放资源。裸指针虽然灵活,但一旦涉及异常、提前返回或多分支逻辑,delete 就很容易被漏掉;更隐蔽的问题是同一块内存被多个指针指向时,重复释放或悬空访问几乎不可避免。C++ 标准库提供的 unique_ptr、shared_ptr 和 weak_ptr 并不是魔法,它们都是围绕 RAII 和引用计数这两个基础机制,在编译期和运行期分别做文章。

C++ 智能指针的底层实现原理有哪些?

一、从裸指针到 RAII:智能指针的基础骨架

RAII(Resource Acquisition Is Initialization)是智能指针的起点。它的思路是在构造函数中获得资源,在析构函数中释放资源;当栈对象离开作用域时,析构函数会被自动调用。智能指针本质上就是一个栈上的类模板,内部保存一个裸指针,析构时根据所有权策略决定是否 delete。最简化的智能指针通常长这样:

template <typename T>
class SimplePtr {
public:
    explicit SimplePtr(T* p = nullptr) : ptr_(p) {}

    ~SimplePtr() {
        delete ptr_;
    }

    // 禁止拷贝,避免重复释放
    SimplePtr(const SimplePtr&) = delete;
    SimplePtr& operator=(const SimplePtr&) = delete;

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

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

private:
    T* ptr_;
};

这个简化的 SimplePtr 已经具备独占智能指针的雏形:它接管了裸指针的所有权,析构时负责释放。问题在于它完全禁止拷贝,如果函数返回或容器存储需要转移所有权,就不够用。真正的智能指针在拷贝和移动语义上做出了不同选择:unique_ptr 保持独占,只允许移动;shared_ptr 允许多个指针共享同一对象,通过引用计数决定最后一次释放的时机。因此理解智能指针底层,主要就是理解这些拷贝控制操作背后发生了什么。

在讨论 shared_ptr 的具体实现前,还有一点值得强调:智能指针本身的大小和裸指针不同。unique_ptr 如果使用默认删除器,大小通常就是一个指针;shared_ptr 则通常包含两个指针,一个指向对象,一个指向控制块。这个差异不是标准强制规定的,但主流标准库实现都采用了类似布局,后面的控制块分析会进一步说明原因。

二、shared_ptr 的控制块:引用计数如何工作

shared_ptr 的引用计数并不是直接放在被管理对象内部。C++ 标准没有要求对象本身携带计数,标准库实现通常使用一个独立的控制块(control block)。控制块中至少包含强引用计数、弱引用计数、删除器和分配器。强引用计数记录当前有多少个 shared_ptr 实例指向同一个对象;弱引用计数则记录多少个 weak_ptr 观察这个控制块。为什么需要两个计数?因为如果只有强计数,当强计数归零时对象可以销毁,但控制块本身不能销毁,仍可能有 weak_ptr 需要查询对象是否存活。让弱引用计数也归零时,控制块才会真正释放。

控制块创建的时机非常关键。使用 make_shared<T>() 时,标准库可以一次分配同时容纳对象和控制块,内存布局更紧凑,也减少一次堆分配。而如果先 new 出裸指针再交给 shared_ptr 构造,比如 shared_ptr<T> sp(new T()),控制块必须单独分配。这里有一个很常见的误用:用同一个裸指针构造两个 shared_ptr。两个 shared_ptr 会各自创建独立控制块,都认为对象的强引用计数是 1,第一个析构时释放对象,第二个析构时再次释放同一块内存,导致未定义行为。正确做法是让参数类型明确表达所有权转移,比如 shared_ptr(T* p) 的构造只发生一次。

#include <atomic>
#include <cstddef>

template <typename T>
class SharedPtr;

struct ControlBlockBase {
    std::atomic<long> strong_count{1};
    std::atomic<long> weak_count{1};

    virtual ~ControlBlockBase() = default;

    void add_strong() {
        strong_count.fetch_add(1, std::memory_order_relaxed);
    }

    void release_strong() {
        if (strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            destroy_object();
            release_weak();
        }
    }

    void add_weak() {
        weak_count.fetch_add(1, std::memory_order_relaxed);
    }

    void release_weak() {
        if (weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            delete this;
        }
    }

    virtual void destroy_object() = 0;
};

template <typename T>
struct ControlBlock final : ControlBlockBase {
    T* object;

    explicit ControlBlock(T* obj) : object(obj) {}

    void destroy_object() override {
        delete object;
    }
};

template <typename T>
class SharedPtr {
public:
    SharedPtr() = default;

    explicit SharedPtr(T* p)
        : object_(p),
          control_(p ? new ControlBlock<T>(p) : nullptr) {}

    SharedPtr(const SharedPtr& other)
        : object_(other.object_),
          control_(other.control_) {
        if (control_) {
            control_->add_strong();
        }
    }

    SharedPtr(SharedPtr&& other) noexcept
        : object_(other.object_),
          control_(other.control_) {
        other.object_ = nullptr;
        other.control_ = nullptr;
    }

    ~SharedPtr() {
        if (control_) {
            control_->release_strong();
        }
    }

private:
    T* object_ = nullptr;
    ControlBlockBase* control_ = nullptr;
};

上面的简化实现展示了控制块的两个计数器如何配合。release_strong 在强计数减到 0 时先销毁对象,再释放一次弱计数。注意 weak_count 初始值为 1 并不是表示有一个 weak_ptr,而是表示控制块自身的生命引用。每次创建 weak_ptr 时增加弱计数,weak_ptr 析构时减少弱计数;当强计数归零后释放的那个虚拟弱引用也参与计数,因此控制块只有在所有 weak_ptr 都销毁后才 delete this。这种设计让 weak_ptr::lock 可以在不访问对象的情况下判断对象是否仍然存活。

线程安全方面,引用计数的增减通常使用原子操作,所以多个线程同时拷贝、析构指向同一对象的 shared_ptr 不会破坏计数。但 shared_ptr 本身并不是完全线程安全的:同一个 shared_ptr 实例被多个线程同时修改时,仍然需要外部同步。更关键的是,引用计数保护的是控制块的计数,不保护被管理对象。如果多个线程通过不同 shared_ptr 同时访问同一个普通对象,对象内部的线程安全问题仍然要由开发者自己处理。这个区别在面试和实际故障排查中经常被忽略。

三、unique_ptr 的独占语义和零开销

unique_ptr 选择了完全不同的实现路线。它在语义上独占对象所有权,因此直接禁止拷贝构造和拷贝赋值。移动构造会把源指针置空,避免两个 unique_ptr 指向同一个对象。由于不需要引用计数字段,unique_ptr 使用默认删除器时大小通常与裸指针相同,这让它在参数传递、容器存储等场景中几乎没有额外开销。

#include <cstddef>

template <typename T, typename Deleter = std::default_delete<T>>
class UniquePtr {
public:
    UniquePtr() = default;

    explicit UniquePtr(T* p) : ptr_(p) {}

    UniquePtr(UniquePtr&& other) noexcept
        : ptr_(other.release()),
          deleter_(std::move(other.deleter_)) {}

    UniquePtr& operator=(UniquePtr&& other) noexcept {
        if (this != &other) {
            reset(other.release());
            deleter_ = std::move(other.deleter_);
        }
        return *this;
    }

    ~UniquePtr() {
        reset();
    }

    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;

    T* get() const {
        return ptr_;
    }

    T* release() {
        T* tmp = ptr_;
        ptr_ = nullptr;
        return tmp;
    }

    void reset(T* p = nullptr) {
        T* old = ptr_;
        ptr_ = p;
        if (old) {
            deleter_(old);
        }
    }

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

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

private:
    T* ptr_ = nullptr;
    Deleter deleter_{};
};

删除器的处理也很有意思。unique_ptr<T, Deleter> 在类型中携带删除器,如果删除器是无捕获的 lambda 或空函数对象,编译器可以通过空基类优化把它压缩到零字节;而 shared_ptr 把删除器存放在控制块里,删除器类型不会影响 shared_ptr 的大小。这个差异导致 unique_ptr 使用自定义删除器时,类型就变了,不同删除器的 unique_ptr 无法放进同一个容器;shared_ptr 则没有这种限制,因为删除器被运行时多态地保存在控制块中。

reset 和 release 是 unique_ptr 两个容易混淆的接口。release 只是放弃所有权并返回裸指针,不会删除对象;reset 会先删除旧对象再接管新指针。移动赋值通常先释放当前对象,再接管其他对象的所有权。正因为删除器是类型的一部分,实现移动赋值时需要同时移动删除器,否则源对象的删除器状态可能丢失。如果删除器不可移动或包含引用,编译期就会给出错误,这也体现了 C++ 类型系统对所有权语义的约束。

四、weak_ptr 如何观察对象并破解循环引用

weak_ptr 不是独立管理资源的智能指针,它必须从 shared_ptr 构造,用来观察对象但不影响对象的生命周期。weak_ptr 内部保存一个指向控制块的指针,并增加弱引用计数。它没有 operator* 和 operator->,因为对象可能已经销毁,直接解引用会悬空。要访问对象,必须先调用 lock(),lock 会检查控制块中的强引用计数是否大于 0,如果大于 0 就返回一个指向该对象的 shared_ptr,否则返回空 shared_ptr。

template <typename T>
class WeakPtr {
public:
    WeakPtr() = default;

    WeakPtr(const SharedPtr<T>& sp)
        : object_(sp.object_),
          control_(sp.control_) {
        if (control_) {
            control_->add_weak();
        }
    }

    ~WeakPtr() {
        if (control_) {
            control_->release_weak();
        }
    }

    SharedPtr<T> lock() const {
        if (!control_) {
            return SharedPtr<T>();
        }
        long expected = control_->strong_count.load();
        while (expected > 0) {
            if (control_->strong_count.compare_exchange_weak(
                    expected, expected + 1)) {
                SharedPtr<T> sp;
                sp.object_ = object_;
                sp.control_ = control_;
                return sp;
            }
        }
        return SharedPtr<T>();
    }

private:
    T* object_ = nullptr;
    ControlBlockBase* control_ = nullptr;
};

lock() 的实现必须处理竞争条件:在检查 strong_count 大于 0 之后、增加计数之前,对象可能被另一个线程销毁。标准库使用原子比较交换(compare_exchange)来保证只在计数不为 0 时递增,从而安全地获得一个强引用。上面的简化代码用 compare_exchange_weak 演示这个思路,但没有处理内存序的细节。实际标准库实现会使用 acquire/release 语义保证对象创建和销毁之间的可见性。这个细节也是 weak_ptr 比很多人想象中更复杂的原因。为了展示核心逻辑,上面的 WeakPtr 省略了与 SharedPtr 的友元声明,实际工程实现还需要严格的异常安全保证。

weak_ptr 最常见的用途是打破循环引用。如果两个对象互相持有 shared_ptr,它们的强引用计数永远无法归零。例如一个双向链表节点既持有 next 又持有 prev,如果都用 shared_ptr,前驱节点和后继节点会互相拉住,整个链表无法释放。把其中一侧改成 weak_ptr 后,弱引用不增加强计数,节点可以在没有外部 shared_ptr 持有它时被销毁。另一个典型场景是缓存:通过 weak_ptr 保存观察,使用前 lock 可以判断缓存对象是否还存活,避免悬空访问。

总结一下,这三类智能指针的底层分工非常清晰:unique_ptr 用编译期约束实现零开销独占;shared_ptr 用控制块和原子引用计数实现共享所有权;weak_ptr 用弱引用计数和 lock 机制实现安全观察。理解这些机制后,所谓循环引用、双重释放、线程安全等常见问题就不再是黑盒。写代码时选择合适的智能指针,本质上是明确所有权和生命周期,而不是仅为了少写 delete。

智能指针引用计数控制块修改时间:2026-09-24 20:57:01

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0924/61454.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。