【C + + 】一文吃透 C++ 智能指针:RAII 思想 + 三大指针 + 实战避坑

【C + + 】一文吃透 C++ 智能指针:RAII 思想 + 三大指针 + 实战避坑

 

🌟个人主页:第七序章  

🌈专栏系列:C++

目录

❄️前言:

☀️一、智能指针的使用

☀️二、RAII和智能指针

⭐RAII思想

⭐智能指针思想

☀️三、C++标准库中智能指针的使用

⭐auto_ptr

⭐unique_ptr

⭐shared_ptr

⭐删除器

☀️四、智能指针的实现原理

⭐share_ptr实现原理

⭐构造函数

⭐拷贝构造

⭐拷贝赋值

⭐析构函数

⭐删除器

☀️五、shared_ptr循环引用问题

⭐weak_ptr解决循环引用问题

⭐weak_ptr

☀️ 六、智能指针总结

☀️七、内存泄漏详解与防范

⭐内存泄漏是什么?为什么危险?

⭐典型的内存泄漏场景分析

⭐常见内存泄漏类型分类

⭐内存泄漏检测工具一览

⭐如何高效避免内存泄漏?

☀️九、shared_ptr的线程安全问题

☀️十、Boost库

☀️十一、本文小结

⭐智能指针使用场景对比表

⭐智能指针常见错误用法避坑表

🌻共勉:


❄️前言:

前面我们已经学完C + + 11,今天我们来学习C + +智能指针。

☀️一、智能指针的使用

还记得,在异常学习的时候,我们分析出了一个问题

double Divide(int x, int y) { if (y == 0) { throw string("the y is zero"); } return (double)x / double(y); } void test(int x, int y) { int* arr = new int[10]; Divide(x, y); delete[] arr; cout << "delete[] arr" << endl; } int main() { while (1) { int x, y; cin >> x >> y; try { test(x, y); } catch (const string& str) { cout << str << endl; } catch (...) { cout << "unknown exception" << endl; } } return 0; } 
在上述代码中,Divide函数,如果y==0就会抛出异常,并且在该函数内没有捕获该异常,就继续将异常抛给外层调用的函数test,在testnew了一个int数组,但是没有捕获Divide函数抛出的异常,程序直接接跳到main函数当中去,就导致申请的空间资源没有被释放。

在异常学习中,我们的解决方法就是在test函数中捕获Divide函数抛出的异常,进行资源的释放再将异常重新抛出。

void test(int x, int y) { int* arr1 = new int[10]; try { Divide(x, y); } catch (...) { delete[] arr; cout << "delete[] arr1" << endl; throw; } delete[] arr; cout << "delete[] arr1" << endl; } 

这样看起来我们似乎解决了这一个问题,但是如果我们开辟了两个数组呢?

void test(int x, int y) { int* arr1 = new int[10]; int* arr2 = new int[10]; try { Divide(x, y); } catch (...) { delete[] arr1; cout << "delete[] arr1" << endl; delete[] arr2; cout << "delete[] arr2" << endl; throw; } delete[] arr1; cout << "delete[] arr1" << endl; delete[] arr2; cout << "delete[] arr2" << endl; } 
在上述代码中,如果我们在new第二个数组时,new抛异常了呢?(这里如果第一个new抛异常,那没啥问题)

我们总不能再给第二个new套一层try吧,那不现实;如果申请了多个资源,每一个都要套上try那太不现实了。

在当时我们也没有解决这一个问题,但是有了智能指针那就好很多了。

☀️二、RAII和智能指针

⭐RAII思想

RAIIResource Acquisition Is Initialization的缩写,它是一个管理资源的类的设计思想;本质上就是利用对象生命周期来管理获取到的动态资源,避免发生内存泄露(这里资源指内存、文件指针、网络连接、互斥锁等等)RAII思想:在获取到资源时把资源委托给一个对象,接着控制对资源的访问,这样资源在该对象的生命周期内始终是有效的,最后该对象生命周期结束,在析构的时候释放了资源;这样我们就保证了资源的正常释放,就可以结局上述资源泄露的问题。

那什么意思呢?

简单来说就是,我们不要去主动管理这些动态资源了,把这些动态资源交个一个对象去管理,这样出了这个对象的作用域,该对象的析构函数就会把这些动态资源自动释放,就不需要我们自己去释放了。
template<class T> class smartptr { public: smartptr(T* ptr) :_ptr(ptr) {} ~smartptr() { delete[] ptr; cout << "delete []" << endl; } private: T* _ptr; }; 

有了上面代码,我们就可以创建smartptr来帮助我们管理动态资源

void test(int x, int y) { smartptr<int> arr1(new int[10]); smartptr<int> arr2(new int[10]); Divide(x, y); } 

