C++笔记 剖析智能指针内部结构及底层实现

张开发
2026/4/20 1:09:21 15 分钟阅读

分享文章

C++笔记 剖析智能指针内部结构及底层实现
智能指针是现代C内存管理的基石其核心是基于RAII资源获取即初始化机制将裸指针封装为类对象通过析构函数自动完成资源释放从根源上解决内存泄漏、空悬指针、重复释放等痛点。不同于表层用法深入其内部结构与底层实现能更精准地理解所有权管理、引用计数等核心机制规避使用陷阱。本文将逐一对std::unique_ptr、std::shared_ptr、std::weak_ptr的内部结构、核心实现逻辑进行拆解搭配简化源码与内存模型帮你吃透智能指针的底层原理。前言、终极脑图除了unique_ptr的其他智能指针内存里到底有什么我给你画最清晰、最完整、最容易理解的结构脑图【整个智能指针体系结构图】 ┌─────────────────────────────────────────────────────────────┐ │ 栈内存 (stack) │ │ │ │ shared_ptr A weak_ptr B │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ T* ptr │─────┐ │ ControlBlock* ctrl │ │ │ │ ControlBlock* ctrl │─────┼────→└─────────────────┘ │ │ └─────────────────┘ │ │ │ └─────────────────────────┼─────┼─────────────────────────────┘ │ │ │ │ ┌─────────────────────────▼─────▼─────────────────────────────┐ │ 堆内存 (heap) │ │ │ │ 【控制块 ControlBlock】 │ │ ┌─────────────────────────────┐ │ │ │ T* ptr → 对象地址 │ │ │ │ long shared_count → 强计数 │ │ │ │ long weak_count → 弱计数 │ │ │ │ Deleter deleter → 删除函数 │ │ │ └─────────────────────────────┘ │ │ ▲ │ │ │ │ │ 【对象 Object】 │ │ ┌────────────────┐ │ │ │ 你的数据/成员 │ │ │ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘一、智能指针的底层核心基石RAII与封装思想所有智能指针的底层设计都围绕两个核心封装与RAII这是理解其实现的前提。1. 封装思想智能指针本质是一个“指针包装类”内部持有一个裸指针指向堆上资源并重载operator*、operator-等运算符模拟裸指针的行为让使用者可以像使用普通指针一样操作智能指针同时隐藏内存管理的细节。2. RAII机制将资源堆内存、文件句柄等的生命周期与智能指针对象的生命周期绑定——智能指针对象构造时获取资源接管裸指针对象析构时离开作用域、异常抛出等场景自动调用析构函数释放资源无需手动调用delete这是智能指针“智能”的核心所在。底层共性结构简化所有智能指针都包含一个核心成员——裸指针T* ptr_用于指向实际管理的堆内存同时提供析构函数在对象销毁时释放ptr_指向的资源。不同智能指针的差异主要在于是否引入引用计数、如何管理所有权以及是否支持自定义删除器。二、std::unique_ptr独占所有权的轻量级实现2.1 内部结构极简设计零额外开销unique_ptr的核心特性是“独占所有权”——同一时间只能有一个unique_ptr指向同一个堆资源禁止拷贝、支持移动其内部结构极简目的是追求接近裸指针的性能无额外内存开销删除器为无状态时内存大小与裸指针完全一致。内部核心成员简化版基于GCC/libstdc实现逻辑template typename T, typename Deleter std::default_deleteT class unique_ptr { private: T* ptr_; // 核心指向堆资源的裸指针 Deleter deleter_; // 可选删除器默认是std::default_deleteT public: // 构造、析构、移动相关函数... };关键细节裸指针ptr_唯一负责指向堆资源是unique_ptr的核心数据无任何额外辅助指针如引用计数指针这是其轻量的关键。删除器deleter_用于自定义资源释放逻辑默认使用std::default_deleteT其底层实现就是调用delete ptr_当管理数组、文件句柄等特殊资源时可传入自定义删除器实现灵活释放。空基类优化EBO若删除器是无状态类型如默认删除器编译器会通过空基类优化让deleter_不占用额外内存此时unique_ptr的内存大小与裸指针完全一致。2.2 底层实现核心禁止拷贝与移动语义unique_ptr的独占性是通过“删除拷贝构造/拷贝赋值运算符、实现移动构造/移动赋值运算符”实现的核心是“所有权转移”而非“拷贝”具体简化源码如下// 1. 禁止拷贝显式删除拷贝构造和拷贝赋值C11语法 unique_ptr(const unique_ptr) delete; unique_ptr operator(const unique_ptr) delete; // 2. 支持移动移动构造转移所有权 unique_ptr(unique_ptr other) noexcept : ptr_(other.ptr_), deleter_(std::move(other.deleter_)) { other.ptr_ nullptr; // 原指针置空避免重复释放 } // 3. 移动赋值先释放当前资源再转移所有权 unique_ptr operator(unique_ptr other) noexcept { if (this ! other) { // 先释放当前管理的资源 if (ptr_ ! nullptr) { deleter_(ptr_); } // 转移所有权 ptr_ other.ptr_; deleter_ std::move(other.deleter_); other.ptr_ nullptr; // 原指针置空 } return *this; } // 4. 析构函数自动释放资源 ~unique_ptr() noexcept { if (ptr_ ! nullptr) { deleter_(ptr_); // 调用删除器释放资源 } }核心逻辑禁止拷贝通过 delete显式删除拷贝构造和拷贝赋值运算符编译期直接报错杜绝多个unique_ptr共享同一资源的可能。移动语义移动构造/赋值时将源对象的ptr_转移给目标对象同时将源对象的ptr_置空确保所有权唯一避免重复释放。自动释放析构函数中判断ptr_是否非空若非空则调用删除器释放资源完美契合RAII机制。2.3 内存模型示意unique_ptr的内存布局极简无额外冗余示意图如下无状态删除器场景unique_ptr 对象栈上 └── 裸指针 ptr_ ──→ 堆上的目标对象说明栈上的unique_ptr对象仅包含一个裸指针直接指向堆上的资源当unique_ptr离开作用域栈上对象析构自动调用删除器释放堆上资源。三、std::shared_ptr共享所有权的引用计数实现shared_ptr支持多个指针共享同一堆资源的所有权其核心是“引用计数机制”——通过一个独立的控制块Control Block管理引用计数多个shared_ptr共享同一个控制块实现所有权的共享与资源的安全释放。相较于unique_ptr它有一定的内存和性能开销但灵活性更高是实际开发中最常用的智能指针之一。3.1 内部结构双指针设计对象指针控制块指针shared_ptr的内部不直接存储引用计数而是通过“双指针”设计将引用计数、删除器等信息封装在独立的控制块中核心成员如下简化版template typename T class shared_ptr { private: T* ptr_; // 指向堆上目标对象的裸指针 ControlBlock* ctrl_; // 指向控制块的指针核心管理引用计数 public: // 构造、析构、拷贝、移动相关函数... }; // 控制块结构核心独立于shared_ptr对象 struct ControlBlock { std::atomicint shared_count; // 强引用计数shared_ptr的数量 std::atomicint weak_count; // 弱引用计数weak_ptr的数量 T* obj_ptr; // 指向目标对象的指针 Deleter deleter; // 自定义删除器 Allocator allocator; // 内存分配器 };关键细节双指针分工ptr_负责直接访问目标对象ctrl_负责指向控制块管理引用计数和资源释放逻辑多个shared_ptr指向同一目标对象时它们的ctrl_指向同一个控制块实现引用计数的共享。控制块的创建控制块在第一个shared_ptr构造时创建如使用std::make_shared或new T初始化一旦创建直到强引用计数和弱引用计数都为0时才会被释放。原子引用计数shared_count和weak_count使用std::atomic封装确保多线程环境下引用计数的增减是原子操作避免数据竞争保证线程安全但目标对象的访问仍需额外同步。3.2 底层实现核心引用计数的增减与资源释放shared_ptr的核心逻辑围绕“引用计数的增减”展开包括构造、拷贝、析构、重置等操作具体简化源码如下// 1. 构造函数以std::make_shared为例核心是创建控制块 template typename T, typename... Args shared_ptrT make_shared(Args... args) { // 一次性分配“控制块目标对象”的内存高效减少malloc次数 auto* ctrl new ControlBlock(); ctrl-obj_ptr new T(std::forwardArgs(args)...); // 构造目标对象 ctrl-shared_count 1; // 初始强引用计数为1 ctrl-weak_count 0; // 初始弱引用计数为0 return shared_ptrT(ctrl-obj_ptr, ctrl); } // 2. 拷贝构造共享控制块强引用计数1 shared_ptr(const shared_ptr other) noexcept : ptr_(other.ptr_), ctrl_(other.ctrl_) { if (ctrl_) { ctrl_-shared_count.fetch_add(1, std::memory_order_acq_rel); // 原子递增 } } // 3. 析构函数强引用计数-1判断是否释放资源 ~shared_ptr() noexcept { if (ctrl_) { // 原子递减强引用计数若递减后为0释放目标对象 if (ctrl_-shared_count.fetch_sub(1, std::memory_order_acq_rel) 1) { ctrl_-deleter(ctrl_-obj_ptr); // 调用删除器释放目标对象 // 若弱引用计数也为0释放控制块 if (ctrl_-weak_count 0) { delete ctrl_; } } } } // 4. 重置操作释放当前所有权可选指向新资源 void reset(T* new_ptr nullptr) noexcept { // 先释放当前资源与析构逻辑一致 if (ctrl_) { if (ctrl_-shared_count.fetch_sub(1, std::memory_order_acq_rel) 1) { ctrl_-deleter(ctrl_-obj_ptr); if (ctrl_-weak_count 0) { delete ctrl_; } } } // 指向新资源若有创建新的控制块 if (new_ptr) { auto* new_ctrl new ControlBlock(); new_ctrl-obj_ptr new_ptr; new_ctrl-shared_count 1; new_ctrl-weak_count 0; ptr_ new_ptr; ctrl_ new_ctrl; } else { ptr_ nullptr; ctrl_ nullptr; } }核心逻辑构造创建控制块初始化强引用计数为1弱引用计数为0绑定目标对象std::make_shared会一次性分配控制块和目标对象的内存比直接用new更高效减少一次内存分配、更安全异常安全。拷贝多个shared_ptr共享同一个控制块拷贝时仅递增强引用计数不复制目标对象实现所有权共享。析构递减强引用计数若递减后为0说明当前是最后一个持有该资源的shared_ptr调用删除器释放目标对象若此时弱引用计数也为0释放控制块避免控制块内存泄漏。3.3 内存模型示意shared_ptr的内存模型核心是“控制块共享”示意图如下shared_ptr 对象1栈上 shared_ptr 对象2栈上 ├── 裸指针 ptr_ ──→ 堆上目标对象 ├── 裸指针 ptr_ ──→ 堆上目标对象 └── 控制块指针 ctrl_ ──→ 控制块 └── 控制块指针 ctrl_ ──→ 控制块 控制块堆上 ├── 强引用计数shared_count2 ├── 弱引用计数weak_count0 ├── 裸指针 obj_ptr ──→ 堆上目标对象 └── 删除器、分配器说明多个shared_ptr的ctrl_指向同一个控制块共同维护强引用计数当所有shared_ptr都析构强引用计数为0目标对象被释放控制块则在强引用计数和弱引用计数都为0时被释放。3.4 关键陷阱循环引用与底层原因shared_ptr的最大陷阱是“循环引用”本质是两个或多个shared_ptr互相持有对方的shared_ptr导致强引用计数无法降为0目标对象和控制块永远无法释放造成内存泄漏。循环引用示例底层分析class B; class A { public: shared_ptrB b_ptr; // A持有B的shared_ptr ~A() { cout A析构 endl; } }; class B { public: shared_ptrA a_ptr; // B持有A的shared_ptr ~B() { cout B析构 endl; } }; int main() { auto a make_sharedA(); // a的强引用计数1 auto b make_sharedB(); // b的强引用计数1 a-b_ptr b; // b的强引用计数2 b-a_ptr a; // a的强引用计数2 // 离开作用域a和b析构强引用计数各减1均变为1 // 此时a和b的强引用计数仍为1无法释放造成内存泄漏 return 0; }底层原因a和b互相持有对方的shared_ptr形成闭环离开作用域后a和b自身析构强引用计数各减1但仍各有1个强引用被对方持有导致目标对象和控制块无法释放。解决该问题的核心是使用std::weak_ptr打破循环。四、std::weak_ptr弱引用辅助打破循环引用weak_ptr本身不拥有资源的所有权是shared_ptr的“观察者”其核心作用是打破shared_ptr的循环引用同时可以观察资源是否存活。它不影响强引用计数仅与控制块交互底层依赖shared_ptr的控制块实现功能自身内存开销极低。4.1 内部结构仅持有控制块指针weak_ptr的内部结构比shared_ptr更简单仅持有一个指向控制块的指针不直接持有目标对象的指针也不管理强引用计数核心成员如下简化版template typename T class weak_ptr { private: ControlBlock* ctrl_; // 仅指向shared_ptr的控制块不持有目标对象 public: // 构造、析构、lock()等核心函数... };关键细节无目标对象指针weak_ptr无法直接访问目标对象必须通过lock()方法获取shared_ptr后才能访问目标对象避免空悬指针问题。仅与控制块交互weak_ptr的所有操作如判断资源是否存活、获取shared_ptr都通过控制块实现不影响强引用计数仅影响弱引用计数。4.2 底层实现核心弱引用计数与lock()机制weak_ptr的核心逻辑的是“管理弱引用计数”和“安全获取shared_ptr”具体简化源码如下// 1. 从shared_ptr构造弱引用计数1 weak_ptr(const shared_ptrT other) noexcept : ctrl_(other.ctrl_) { if (ctrl_) { ctrl_-weak_count.fetch_add(1, std::memory_order_relaxed); // 原子递增弱引用计数 } } // 2. 析构函数弱引用计数-1判断是否释放控制块 ~weak_ptr() noexcept { if (ctrl_) { // 原子递减弱引用计数若递减后为0且强引用计数为0释放控制块 if (ctrl_-weak_count.fetch_sub(1, std::memory_order_acq_rel) 1) { if (ctrl_-shared_count 0) { delete ctrl_; } } } } // 3. 核心方法lock()——获取shared_ptr安全访问目标对象 shared_ptrT lock() const noexcept { if (ctrl_ ctrl_-shared_count 0) { // 强引用计数0说明资源存活返回shared_ptr强引用计数1 return shared_ptrT(ctrl_-obj_ptr, ctrl_); } // 资源已释放返回空shared_ptr return shared_ptrT(); } // 4. 辅助方法expired()——判断资源是否存活强引用计数是否为0 bool expired() const noexcept { return !ctrl_ || ctrl_-shared_count 0; }核心逻辑弱引用计数管理weak_ptr构造时递增控制块的弱引用计数析构时递减弱引用计数弱引用计数不影响目标对象的生命周期仅影响控制块的释放控制块需强引用计数和弱引用计数都为0才释放。lock()机制判断控制块的强引用计数是否大于0资源是否存活若存活返回一个指向目标对象的shared_ptr强引用计数1确保访问时资源不会被释放若资源已释放返回空shared_ptr避免空悬指针。打破循环引用将循环引用中的一个shared_ptr改为weak_ptr此时该指针不再递增强引用计数仅递增弱引用计数当其他shared_ptr析构后强引用计数可降为0目标对象被释放循环被打破。4.3 内存模型示意weak_ptr与shared_ptr共享控制块内存模型如下shared_ptr 对象栈上 weak_ptr 对象栈上 ├── 裸指针 ptr_ ──→ 堆上目标对象 └── 控制块指针 ctrl_ ──→ 控制块 └── 控制块指针 ctrl_ ──→ 控制块 控制块堆上 ├── 强引用计数shared_count1 ├── 弱引用计数weak_count1 ├── 裸指针 obj_ptr ──→ 堆上目标对象 └── 删除器、分配器说明weak_ptr仅持有控制块指针不直接指向目标对象当shared_ptr析构强引用计数为0目标对象被释放但控制块仍保留弱引用计数为1当weak_ptr也析构弱引用计数为0控制块被释放。五、三种智能指针底层实现对比与核心总结5.1 底层实现对比表智能指针内部核心结构引用计数所有权内存开销核心底层逻辑unique_ptr裸指针 可选删除器无独占极低与裸指针一致禁止拷贝、支持移动析构时调用删除器释放资源shared_ptr双指针对象指针控制块指针强引用计数弱引用计数原子操作共享中等双指针控制块共享控制块通过强引用计数管理资源生命周期弱引用计数管理控制块生命周期weak_ptr仅控制块指针仅影响弱引用计数无观察者极低仅一个控制块指针依托shared_ptr的控制块观察资源存活通过lock()安全获取shared_ptr5.2 核心总结所有智能指针的底层核心都是RAII机制通过封装裸指针将资源生命周期与对象生命周期绑定实现自动释放。unique_ptr追求轻量高效通过禁止拷贝、支持移动实现独占所有权无引用计数开销适合无需共享资源的场景。shared_ptr通过控制块和双指针设计实现共享所有权引用计数的原子操作保证线程安全但存在内存和性能开销核心陷阱是循环引用。weak_ptr是shared_ptr的辅助工具不拥有所有权、不影响强引用计数通过弱引用计数和lock()机制打破循环引用并安全观察资源。底层实现的核心差异本质是“所有权管理方式”的差异——独占vs共享而引用计数是实现共享所有权的关键手段。掌握智能指针的底层实现不仅能更灵活地使用它进行内存管理更能规避使用陷阱如循环引用、裸指针混用理解C RAII、引用计数、原子操作等核心编程思想为后续学习更复杂的内存管理如自定义智能指针打下基础。

更多文章