C++智能指针进阶详解|enable_shared_from_this、weak_ptr、循环引用、容器存储、make_shared/make_unique底层
2026/8/2 4:03:36 网站建设 项目流程

本篇博客基于完整可运行源码,深度讲解C++智能指针进阶核心知识点。涵盖自定义实体类、enable_shared_from_this使用陷阱、weak_ptr弱指针原理、循环引用内存泄漏解决方案、unique_ptr/shared_ptr容器存储规则、make系列函数底层差异。

一、全局头文件与前置说明

#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; #include<cstdlib> #include<assert.h> #include<atomic> #include<memory> #include<vector>//底层是连续的内存空间,list则是链表,各个节点之间的内存互不联系 #include<list> // 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_this<Object> //继承这个类,就可以使用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_ptr<Object> GetObject() { //return std::shared_ptr<Object>(this); //这样子写是通过值拷贝一个右值智能指针,返回它,但是值得注意的是,智能指针的值拷贝有一个特点就是, //它会new一个_Users=1和_weaks=1的新的计数器,不是原来那个堆内存资源原有的计数器,就有两个计数器,进而程序出现错误 //但是,如果你硬要通过调用该函数返回的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_ptr<Object>(this)。

  • 错误原因:该写法会新建一套独立的_Users、_Weaks计数器,和原有资源计数器不互通,同一块堆内存存在两套计数器,最终导致程序出错、重复释放。

  • 正确方案:继承enable_shared_from_this,使用shared_from_this(),复用原有资源的计数器,不会新建计数器,保证资源唯一绑定。

四、enable_shared_from_this 可运行测试用例

#if 1 int main() { std::shared_ptr<Object> p(new Object(100)); p->Print(); std::shared_ptr<Object> p1 = p->GetObject(); } #endif

测试解析(纯原生逻辑提炼)

  • 初始智能指针p管理堆对象,调用GetObject通过shared_from_this获取新shared_ptr。

  • 全程复用同一组引用计数器,仅递增计数,无内存错乱、无重复释放问题。

五、weak_ptr弱指针基础特性测试

#if 0 int main() { std::weak_ptr<Int> pw; { std::shared_ptr<Int> pa(new Int(100)); pw = pa; } //此时弱引用仍为1,引用技术块的内存不会释放。 return 0; } #endif
  • weak_ptr赋值给shared_ptr后,不会增加强引用计数。

  • 局部shared_ptr析构后,对象资源被释放,但弱引用计数依旧为1。

  • 弱引用计数不为0时,引用计数控制块内存不会被释放,会保留资源状态信息。

六、shared_ptr循环引用问题与weak_ptr解决方案

