C++智能指针进阶详解|enable_shared_from_this、weak_ptr、循环引用、容器存储、make_shared/make_unique底层

📅 2026/8/2 4:03:43
C++智能指针进阶详解|enable_shared_from_this、weak_ptr、循环引用、容器存储、make_shared/make_unique底层
本篇博客基于完整可运行源码深度讲解C智能指针进阶核心知识点。涵盖自定义实体类、enable_shared_from_this使用陷阱、weak_ptr弱指针原理、循环引用内存泄漏解决方案、unique_ptr/shared_ptr容器存储规则、make系列函数底层差异。一、全局头文件与前置说明#define _CRT_SECURE_NO_WARNINGS #includeiostream using namespace std; #includecstdlib #includeassert.h #includeatomic #includememory #includevector//底层是连续的内存空间list则是链表各个节点之间的内存互不联系 #includelist // map set //------------------------------------------------------- //开头补充系统重载的智能指针都是十分完善的我们能想到的什么赋值重载这些系统都帮我们做好了设计思路就和我们自己设计的一样可以直接使用知识点解析纯原生注释提炼vector底层为连续内存空间list是链表结构各节点内存相互独立、不连续。map、set为树形关联容器。C标准库智能指针的各类重载、生命周期逻辑、赋值移动规则都已完善设计思路和手写智能指针完全一致可直接放心使用。二、自定义Int测试实体类生命周期观测类class Int { private: int value; public: Int(int x 0) :value(x) { cout create Int Object this value endl; } ~Int() { cout delete Int Object this value endl; value -1; } Int(const Int it) :value(it.value) { cout it copy Int Object this value endl; } Int(Int it) :value(it.value) { it.value -1; cout move Int Object this value endl; } Int operator(Int it) { if (this ! it) { this-value it.value; it.value -1; } } Int operator(const Int a) { if (this ! a) this-value a.value; cout this operator (const Int it) a endl; return *this; } void Print()const { cout value endl; } ostream operator (ostream os) const { os value; return os; } istream operator (istream in) { in this-value; return in; } void Set(int x) { value x; } int GetValue()const { return value; } int Value() { return value; } const int Value()const { return value; } Int operator() { this-value 1; return *this;// } Int operator(int) { Int tmp(*this); *this; return tmp; } operator int() const { return this-value; } Int* operator () { return this; } const Int* operator () const { return this; } };知识点解析纯原生注释提炼自定义Int类用于完整观测对象创建、拷贝、移动、赋值、销毁全过程适配智能指针所有测试场景。移动语义中资源转移后将原对象value置为-1标记资源转移完成避免资源复用与重复释放问题。重载流输入输出、自增、隐式类型转换、取地址运算符完全适配智能指针底层调用逻辑。三、enable_shared_from_this 核心用法与避坑原理//------------------------------------------------------- class Object : public::enable_shared_from_thisObject //继承这个类就可以使用shared_from_this()函数返回一个shared_ptr { private: int count; public: Object(int x 0):count(x) { cout Object() endl; } ~Object() { cout ~Object() endl; } void Print()const { cout count endl; } std::shared_ptrObject GetObject() { //return std::shared_ptrObject(this); //这样子写是通过值拷贝一个右值智能指针返回它但是值得注意的是智能指针的值拷贝有一个特点就是 //它会new一个_Users1和_weaks1的新的计数器不是原来那个堆内存资源原有的计数器,就有两个计数器进而程序出现错误 //但是如果你硬要通过调用该函数返回的this指针值来实现你所想的那样的话 //就可以在此类加上: public::enable_shared_from_this你的类名这句话然后return shared_from_this(); return shared_from_this();//返回一个shared_ptr } };知识点解析纯原生注释提炼面试高频类继承enable_shared_from_this类名即可调用shared_from_this()安全返回指向自身的shared_ptr。致命错误写法直接通过裸指针this构造return shared_ptrObject(this)。错误原因该写法会新建一套独立的_Users、_Weaks计数器和原有资源计数器不互通同一块堆内存存在两套计数器最终导致程序出错、重复释放。正确方案继承enable_shared_from_this使用shared_from_this()复用原有资源的计数器不会新建计数器保证资源唯一绑定。四、enable_shared_from_this 可运行测试用例#if 1 int main() { std::shared_ptrObject p(new Object(100)); p-Print(); std::shared_ptrObject p1 p-GetObject(); } #endif测试解析纯原生逻辑提炼初始智能指针p管理堆对象调用GetObject通过shared_from_this获取新shared_ptr。全程复用同一组引用计数器仅递增计数无内存错乱、无重复释放问题。五、weak_ptr弱指针基础特性测试#if 0 int main() { std::weak_ptrInt pw; { std::shared_ptrInt pa(new Int(100)); pw pa; } //此时弱引用仍为1引用技术块的内存不会释放。 return 0; } #endifweak_ptr赋值给shared_ptr后不会增加强引用计数。局部shared_ptr析构后对象资源被释放但弱引用计数依旧为1。弱引用计数不为0时引用计数控制块内存不会被释放会保留资源状态信息。六、shared_ptr循环引用问题与weak_ptr解决方案#if 0 //循环引用问题 struct Child; struct Parent { //std::shared_ptrChild child; std::weak_ptrChild child; Parent() { std::cout Parent() std::endl; } ~Parent() { std::cout ~Parent() std::endl; } void hi() { std::cout hi parent std::endl; } }; struct Child { //std::shared_ptrParent parent; std::weak_ptrParent parent; Child() { std::cout Child() std::endl; } ~Child() { std::cout ~Child() std::endl; } }; int main() { std::shared_ptrChild pc(new Child()); std::shared_ptrParent pp(new Parent()); pc-parent pp;//这里的赋值已经被重载了pc-parent指向的是pp相同的资源也就是说智能指针里面存储的类的智能指针指向了pp相同的资源。 pp-child pc;//但需要注意的是尽管智能指针管理着裸指针有嵌套的关系但是这并不是多级指针的关系智能指针和该裸指针的使用层级相同。 return 0; } //结果 //Child() //Parent() //为什么没有析构掉呢或者说循环引用是什么 //首先我们画张图出来 //由图我们知道在程序结束之间我们先要将这个pp智能指针进行回收再对pc指针进行回收注意在执行回收资源的时候不是像回收普通指针那样直接回收指针其指向的资源 //而是只访问计数器由计数器的数字来决定资源能不能正常回收而不是我们想回收就回收的 //所以此时Parent和Child的引用计数都是2当第一个Parent执行析构步骤的时候因为其child引用计数为2减一之后仍为1系统认为这块内存还有人使用 //那么此时这个Parent类中的child智能指针就变为空引用计数器也为空了。但是这个child类资源还是在堆内存中没有释放掉。 //同理第二个Child也是如此最终就导致了内存泄漏并且由于在图中这两个类形成一个环所以这也被叫做循环引用问题。 //我们发现如果这个引用计数不加一就不会发生这种情况于是我们想到了调用weak_ptrweak_ptr不会增加引用计数所以就不会出现这种情况。 //Child() //Parent() //~Parent() //~Child() //那么析构的顺序是什么样的呢? //因为pc和pp在栈区按照栈的先进后出原则所以先析构pp再析构pc,在这里是弱指针weak_ptr 是 shared_ptr 的观察者可以监视资源但不会影响其生命周期所以析构智能指针的时候引用计数块减去一 //为0那么这块内存就会被释放掉。先打印出pp的析构再打印出pc的析构。 //弱引用智能指针weak_ptr实际上是用来监视shared_ptr的生命周期是shared_ptr的一个助手weak_ptr没有重载操作符*和-因为它不与shared_ptr共享指针不能操作资源它的构造不会增 //加引用计数析构也不会减少引用计数它的存在就是监视shred_ptr中管理的资源是否存在其他用途就是用来解决循环引用问题。 //另外它只能和shared_ptr进行搭配使用。 #endif图1图2知识点解析纯原生注释提炼核心难点循环引用成因两个类互相持有对方的shared_ptr强引用双向嵌套赋值后双方引用计数维持为2。内存泄漏原理智能指针回收资源不直接销毁内存依靠引用计数判断。栈智能指针析构仅让计数减1剩余计数为1系统判定资源仍被占用堆内存无法释放造成永久内存泄漏。解决核心weak_ptr构造、赋值、析构不增减引用计数可打破双向强引用闭环。weak_ptr核心特性作为shared_ptr的观察者无*、-重载无法操作资源仅监视资源生命周期唯一核心用途是解决循环引用内存泄漏且只能搭配shared_ptr使用。析构顺序规则栈变量先进后出使用weak_ptr破除循环引用后计数可正常归零资源依次正常析构。补充智能指针嵌套指向资源不属于多级指针关系和裸指针使用层级一致。七、weak_ptr核心成员函数use_count/expired/lock详解#if 0 int main() { std::weak_ptrInt wp; { std:;shared_ptrInt pa(new Int(100)); pa-Print(); wp pa; coutwp.use_count()wp.use_count()endl; coutwp.expired()endl;//expired()函数用来判断weak_ptr所监视的shared_ptr是否已经不存在了0为存在1为不存在。 cout \n-------------------------------------------------{}end---------------------------------------------\n endl; } cout wp.use_count() wp.use_count() endl; cout wp.expired() endl; cout \n-------------------------------------------------{}end---------------------------------------------\n endl; std::shared_ptrInt pb wp.lock();//lock函数将weak_ptr转换成shared_ptr如果weak_ptr所监视的shared_ptr已经不存在了那么lock函数会返回一个空的shared_ptr //如果是存在的则会创建了一个新的 shared_ptr增加引用计数,指向那个弱指针所监视的资源也就是说被监视的shared_ptr与load生成的shared_ptr是相同的资源并且引用计数加一。 if (pb) { pb-Print(); } return 0; } #endif //create Int Object01661C00 100 //100 //wp.use_count() 1 //0 // //------------------------------------------------ - {}end-------------------------------------------- - // //delete Int Object01661C00 100 //wp.use_count() 0 //1 // //------------------------------------------------ - {}end-------------------------------------------- -知识点解析纯原生注释提炼expired()判断监视的shared_ptr资源是否失效0代表资源存在1代表资源已释放。lock()weak_ptr转shared_ptr核心函数。资源存在则生成新shared_ptr、引用计数1资源失效则返回空智能指针规避野指针崩溃。use_count()获取当前监视资源的强引用计数weak_ptr自身不参与计数统计。八、unique_ptr list容器存储基础#if 0 int main() { std::liststd::unique_ptrInt ilist; std::liststd::shared_ptrInt slist; std::unique_ptrInt pa(new Int(10)); ilist.push_back(std::move(pa)); } #endif知识点解析纯原生逻辑提炼unique_ptr无拷贝能力仅支持移动语义存入容器必须通过std::move转移资源所有权。九、unique_ptr vector容器存储 make_unique 数组避坑#if 0 int main() { std::vectorstd::unique_ptrInt pavec; int n 10; pavec.reserve(n);//先预留10个空间 #if 0 for (int i 0; i n; i) { pavec.push_back(std::unique_ptrInt(new Int(i)));//注意这里是右值右值在这里调用的是unique的移动语义构造资源的转移因为赋值以及拷贝构造函数已经被删除了。 } #endif for (int i 0; i n; i) { std::unique_ptrInt p(new Int(i)); pavec.push_back(std::move(p)); //这里是具名对象是左值但是在unique_ptr的函数中左值并没有拷贝构造函数和移动构造函数移动构造和移动赋值本身就是为右值所准备的所以就无法进行操作会报错。 //如果不使用 std::movep 会尝试通过拷贝成为 vector 的一员也就是说p要先拷贝一个自己的副本这个副本是存在于这个pavev数组的但这这个智能指针并没有拷贝构造函数 //所以这种左值的操作对于 unique_ptr 是不允许的。 //使用 std::move 后p 本身它的所有权成为 vector 的一员而不是创建一个副本也就是将资源转移到这个pavec这个数组底下。 //感悟 //我们创建一个左值和一个数组或者一些容器的时候这个左值的地址往往是不在容器所管理的地址的范围内的那么如果我们想要这个左值存储在这个容器里头的时候有两种方式 //要么拷贝创建一个处于容器管理地址范围内的副本成为这个容器中能够通过下标访问的一员。或者将这个左值转换成右值并通过资源的转移将资源转移到这个容器底下。 } std::unique_ptrInt vec(new Int(10)); std::unique_ptrInt vec2 std::make_uniqueInt(10);//make_unique是生成这个智能指针的函数这里生成的智能指针由vec2接收 //也就是说std::make_uniqueInt(10)与new Int(10)是等价的。 std::unique_ptrInt[] vec3 (new Int[10]);//unique_ptr的数组版本这里生成的智能指针由vec3接收. //std::unique_ptrInt vec3(new Int[10]); error,因为这里显示的vec本身管理的是一组指针但是类型写成了一个而不是数组导致它实际上之管理头一个元素 // 析构的时候只会析构头一个元素剩下的全部内存泄露 std::unique_ptrInt[] vec4 std::make_uniqueInt[](10);//make_unique的数组版本这里生成的智能指针由vec4接收.; return 0; } #endif知识点解析纯原生注释提炼避坑重点右值unique_ptr入容器直接调用移动构造完成资源转移因为unique_ptr拷贝构造、拷贝赋值已被删除。左值具名unique_ptr禁止直接入容器左值会触发拷贝逻辑unique_ptr无拷贝能力直接编译报错。解决方案通过std::move将左值转为右值转移资源所有权不生成副本适配vector存储规则。容器存储底层感悟外部左值内存不在容器管理范围存入容器只有两种方式拷贝容器内副本、move转移资源所有权。make_unique作用等价于new创建对象专门用于生成unique_ptr智能指针。数组致命坑点普通unique_ptr无法管理数组资源仅会析构数组首元素造成大面积内存泄漏管理数组必须使用unique_ptr类型[]。make_unique支持普通对象和数组对象的创建写法简洁安全。十、shared_ptr容器存储 make_shared底层原理差异#if 0 int main() { std::vectorstd::shared_ptrInt pavec; //在容器中尽量插入指针不要是裸指针要插入智能指针因为加入这个裸指针是堆区的指针你还要手写一个函数去对原本在这个数组里面现在却不在了的指针进行销毁回收堆内存十分麻烦。 int n 10; pavec.reserve(n); for (int i 0; i n; i) { #if 0 pavec.push_back(std::make_sharedInt(i));//make_shared是生成这个智能指针的函数这里生成的智能指针由pavec接收 pavec.push_back(std::shared_ptrInt(new Int(i)));//二者同unique智能指针的使用方式是一样的他们实际的效果一样但是底层实现却有所不同 //不同之处体现在哪里 //首先make_shared是一个生成共享智能指针的函数它分配的内存中引用计数器和指针都是在同一个内存块的也就是说这个计数器和指针都是连续的紧挨的。 //而我们shared_ptr设计的时候共享智能指针的计数器以及指针是处于堆内存中的两个毫无关联的位置上的二者在物理上不会相邻换一种角度来说就是它要分配两次堆内存 //然而make_shared只分配一次内存在这内存中就已经包含了引用计数器以及指针。例如[控制块 | 对象] //当然这样设计也是有优缺点的。 //首先make_shared内存中的设计就体现了相比于传统的定义共享指针的方式它的效率更高将两次操作直接合并为一次操作 //并且你去回收这块内存的时候只需回收一次。 //但是它的缺点就是 //1.如果这个对象很大那么这个一整体的内存占用也会很大另外循环引用的问题也会更加突出。循环引用会导致当你的共享计数为0的时候让你的弱引用不为0导致共享指针 //指向的资源是可以释放的但是你的计数器因为弱引用计数不为0而无法进行释放造成内存泄露无法回收给系统继续使用久而久之你的系统资源就会耗尽。 #endif std::shared_ptrInt pa std::make_sharedInt(i); pavec.push_back(pa); } } #endif知识点解析纯原生注释提炼面试必问容器存储规范禁止存储裸指针裸指针需要手动编写销毁逻辑回收内存极易造成内存泄漏优先存储智能指针自动管理生命周期。原生newshared_ptr底层分配两次堆内存对象内存、引用计数控制块内存相互独立、不连续。make_shared底层仅分配一次连续堆内存内存布局为【控制块 对象】分配、回收效率更高。make_shared优点合并两次内存操作为一次运行效率高内存回收简单。make_shared缺点大对象场景内存占用集中循环引用场景泄漏问题更严重弱引用残留会导致控制块无法释放长期运行耗尽系统资源。十一、全文原生注释核心面试总结智能指针系统重载逻辑完善设计思路与手写智能指针完全一致可直接使用。enable_shared_from_this 解决类内返回自身shared_ptr的双计数器崩溃问题禁止裸指针二次构造。weak_ptr无引用计数增减、不管控资源生命周期唯一核心作用是解决shared_ptr循环引用内存泄漏。unique_ptr独占资源、禁止拷贝左值入容器必须std::move转移所有权数组资源必须匹配数组模板类型。make_shared单次内存分配效率更高但循环引用内存泄漏风险更突出原生new双次分配更灵活。容器铁律优先存储智能指针杜绝裸指针手动管理内存从根源避免内存泄漏。