可以看到,无论Divide是否抛异常,我们申请的资源都能成功释放(因为arr1arr2出了作用域就调用析构函数,就会对资源进行释放

⭐智能指针思想

通过观察上述代码,我们可以发现一个问题:如何访问动态资源呢?

我们将动态资源交给一个对象去管理,是可以解决资源泄露的问题;但是我们如何去访问这个动态资源呢?

所以为了方便我们访问动态资源,智能指针就还要实现重载operator*operator->opeartor[]这些运算符

这里这种思想就类似于迭代器,我们可以像指针一样去访问迭代器,这里智能指针也一样,我们也要可以像指针一样去访问智能指针。
template<class T> class smartptr { public: smartptr(T* ptr) :_ptr(ptr) {} T& operator*() { return *_ptr; } T& operator[](size_t i) { return _ptr[i]; } T operator->() { return _ptr; } ~smartptr() { delete[] _ptr; cout << "delete []" << endl; } private: T* _ptr; }; 

这里其实还存在一个致命的问题,那就是拷贝的问题:

对于我们自己申请的资源,我们就行拷贝(赋值)时,就是简单的值拷贝,并且释放的时候我们就只需要释放一个即可

但是如果使用智能指针,拷贝肯定不能使用深拷贝(我们想要的就是值拷贝),那我们该如何去释放这个资源呢?

同一个资源是不能释放两次的——现在来看C++库里面的智能指针是如何解决这一问题的。

☀️三、C++标准库中智能指针的使用

C++标准库中智能指针都在这个头文件下;

智能指针有很多种,除了weak_ptr以外都符合RAII和可以像指针一样访问的行为,在原理上来说就是解决拷贝的问题不同。

⭐auto_ptr

auto_ptr:这是C++98中设计出来的智能指针,它解决拷贝问题的方法就是在拷贝时将被拷贝对象资源的管理权转移给拷贝对象

简单来说就是,我把资源转给你,我自己置为nullptr),这个可以说很糟糕,它会把被拷贝对象悬空,我们再访问就会报错;这里不推荐使用auto_ptr

这里为了观察就简单实现一个Date