#if 0 //循环引用问题: struct Child; struct Parent { //std::shared_ptr<Child> child; std::weak_ptr<Child> 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_ptr<Parent> parent; std::weak_ptr<Parent> parent; Child() { std::cout << "Child()" << std::endl; } ~Child() { std::cout << "~Child()" << std::endl; } }; int main() { std::shared_ptr<Child> pc(new Child()); std::shared_ptr<Parent> 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_ptr,weak_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_ptr<Int> wp; { std:;shared_ptr<Int> pa(new Int(100)); pa->Print(); wp = pa; cout<<"wp.use_count()="<<wp.use_count()<<endl; cout<<wp.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_ptr<Int> 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::list<std::unique_ptr<Int>> ilist; std::list<std::shared_ptr<Int>> slist; std::unique_ptr<Int> 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::vector<std::unique_ptr<Int>> pavec; int n = 10; pavec.reserve(n);//先预留10个空间 #if 0 for (int i = 0; i < n; i++) { pavec.push_back(std::unique_ptr<Int>(new Int(i)));//注意这里是右值,右值在这里调用的是unique的移动语义构造(资源的转移),因为赋值以及拷贝构造函数已经被删除了。 } #endif for (int i = 0; i < n; i++) { std::unique_ptr<Int> p(new Int(i)); pavec.push_back(std::move(p)); //这里是具名对象,是左值,但是在unique_ptr的函数中,左值并没有拷贝构造函数和移动构造函数(移动构造和移动赋值本身就是为右值所准备的),所以就无法进行操作,会报错。 //如果不使用 std::move,p 会尝试通过拷贝成为 vector 的一员,也就是说p要先拷贝一个自己的副本,这个副本是存在于这个pavev数组的,但这,这个智能指针并没有拷贝构造函数, //所以这种左值的操作对于 unique_ptr 是不允许的。 //使用 std::move 后,p 本身(它的所有权)成为 vector 的一员,而不是创建一个副本,也就是将资源转移到这个pavec这个数组底下。 //感悟: //我们创建一个左值和一个数组(或者一些容器)的时候,这个左值的地址往往是不在容器所管理的地址的范围内的,那么如果我们想要这个左值存储在这个容器里头的时候,有两种方式: //要么拷贝创建一个处于容器管理地址范围内的副本,成为这个容器中,能够通过下标访问的一员。或者,将这个左值转换成右值,并通过资源的转移,将资源转移到这个容器底下。 } std::unique_ptr<Int> vec(new Int(10)); std::unique_ptr<Int> vec2= std::make_unique<Int>(10);//make_unique是生成这个智能指针的函数,这里生成的智能指针由vec2接收 //也就是说,std::make_unique<Int>(10)与new Int(10)是等价的。 std::unique_ptr<Int[]> vec3 (new Int[10]);//unique_ptr的数组版本,这里生成的智能指针由vec3接收. //std::unique_ptr<Int> vec3(new Int[10]); error,因为这里显示的vec本身管理的是一组指针,但是类型写成了一个,而不是数组,导致它实际上之管理头一个元素, // 析构的时候,只会析构头一个元素,剩下的全部内存泄露 std::unique_ptr<Int[]> vec4 = std::make_unique<Int[]>(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::vector<std::shared_ptr<Int>> pavec; //在容器中尽量插入指针不要是裸指针,要插入智能指针,因为加入这个裸指针是堆区的指针,你还要手写一个函数去对原本在这个数组里面现在却不在了的指针进行销毁,回收堆内存,十分麻烦。 int n = 10; pavec.reserve(n); for (int i = 0; i < n; i++) { #if 0 pavec.push_back(std::make_shared<Int>(i));//make_shared是生成这个智能指针的函数,这里生成的智能指针由pavec接收 pavec.push_back(std::shared_ptr<Int>(new Int(i)));//二者同unique智能指针的使用方式是一样的,他们实际的效果一样,但是底层实现却有所不同 //不同之处体现在哪里 //首先,make_shared是一个生成共享智能指针的函数,它分配的内存中,引用计数器和指针都是在同一个内存块的,也就是说这个计数器和指针都是连续的紧挨的。 //而我们shared_ptr设计的时候,共享智能指针的计数器以及指针是处于堆内存中的两个毫无关联的位置上的,二者在物理上不会相邻,换一种角度来说就是,它要分配两次堆内存 //然而make_shared只分配一次内存,在这内存中就已经包含了引用计数器以及指针。例如[控制块 | 对象] //当然这样设计也是有优缺点的。 //首先make_shared内存中的设计,就体现了相比于传统的定义共享指针的方式,它的效率更高,将两次操作直接合并为一次操作 //并且你去回收这块内存的时候,只需回收一次。 //但是它的缺点就是: //1.如果这个对象很大,那么这个一整体的内存占用也会很大,另外循环引用的问题也会更加突出。循环引用会导致,当你的共享计数为0的时候,让你的弱引用不为0,导致共享指针 //指向的资源是可以释放的,但是你的计数器因为弱引用计数不为0,而无法进行释放,造成内存泄露,无法回收给系统继续使用,久而久之,你的系统资源就会耗尽。 #endif std::shared_ptr<Int> pa = std::make_shared<Int>(i); pavec.push_back(pa); } } #endif

知识点解析(纯原生注释提炼|面试必问)

  • 容器存储规范:禁止存储裸指针,裸指针需要手动编写销毁逻辑回收内存,极易造成内存泄漏;优先存储智能指针,自动管理生命周期。

  • 原生new+shared_ptr底层:分配两次堆内存,对象内存、引用计数控制块内存相互独立、不连续。

  • make_shared底层:仅分配一次连续堆内存,内存布局为【控制块 + 对象】,分配、回收效率更高。

  • make_shared优点:合并两次内存操作为一次,运行效率高,内存回收简单。

  • make_shared缺点:大对象场景内存占用集中;循环引用场景泄漏问题更严重,弱引用残留会导致控制块无法释放,长期运行耗尽系统资源。

十一、全文原生注释核心面试总结

  • 智能指针系统重载逻辑完善,设计思路与手写智能指针完全一致,可直接使用。

  • enable_shared_from_this 解决类内返回自身shared_ptr的双计数器崩溃问题,禁止裸指针二次构造。

  • weak_ptr无引用计数增减、不管控资源生命周期,唯一核心作用是解决shared_ptr循环引用内存泄漏。

  • unique_ptr独占资源、禁止拷贝,左值入容器必须std::move转移所有权,数组资源必须匹配数组模板类型。

  • make_shared单次内存分配效率更高,但循环引用内存泄漏风险更突出;原生new双次分配更灵活。

  • 容器铁律:优先存储智能指针,杜绝裸指针手动管理内存,从根源避免内存泄漏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询