struct Date { Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout << "~Date()" << endl; } int _year; int _month; int _day; }; int main() { auto_ptr<Date> ap1(new Date); auto_ptr<Date> ap2(ap1);//拷贝之后ap1被置为空 return 0; } 

最后资源也是释放一次。

⭐unique_ptr

unique_ptrC++11设计出来的,它解决拷贝问题的方法就简单粗暴了,直接不支持拷贝,只支持移动;

在不需要拷贝的场景下就推荐使用unique_ptr

通过看文档可以发现,它的拷贝构造和拷贝赋值是delete掉的。
int main() { unique_ptr<Date> up1(new Date); //unique_ptr<Date> up2(up1);//不支持拷贝 //支持移动,但移动之后up1也置为空,使用要小心 unique_ptr<Date> up2(move(up1)); return 0; } 

⭐shared_ptr

前面两个智能指针,一个拷贝是管理权转移,应该干脆就不支持,那还是没有到达我们想要的结果,现在来看shared_ptr

shared_ptrC++11设计的智能指针,它支持拷贝,也支持移动

需要拷贝的场景就需要它了。(其底层使用引用计数来实现的
int main() { shared_ptr<Date> sp1; shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp1); //查看当前有多少对象管理这一资源 cout << sp1.use_count() << endl; sp1->_year = 2025; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; return 0; } 
3 2025 2025 2025 ~Date() 
weak_ptr也是C++11设计的智能指针,它和上述智能指针不同,不支持RAII它不能直接管理资源;

weak_ptr实现本质上是为了解决shared_ptr循环引用导致内存泄露的问题
⭐删除器

智能指针的析构默认是进行delete来释放资源,那也就是说,如果我们将不是new的资源交给智能指针,在析构的时候就会崩溃;

所以智能指针在构造时就支持给一个删除器。

本质上所谓的删除器就是一个可调用对象,这个可调用对象中实现我们需要是释放资源的方式;

当我们在构造智能指针时,给了删除器,在智能指针析构时就会调用删除器去释放资源。

而我们又经常使用new[],所以unique_ptr和shared_ptr都特化了一个[]的版本,我们在使用时只需类型给成T[]即可;(例如:unique_ptr<Date[]> up1(new Date[10])和shared_ptr<Date[]> sp1(new Date[10])。
int main() { unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]); //删除器 //删除器是一个可调用对象,那我们就可以使用仿函数、函数指针、lambda来做删除器 //仿函数 unique_ptr<Date, DeleteArr<Date>> up2(new Date[3]);//类模版这里要传的是类型 shared_ptr<Date> sp2(new Date[3], DeleteArr<Date>());//构造函数这里我们要传对象 //函数指针 //这里类模版要传的是类型,而根据函数类型又没有办法得到函数,构造也要显示传递 unique_ptr<Date, void(*)(Date*)> up3(new Date[3],DeleteArrFunc<Date>); shared_ptr<Date> sp3(new Date[3], DeleteArrFunc<Date>); //lambda auto del = [](Date* ptr) {delete[] ptr; }; //这里我们没办法指定lambda的类型,所以要先创建一个lambda对象然后使用decltype来推它的类型 unique_ptr<Date, decltype(del)> up4(new Date[3], del); //shared_ptr就很好用了,只用在构造函数时传递即可 shared_ptr<Date> sp4(new Date[3], [](Date* ptr) {delete[] ptr; }); return 0; } 
这里shared_ptr1除了可以使用指向资源的指针构造,还可以使用make_shared有初始化资源对象的值直接进行构造。

shared_ptr和unique_ptr都支持operator bool的类型转换,如果智能指针对象是一个空的对象就返回false;赋值返回true。(这样我们就可以直接对智能指针对象进行判断是否为空)。

☀️四、智能指针的实现原理

首先对于auto_ptr,它的拷贝是管理权转移,感觉很不符合逻辑;而unique_ptr是直接不支持拷贝,这两种智能指针实现起来还是非常简单,这里就不详细叙述了;

 template<class T> class auto_ptr { public: auto_ptr(T* ptr) :_ptr(ptr) {} auto_ptr(auto_ptr<T>& p) :_ptr(p._ptr) { p._ptr = nullptr; } auto_ptr<T>& operator=(auto_ptr<T>& ap) { if (*this != ap) { if (_ptr) delete _ptr; _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } T& operator*() { return *_ptr; } T& operator[](size_t i) { return _ptr[i]; } T* operator->() { return _ptr; } ~auto_ptr() { if (_ptr) delete _ptr; } private: T* _ptr; }; template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {} unique_ptr(unique_ptr<T>& p) = delete; unique_ptr<T>& operator=(unique_ptr<T>& p) = delete; unique_ptr(unique_ptr<T>&& p) :_ptr(p._ptr) { p._ptr = nullptr; } T& operator*() { return *_ptr; } T& operator[](size_t i) { return _ptr[i]; } T* operator->() { return _ptr; } private: T* _ptr; }; 

⭐share_ptr实现原理

shared_ptr使用了引用计数,简单来说呢,我们不光要在智能指针中记录要管理的资源,还要记录一下当前管理资源的智能指针的个数,所以这里我们需要一个引用计数;

那如何去实现这个引用计数呢?

这里首先肯定不能在类中存放一个值;

那静态成员变量是否可以呢?

显然是不可以的,因为静态成员变量是属于类的,我们想要的引用计数是和管理资源有关的,所以这里我们就只能存放int*,采用堆上动态开辟的方式,在构造的时候开辟一块空间,在拷贝的时候,将指针值拷贝给另一个对象,并且对值进+1

那这样多个shared_ptr管理一块资源时,当析构的时候就--引用计数,当引用计数减到0时,表示当前析构的就是管理这一块资源的最后一个智能指针对象,就要析构资源。

OK呢,那现在就来简简单单手搓一个简易版shared_ptr出来

首先对于shared_ptr的成员:T*的指针、int*pcount引用计数

简单的operator*operator->operator[]这些就不解释了,直接看代码:

 template<class T> class shared_ptr { T& operator*() { return *_ptr; } T& operator[](size_t i) { return _ptr[i]; } T* operator->() { return _ptr; } private: T* _ptr; int* _pcount; }; 
⭐构造函数

对于构造函数,首先就是默认构造,我们直接将_ptr和_pcount赋值为nullptr即可

然后就是:我们在创建一个shared_ptr的智能指针对象时,要做的就是开辟一块引用计数的空间,并赋值成1(表示当前有一个对象管理这一块资源);然后把传过来的一块资源赋值给_ptr(让ptr指向要管理的资源即可)。

 shared_ptr() :_ptr(nullptr) , _pcount(nullptr) {} shared_ptr(T* ptr) :_ptr(ptr) { _pcount = new int(1); } 
⭐拷贝构造

对于拷贝构造,当我们调用拷贝构造时,就表明我们要将一个智能指针对象管理的资源共享给另一个智能指针对象,此时我们的引用计数要进行+1

这里因为我们调用拷贝构造时,我们当前对象是没有管理任何资源的(_ptrpcount都为nullptr),我们才能直接将被拷贝对象sp_ptr_pcount直接赋值给我们*this_ptr_pcount
 shared_ptr(shared_ptr<T>& sp) :_ptr(sp._ptr) , _pcount(sp._pcount) { (*_pcount)++; } 
⭐拷贝赋值

对于拷贝赋值,它拷贝构造那样,可以直接进行赋值操作;

当我们调用拷贝赋值时,我们当前对象可能是管理着其他资源的;所以我们要先将当前管理的资源进行处理(如果有其他对象管理着这一块资源,那--引用计数即可;如果没有其他对象管理这一块资源,我们还要对其进行释放)。

这一块对资源进行处理的操作,我们可以发现和析构函数的逻辑一样,所以我们可以将其单独写成一个函数release
 void release() { (*_pcount)--; if (*_pcount == 0) { delete _ptr; delete _pcount; } _ptr = nullptr; _pcount = nullptr; } shared_ptr<T>& operator=(const shared_ptr<T>& sp) { release(); _ptr = sp._ptr; _pcount = sp._pcount; (*_pcount)++; return *this; } 
⭐析构函数

对于析构函数,它的逻辑就是--引用计数,如果引用计数减到0,那就释放资源;(逻辑和上面拷贝赋值处理资源的逻辑一样

这里就可以直接复用release

 ~shared_ptr() { release(); } 

这里对于拷贝赋值和析构函数这里,博主还有一种想法:

我们在拷贝赋值的参数那里让他传值传参,这样就会调用一次拷贝构造,构造了一个临时对象sp

我们再让this指向的_ptr_pcountsp进行一下交换,这样出了拷贝赋值函数,sp会自动调用析构函数;

这样我们只需要在析构函数内部实现处理资源的操作就OK了。
 void swap(shared_ptr<T>& sp) { std::swap(_ptr, sp._ptr); std::swap(_pcount, sp._pcount); } shared_ptr<T>& operator=(shared_ptr<T> sp) { swap(sp); return *this; } ~shared_ptr() { (*_pcount)--; if (*_pcount == 0) { delete _ptr; delete _pcount; } _ptr = nullptr; _pcount = nullptr; } 
上面这种方法,博主在vector模拟实现时有所耳闻,也是非常好理解的

我们拷贝赋值的参数写的是shared_ptr<T>,这样在传参时是传值传参,就会去调用拷贝构造,构造一个新的对象sp指向被调用对象管理的资源;

sp的作用域就是operator=函数内,所以我们把this的指向的对象和生成的形参对象sp进行交换(值交换);

这样出了作用域sp要调用析构函数,就会把*this对象原来管理的资源进行处理;

到这里,简易版的shared_ptr就实现完成了

现在我们来加上删除器

⭐删除器
删除器,我们要想在类中可以调用这个删除器,那我们要将删除器存下来;

shared_ptr我们只需要在构造函数中传递就可以了,那我们构造函数如下面所示
 template<class D> shared_ptr(T* ptr, D del) :_ptr(ptr) ,_pcount(new int(1)) ,_del(del) {} 
但是但是,对于这个类型D我们只在构造函数中指定它是什么啊,那在类中如何去存储这个删除器呢?

真的要在模版参数那里多一个吗?库里面也没有多这一个模版参数啊.

这里就使用function包装器就可以解决问题了

因为我们的删除器肯定都是没有返回值(void),且参数肯定是T*,所以使用function<void(T*)>包装即可。

这里在展示代码之前,罗列几个要注意的点:在拷贝赋值时,我们要将删除器一同传递过去;(不同的类型,删除器不一样)在析构逻辑中,我们直接调用删除器去资源,但是对于引用计数的修改还是需要我们就行操作的。删除器我们要给缺省值,当我们在构造函数不穿第二个参数时,我们默认的删除器是delete的。
 template<class T> class shared_ptr { public: explicit shared_ptr(T* ptr = nullptr) : _ptr(ptr) , _pcount(new int(1)) {} template<class D> shared_ptr(T* ptr, D del) :_ptr(ptr) ,_pcount(new int(1)) ,_del(del) {} shared_ptr(shared_ptr<T>& sp) :_ptr(sp._ptr) , _pcount(sp._pcount) { (*_pcount)++; } void swap(shared_ptr<T>& sp) { std::swap(_ptr, sp._ptr); std::swap(_pcount, sp._pcount); std::swap(_del, sp._del); } shared_ptr<T>& operator=(shared_ptr<T> sp) { swap(sp); return *this; } ~shared_ptr() { (*_pcount)--; if (*_pcount == 0) { //delete _ptr; _del(_ptr); delete _pcount; } _ptr = nullptr; _pcount = nullptr; } T& operator*() { return *_ptr; } T& operator[](size_t i) { return _ptr[i]; } T* operator->() { return _ptr; } private: T* _ptr; int* _pcount; function<void(T*)> _del = [](T* ptr) {delete ptr;}; }; 

☀️五、shared_ptr循环引用问题

对于shared_ptr大多数情况下已经可以去管理资源了,支持RAII也支持拷贝;

但是有一种特殊情况,循环引用的场景下,还是会遇到问题的;(会导致资源没得到释放)
struct ListNode { int _date; std::shared_ptr<ListNode> _next; std::shared_ptr<ListNode> _prve; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { std::shared_ptr<ListNode> n1(new ListNode); std::shared_ptr<ListNode> n2(new ListNode); cout << n1.use_count() << endl; cout << n2.use_count() << endl; n1->_next = n2; n2->_prve = n1; cout << n1.use_count() << endl; cout << n2.use_count() << endl; return 0; } 

可以看到,在上述代码中,我们让n1->_next指向n2n2->_prve_prve指向n1

这样指向之后我们发现n1n2的引用计数都变成了2,并且知道程序结束,也没有释放资源。

这种情况,就是我们所说的内存泄露。

我们现在来分析一下,为什么会造成内存泄露呢?

如上图所示,n1->_next = n2n2->_next = n1之后,n1n2对应的引用计数都+1,变成了2

那我们现在n1n2调用析构:

n1n2调用析构之后,其对应的引用计数-1减到了1,没有减到0,这两块空间还没有释放;

那我们右边节点什么时候释放呢,左边节点中的_next管理着,左边节点中_next析构后,右边节点就释放了;

那左边节点的_next什么时候释放呢?,那要等到左边节点析构,右边节点的_prve管理着,等右边节点_prve析构,左边节点就释放了。

左边节点右边的_prve管理着,右边节点左边的_next管理着,那这样都在等对方析构,而谁都不会释放,就形成了循环引用,从而导致内存泄露。

⭐weak_ptr解决循环引用问题

那这个问题如何解决呢?

要像解决这个问题,我们来看一下这个问题的本质是什么?

*那就是我们将n1->_next绑定n2节点时,n2节点的引用计数会+1;将n2->_prve绑定n1节点时,n1几点的引用计数会+1。**这样就导致我们在析构n1和n2时,引用计数-1之后不等于0,就无法释放资源。

简单来说,就是n1->_next和n2->_prve参与了资源的管理。
c++11还有一种智能指针weak_ptr,它就是专门来解决这个问题的。

我们先来看一下weak_ptr

其实通过观察weak_ptr的构造函数和赋值重载就可以发现,它支持使用shared_ptr去构造和赋值;

但是它有一个特点,我们将shared_ptr的智能指针对象赋值给weak_ptr,我们shared_ptr对象的计数引用不会变化(weak_ptr不会参与shared_ptr的管理资源)。

那这样,我们再看上述问题,我们将ListNode结构体中_next_prve的类型改成weak_ptr

那这样,将n1->_next绑定n2节点时,n2节点的引用计数不会+1

n2->_prve绑定n1节点时,n1节点的引用计数不会+1

这样我们在析构n1n2时,引用计数-1就等于0,就会释放资源。
struct ListNode { int _date; //std::shared_ptr<ListNode> _next; //std::shared_ptr<ListNode> _prve; std::weak_ptr<ListNode> _next; std::weak_ptr<ListNode> _prve; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { std::shared_ptr<ListNode> n1(new ListNode); std::shared_ptr<ListNode> n2(new ListNode); cout << n1.use_count() << endl; cout << n2.use_count() << endl; n1->_next = n2; n2->_prve = n1; cout << n1.use_count() << endl; cout << n2.use_count() << endl; return 0; } 
可以看到引用计数并没有+1,也成功析构,没有造成资源泄露。

⭐weak_ptr

这里简单了解有效weak_ptr
  • weak_ptr不支持RAII,也不支持访问资源,我们在文档中也会发现weak_ptr构造没有支持绑定资源,而是支持绑定到shared_ptr;在绑定到shared_ptr时,不会增加shared_ptr的引用计数。
  • weak_ptr没有重载operator*operator->,它不参与资源管理(如果weak_ptr绑定的shared_ptr已经析构了,那如果再去访问就和危险);
  • weak_ptr支持了expired检查指向的资源是否过期,use_count也支持获取shared_ptr的引用计数。
  • weak_ptr还支持了lock,它可以返回shared_ptr;如果资源已经释放,那返回的就是空对象;如果没有释放,那返回的shared_ptr可以进行访问资源。

☀️ 六、智能指针总结

C++ 智能指针Boost 对应主要用途关键机制
auto_ptr管理权转移(已废弃)所有权转移
unique_ptrscoped_ptr独占所有权禁止拷贝,支持移动
shared_ptrshared_ptr共享所有权引用计数
weak_ptrweak_ptr辅助管理共享资源,防循环引用非拥有引用

☀️七、内存泄漏详解与防范

⭐内存泄漏是什么?为什么危险?

内存泄漏(Memory Leak) 是指程序在动态分配内存后,未能在不再使用时及时释放,导致该内存空间永久性不可达,从而造成资源浪费。

注意】:

内存泄漏≠内存“丢失”;而是程序失去了对已分配内存的控制权,这部分内存依然占用物理资源,无法再被回收或利用。

内存泄漏的危害】:

对短生命周期程序影响较小但对操作系统、后台服务、游戏引擎等长期运行程序危害极大,表现为:内存占用不断增加;程序响应变慢、延迟变高;最终导致系统崩溃或“卡死”。

⭐典型的内存泄漏场景分析

void MemoryLeaks() { // 场景 1:手动申请忘记释放 int* p1 = (int*)malloc(sizeof(int)); // 未调用 free(p1) int* p2 = new int; // 未调用 delete p2 // 场景 2:异常安全问题 int* p3 = new int[10]; Func(); // 若此处 Func 抛出异常,则下面 delete[] 无法执行 delete[] p3; } 

常见原因总结】:

忘记释放内存(最常见);异常未捕获导致资源释放语句跳过;循环引用(例如 shared_ptr 的互相引用);new/delete、malloc/free 混用提前 return 导致资源未释放资源转移不清晰(裸指针管理堆内存)

⭐常见内存泄漏类型分类

1.【堆内存泄漏(Heap Leak)

程序使用 new/malloc 动态分配堆内存由于设计缺陷未调用 delete/free 释放,导致内存永久占用堆泄漏是内存泄漏中最常见、最典型的一种。

2.【系统资源泄漏(Resource Leak)

不只是“内存”会泄漏,系统级资源也可能泄漏,如:文件描述符(File Descriptor);网络套接字(Socket);管道、线程句柄等;

这些资源一旦未正确关闭,将导致系统资源枯竭,影响系统稳定性。

⭐内存泄漏检测工具一览

Linux 下常用检测工具】:

工具名称功能特点
Valgrind强大但运行缓慢,最常用的内存检测工具
AddressSanitizer (ASan)编译时加入 -fsanitize=address,效率高
gperftoolsGoogle 出品,性能友好

Windows 下工具推荐】:

Visual Leak Detector (VLD):集成简单,适用于 Visual Studio;Dr. Memory:Valgrind 的 Windows 替代;CRT Debug 功能:使用 _CrtDumpMemoryLeaks()

⭐如何高效避免内存泄漏?

编程规范层面

【计数追踪法】:每次 new/malloc +1,每次 delete/free -1,程序结束时判断是否为 0;【RAII 原则】:资源绑定对象生命周期,避免手动释放;【构造异常安全】:在构造中申请资源,异常抛出前务必释放;【析构函数声明为虚函数】:基类指针指向子类对象时,确保析构函数调用链完整;【malloc/free 和 new/delete 不混用】:必须匹配释放方式;避免裸指针直接管理堆资源

2.【工具与辅助手段

使用智能指针(如 unique_ptrshared_ptr)管理资源,自动释放;使用内存检测工具进行“事后排查”;引入内存池、资源池管理统一分配与释放。

内存泄漏问题往往“悄无声息”,但在系统级项目中可能是致命的。良好的编程习惯、正确使用智能指针、配合检测工具,是预防与排查内存泄漏的有效手段。

☀️九、shared_ptr的线程安全问题

  • shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以 shared_ptr 引用计数是需要加锁或者原子操作保证线程安全的。
  • shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr 管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
  • 下面的程序会崩溃或者A资源没释放,bit::shared_ptr 引用计数从 int* 改成 atomic* 就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。
  • 下面的程序会崩溃或者A资源没释放,bit::shared_ptr 引用计数从 int* 改成 atomic* 就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。
int main() { lrq::shared_ptr<AA> p(new AA); const size_t n = 100000; mutex mtx; auto func = [&]() { for (size_t i = 0; i < n; ++i) { // 这里智能指针拷贝会++计数 lrq::shared_ptr<AA> copy(p); { unique_lock<mutex> lk(mtx); copy->_a1++; copy->_a2++; } } }; thread t1(func); thread t2(func); t1.join(); t2.join(); cout << p->_a1 << endl; cout << p->_a2 << endl; cout << p.use_count() << endl; return 0; }

☀️十、Boost库

最后,我们来了解一下Boost

Boost库是c++语言标准库提供的扩展的一些C++程序库,Boost社区建立的初衷之一就是为了c++标准化工作提供参考。

Boost社区的发起人Dawes本人就是C++委员会的成员之一。

Boost库的开发中Boost社区在这个方向上取得了丰硕的成果。C++98有了第一个智能指针auto_ptrC++boost库给出了更多实用的智能指针scoped_ptr/scoped_array和shared_ptr/shared_array/weak_ptrC++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。C++ 11,引⼊了unique_ptrshared_ptrweak_ptr。需要注意的是unique_ptr对应boost的

☀️十一、本文小结

核心模块关键内容
智能指针核心解决问题1. 异常场景下动态资源未释放导致的内存泄漏;2. 手动管理资源(new/delete)的繁琐与失误;3. 资源共享时的释放时机同步问题
RAII 思想资源获取即初始化,将动态资源委托给对象,利用对象生命周期自动管理资源(构造时获取资源,析构时释放资源),确保资源不泄露
标准库智能指针(<memory>头文件)
auto_ptr(已废弃)- 核心机制:拷贝时转移资源管理权(原对象指针置空);- 缺陷:使用原对象会触发空指针访问,逻辑不符合直觉
unique_ptr- 核心机制:独占资源所有权,禁止拷贝,支持移动语义(move);- 优势:高效轻量,无额外开销,支持数组资源(unique_ptr<T []>)
shared_ptr- 核心机制:共享资源所有权,通过引用计数(int* _pcount)跟踪管理对象数量;- 关键特性:支持拷贝 / 赋值,引用计数为 0 时自动释放资源;支持自定义删除器
weak_ptr- 核心作用:解决 shared_ptr 循环引用导致的内存泄漏;- 关键特性:不参与资源管理(不增加引用计数),无 operator * 和 operator->;支持 expired () 检查资源有效性、lock () 获取 shared_ptr 访问资源
智能指针实现核心
shared_ptr 核心成员T* _ptr(指向资源)、int* _pcount(引用计数,堆上分配)、function<void (T*)> _del(删除器,默认 delete)
关键函数实现1. 构造:默认构造(指针 / 计数置空)、带参构造(初始化资源 + 计数 = 1)、删除器构造(绑定自定义释放逻辑);2. 拷贝构造:共享指针 + 计数 + 1;3. 拷贝赋值:先释放当前资源(计数为 0 则删除),再共享目标资源 + 计数 + 1;4. 析构:计数 - 1,为 0 则调用删除器释放资源 + 计数
删除器支持形式仿函数、函数指针、lambda 表达式,适配非 new 分配的资源(如数组、文件指针等)
常见问题与解决方案
循环引用(shared_ptr)- 场景:两个 shared_ptr 互相引用(如链表节点_next/_prev),导致引用计数无法归零;- 解决方案:将其中一个指针改为 weak_ptr
线程安全问题- 引用计数:需通过原子操作(atomic)或互斥锁保证线程安全(多线程拷贝 / 析构 shared_ptr 时修改计数);- 资源对象:线程安全需用户自行控制(shared_ptr 不管理对象内部数据的线程安全)
内存泄漏相关
内存泄漏定义动态分配的内存未被释放,程序失去对该内存的控制权,导致资源浪费、程序变慢甚至崩溃
典型泄漏场景1. 手动申请忘记释放(new 未 delete、malloc 未 free);2. 异常跳过释放语句;3. shared_ptr 循环引用;4. new/delete 与 malloc/free 混用;5. 提前 return 导致释放语句未执行
泄漏检测工具Linux:Valgrind(功能强)、AddressSanitizer(编译选项 - fsanitize=address,高效)、gperftools;Windows:Visual Leak Detector(VLD)、Dr. Memory、CRT Debug(_CrtDumpMemoryLeaks ())
泄漏防范措施1. 遵循 RAII 原则,优先使用智能指针;2. 避免裸指针直接管理堆资源;3. 统一使用 new/delete 或 malloc/free,不混用;4. 基类析构函数声明为虚函数;5. 利用工具常态化检测

⭐智能指针使用场景对比表

使用场景推荐智能指针不推荐选择核心原因分析
单个对象独占资源(无需拷贝)unique_ptrauto_ptr、shared_ptrunique_ptr 轻量无额外开销,禁止拷贝避免资源竞争,支持移动语义满足转移需求
数组类型资源(new [] 分配)unique_ptr<T[]>shared_ptr(默认)、auto_ptrunique_ptr<T []> 默认匹配 delete [] 释放,shared_ptr 需手动指定数组删除器才安全
资源需要多对象共享(需拷贝 / 赋值)shared_ptrunique_ptr、auto_ptrshared_ptr 通过引用计数同步释放时机,支持多对象协作访问同一资源
链表 / 树等循环引用结构(双向指针)weak_ptr + shared_ptr仅 shared_ptrweak_ptr 不增加引用计数,打破循环引用,通过 lock () 安全访问 shared_ptr 管理的资源
函数返回动态资源(无需共享)unique_ptr裸指针、auto_ptr避免裸指针所有权模糊,unique_ptr 通过移动语义返回,高效且无泄漏风险
容器存储动态资源(需频繁增删)unique_ptr(转移)、shared_ptr(共享)auto_ptrauto_ptr 拷贝时转移所有权,容器操作(如排序、拷贝)会导致指针失效;unique_ptr 需移动,shared_ptr 支持直接存储
非 new 分配的资源(如 malloc、文件句柄)shared_ptr(自定义删除器)、unique_ptr(自定义删除器)无默认推荐需通过删除器指定释放逻辑(如 free、fclose),确保资源正确释放
线程间共享动态资源shared_ptrunique_ptr、auto_ptrshared_ptr 的引用计数线程安全(需原子操作 / 加锁实现),支持多线程安全拷贝 / 析构

⭐智能指针常见错误用法避坑表

错误用法场景问题表现 / 风险正确解决方案
使用auto_ptr进行拷贝或赋值原对象指针被置空,后续访问触发未定义行为(如崩溃)废弃auto_ptr,改用unique_ptr(禁止拷贝)或shared_ptr(共享所有权)
unique_ptr强制拷贝(如unique_ptr<int> p2(p1)编译报错(已删除拷贝构造)若需转移所有权,使用std::moveunique_ptr<int> p2(std::move(p1))(转移后原指针失效)
shared_ptr管理数组资源未指定删除器调用delete而非delete[],导致内存泄漏或崩溃1. 使用shared_ptr<T[]>(C++17 及以上支持);2. 手动指定数组删除器:shared_ptr<int>(new int[10], [](int* p){delete[] p;})
shared_ptr循环引用(如双向链表节点互相引用)引用计数无法归零,资源永久泄漏将循环引用的一方改为weak_ptr,打破计数依赖:struct Node { weak_ptr<Node> next; shared_ptr<Node> prev; }
weak_ptr直接访问资源(如*wp编译报错(weak_ptroperator*先通过lock()获取shared_ptrauto sp = wp.lock(); if (sp) { /* 访问*sp */ }
智能指针管理栈上对象(如unique_ptr<int> p(&a)析构时调用delete释放栈内存,触发未定义行为智能指针仅管理动态分配资源new/malloc等),栈对象无需智能指针管理
shared_ptr与裸指针混用(如delete sp.get()重复释放资源,导致崩溃禁止手动释放智能指针管理的资源,由智能指针自动处理;如需获取裸指针,仅用于临时访问(不负责释放)
多线程直接修改shared_ptr指向的对象对象内部数据竞争,导致数据错乱对对象的访问需加锁(如std::mutex),shared_ptr仅保证自身引用计数的线程安全
函数参数传递shared_ptr时按值传递不必要的引用计数增减,性能损耗非必要时按引用传递:void func(const shared_ptr<int>& sp)
自定义删除器类型不匹配(如unique_ptr未指定删除器类型)编译报错或调用错误删除器unique_ptr需显式指定删除器类型:unique_ptr<int, void(*)(int*)> p(new int[10], [](int* p){delete[] p;})

🌻共勉:

以上就是本篇博客的所有内容,如果你觉得这篇博客对你有帮助的话,可以点赞收藏关注支持一波~~🥝


Read more

YOLOs-CPP:一个免费开源的YOLO全系列C++推理库(以YOLO26为例)

YOLOs-CPP:一个免费开源的YOLO全系列C++推理库(以YOLO26为例)

YOLOs-CPP:一个免费开源的YOLO全系列C++推理库(以YOLO26为例) * 前言 * 环境要求 * 相关介绍 * C++简介 * ONNX简介 * ONNX Runtime 简介 * **核心特点** * YOLOs-CPP简介 * 核心能力 * Windows下使用YOLOs-CPP * 下载YOLOs-CPP项目 * Windows * YOLOs-CPP项目结构 * 导出ONNX * 使用VS2019编译运行C++推理 * infer_utils.hpp * main.cpp * 推理结果(以YOLO26为例)) * 图像分类 * 目标检测 * 实例分割 * 旋转框检测 * 姿势估计 * 更多功能 * 参考 前言 由于本人水平有限,难免出现错漏,敬请批评改正。更多精彩内容,可点击进入Python日常小操作专栏、OpenCV-Python小应用专栏

By Ne0inhk
C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展

C++ 运算符重载:自定义类型的运算扩展 💡 学习目标:掌握运算符重载的核心语法与规则,能够为自定义类型重载常用运算符,实现类对象的灵活运算。 💡 学习重点:运算符重载的基本形式、成员函数与全局函数重载的区别、常见运算符的重载实现、禁止重载的运算符。 一、运算符重载的概念与核心价值 ✅ 结论:运算符重载是 C++ 静态多态的重要体现,允许为自定义类型(如类、结构体)重新定义运算符的行为,让自定义对象可以像内置类型一样使用运算符。 运算符重载的核心价值: 1. 简化代码书写:用直观的运算符替代繁琐的成员函数调用,提升代码可读性 2. 统一操作风格:让自定义类型的运算逻辑与内置类型保持一致,降低学习和使用成本 3. 扩展类型功能:根据业务需求定制运算符的行为,满足自定义类型的运算需求 ⚠️ 注意事项:运算符重载不会改变运算符的优先级和结合性,也不会改变运算符的操作数个数。 二、运算符重载的基本语法 运算符重载的本质是函数重载,分为成员函数重载和全局函数重载两种形式。 2.1 成员函数重载语法 将运算符重载函数定义为类的成员函数,语法格式如下: class

By Ne0inhk
【 C/C++ 算法】入门动态规划 ----- 简单多状态 dp 问题》打家劫舍 和 股票买卖问题

【 C/C++ 算法】入门动态规划 ----- 简单多状态 dp 问题》打家劫舍 和 股票买卖问题

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论 : ———————— 本章是dp的第三章,从第一章的简单理解dp的核心框架和写法&一维dp,再到第二章的路径问题&二维dp,到本章的多状态dp问题,本章将结合前面的所有基础引入多状态这个问题,并将由浅到深的从简单的打家劫舍两状态的dp到最后股票问题的四状态dp进行以练代学的方式学习,并且过程中会不断总结(具体见目录)。友情提示若没看过前面篇章的动规小白一定要先看看前面两章并简单练习下再往后看(一维dp - 路径dp),后续还将持续更新,敬请期待~ 早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。 打家劫舍 常见的思考是否使用打家劫舍问题时,遇见相邻问题不能选择此时就能思考是不是要使用打家劫舍 打家劫舍,常使用个dp表进行存储情况 1. f [ i ]:选择 i 位置时的最大价值 2. g [ i ]:不选择 i 位置时的最大价值 具体训练:

By Ne0inhk