📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
C++之智能指针
- 为什么需要智能指针
- 什么是内存泄漏
- 内存泄漏的危害
- 如何检测内存泄漏
- 常见的几种内存泄漏
- 如何规避内存泄漏
- 如何使用智能指针
- RAII机制
- C++中的几种智能指针介绍
- auto_ptr智能指针
- 模拟实现
- 测试
- unique_ptr智能指针
- unique_ptr智能指针的简单模拟实现
- 测试
- shared_ptr智能指针
- 什么是引用计数
- 错误版本的shared_ptr
- 正确版本的shared_ptr
- shared_ptr智能指针还存在的问题
- weak_ptr
- 模拟实现
- delete和delete[]
- C++11和boost中智能指针的关系
前言:上篇博客,我们提到C++中的异常管理,其中的异常安全会导致一系列问题,其中一个比较严重的问题就是内存泄漏,今天我们将学习智能指针,帮助我们更好的减少异常安全的发生。
为什么需要智能指针
上面谈到了,C++由于没有像
java
中的垃圾回收机制,所以需要手动管理内存,可能会导致内存泄漏,所以需要智能指针去规范指针的行为,特别是减少内存泄漏问题的发生。
看下面的代码,思考为什么需要智能指针:
void Func1()
{int* a = (int*)malloc(sizeof(int));int* b = (int*)malloc(sizeof(int));throw "出现异常";//释放相应的空间free(a);free(b);
}
中间抛出了异常,由于动态申请的内存需要手动去管理,直接抛出异常,就会导致内存泄漏。但是如果我们使用智能指针就不需要手动去管理了。如果不使用智能指针就会让代码的可读性变的很差。
看下面的代码:
void Func()
{int* p1 = new int[3];int* p2,*p3;try{p2 = new int[3];try{p3 = new int[3];}catch (...){delete[] p1;delete[] p2;//delete[] p3;p3 申请空间出现问题了,不用释放p3的空间throw;}}catch (...){delete[] p1;//p2申请空间出现问题了throw;}//其它程序//都没有出现问题delete[] p1;delete[] p2;delete[] p3;throw;
}
虽然我们手动的释放了内存,但是情况也是极为复杂,不仅代码可读性不好,而且容易漏掉某个申请内存指针,从而造成内存泄漏。
什么是内存泄漏
内存泄漏就是程序在运行的过程中,一直在申请内存,但是使用完内存忘记释放内存了,直到程序挂掉才会释放掉内存的行为。
内存泄漏的危害
内存泄漏会导致内存不足,系统会被迫挂掉其所在的程序,导致我们的程序出现问题。
如何检测内存泄漏
常见的检测内存泄漏的办法,就是借助第三方的工具,每个平台的工具不同,大家可以了解一下。
windows下常见的检测内存泄漏的工具VLD
Linux下常见的内存泄漏检测工具
其它工具
常见的几种内存泄漏
- 堆内存泄漏:
- 堆内存泄漏是指的是我们调用
new
/malloc
/calloc
/realloc
等函数申请后,忘记使用delete
或者free
释放在堆上申请的空间。- 系统资源内存泄漏
- 系统资源内存泄漏指的是我们在程序中使用系统资源后但是忘记正确的释放了,比如使用套接字、文件描述符、锁等,会使系统资源较少,严重可能影响程序的运行。
如何规避内存泄漏
- 使用智能指针:如
std::unique_ptr
和std::shared_ptr
,它们可以自动管理内存,减少手动释放内存的需要。- RAII(资源获取即初始化):确保资源在其作用域结束时自动释放。
- 异常安全:在编写可能抛出异常的代码时,确保资源在异常发生时能够被正确释放。
- 定期检测:使用内存检测工具(如Valgrind)来检测和定位内存泄漏。
- 代码审查:定期进行代码审查,检查潜在的内存泄漏问题。
如何使用智能指针
RAII机制
我们的C++中的智能指针,就是使用的RAII(Resource Acquisition Is Initialization)的机制实现的,它又被称为资源获取就是初始化,是C++语言中一种管理内存资源、避免泄漏的重要机制。其核心思想是将资源的申请与对象的初始化(构造函数)绑定,将资源的释放与对象的析构绑定,从而实现资源申请和释放的自动化。不再需要我们手动管理内存。
使用RAII
机制实现简单的智能指针,解决内存泄漏的问题:
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):ptr_(ptr){}~SmartPtr(){cout << "~SmartPtr(): " << ptr_ << endl;delete ptr_;}
private:T* ptr_;//要管理的对象的指针
};
上面简单的智能指针就可以完成内存的自动释放了,因为析构函数是自动调用的
这并不是完整的智能指针,智能指针需要模拟指针的行为,所以需要重载operator*()
和operator->()
函数。
C++中的几种智能指针介绍
auto_ptr智能指针
auto_ptr
类在c++98的时候就出现了:
模拟实现
template<class T>class auto_ptr{public:auto_ptr(T* ptr) :ptr_(ptr){}auto_ptr(auto_ptr<T>& x)//浅拷贝,智能指针只负责管理资源,自己不拥有资源,所以它内部不会深拷贝,不能加const,因为我们不能让两个智能指针管一个资源,会造成多次析构{ptr_ = x.ptr_;x.ptr_ = nullptr;}~auto_ptr(){cout << "~auto_ptr()" << ptr_ << endl;delete ptr_;}T& operator*() {return *ptr_;}T* operator->() {return ptr_;}private:T* ptr_;};
- 上面注释部分也解释了为什么智能指针只需要浅拷贝,因为它只负责管理资源,自己并不拥有资源。
auto_ptr
的拷贝函数在拷贝的同时把前一个指针的ptr_
置为空,是为了防止同一指针多次析构的问题。但是这样做也会造成悬空问题。
测试
#include"SmartPtr.h"int main()
{mystd::auto_ptr<int> sp1(new int(1));mystd::auto_ptr<int> sp2(sp1);*sp2 += 10;int* x = nullptr;// 悬空/**sp1 += 10;*/return 0;
}
运行结果:
auto_ptr
虽然也支持RAII
机制,但是它暴力的处理多个智能指针管理同一资源的问题,会造成被拷贝对象指针为空,如果我们继续使用这个被拷贝对象就会出问题,下面我们来看unique_ptr
版本的智能指针。
unique_ptr智能指针
unique_ptr
版本的智能指针是C++11出现的,它的函数方法比较简单我们就不介绍了,参考官方文档查看。
unique_ptr智能指针的简单模拟实现
我们通过简单的模拟实现,来模拟这个智能指针的行为。
template<class T>class unique_ptr{public:unique_ptr(T* ptr):ptr_(ptr){}unique_ptr(const unique_ptr<T>& x) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& x) = delete;~unique_ptr(){cout << "~unique_ptr()" << ptr_ << endl;}T& operator*() {return *ptr_;}T* operator->() {return ptr_;}private:T* ptr_;};
unique_ptr
为了解决同一指针多次析构和被拷贝对象指针被置空的问题,它暴力的将拷贝构造和赋值构造给禁止了,delete
关键字我们在C++11简介篇已经谈过了。
测试
#include"SmartPtr.h"int main()
{mystd::unique_ptr<int> sp1(new int(1));//mystd::unique_ptr<int> sp2(sp1);mystd::unique_ptr<int> sp2(new int(10));//sp1 = sp2;*sp2 += 10;*sp1 += 10;return 0;
}
运行结果:
unique_ptr
指针看似没有问题,但是却不允许多个智能指针管理同一资源,那有些场景我们就是需要多个智能指针管理同一资源,unique_ptr
就无法解决需求,下面我们引出拥有引用计数版本的智能指针shared_ptr
.
shared_ptr智能指针
类成员函数我们同样不再介绍,有需要可以自行访问官方文档.
什么是引用计数
引用计数就是一个计数器,在
string
类的深拷贝问题中,我们提到过,写时深拷贝,但是只读的情况,我们不用拷贝,调用拷贝构造时将count++
,执行浅拷贝,析构时count--
,也解决了浅拷贝多次析构同一空间的问题,这里是相似的思路。
错误版本的shared_ptr
下面是错误的版本,思考引用计数出现了什么问题:
错误版本,使用引用计数,计数使用静态成员template<class T>class shared_ptr{public:shared_ptr(T* ptr):ptr_(ptr){cout << "shared_ptr(T* ptr): " << ptr_ << endl;count = 1;}shared_ptr(const shared_ptr<T>& x){count++;ptr_ = x.ptr_;}~shared_ptr(){if (--count == 0){cout << "~shared_ptr(): " << ptr_ << endl;delete ptr_;}}int use_count() const{return count;}T* operator->() const{return ptr_;}T& operator*() const{return *ptr_;}private:T* ptr_;static int count;};template<class T>int shared_ptr<T>::count = 0;
使用静态的
count
,看似好像没有问题,实则问题不小,因为静态count
是属于整个类,大家看到都是一个count
,但是我们的智能指针不仅仅会管理一份资源,可能会管理多份资源,所以使用静态的count
肯定解决不了问题。
测试函数:
#include"SmartPtr.h"int main()
{mystd::shared_ptr<int> sp1(new int(1));cout << sp1.use_count() << endl;mystd::shared_ptr<int> sp2(sp1);cout << sp1.use_count() << endl;*sp2 += 10;*sp1 += 10;mystd::shared_ptr<int> sp4(new int(5));mystd::shared_ptr<int> sp3(sp2);cout << sp3.use_count() << endl;return 0;
}
运行结果:
可以看到外面申请了两份资源,但是最后只释放了一份,说明使用静态的
count
来进行计数,肯定行不通。
我们需要让所有管理一份资源的智能指针使用同一个
count
,但是管理不同资源的智能指针使用不同的count
,可以自己在堆上申请一份空间,作为计数器count
的地址,然后拷贝构造的同时将(*count)++
。
正确版本的shared_ptr
每个指针对象都有一个自己的计数器,当调用构造函数的时候给这个计数器分配资源,其它情况将计数器指针赋值给其它智能指针对象的
count_
即可,并将计数器中的值++
,这样很好的解决了同一指针多次析构的问题。
//正确处理办法,采用template<class T>class shared_ptr{public:shared_ptr(T* ptr) :ptr_(ptr),count_(new int(1)){cout << "shared_ptr(T* ptr): " << ptr_ << endl;}shared_ptr(const shared_ptr<T>& x){count_ = x.count_;(*count_)++;ptr_ = x.ptr_;}shared_ptr<T>& operator=(const shared_ptr<T>& x){if (x.ptr_ != ptr_){release();//把它之前管理的指针计数--count_ = x.count_;(*count_)++;ptr_ = x.ptr_;}return *this;}int use_count() const{return *count_;}void release(){if (--(*count_) == 0)//指针计数减到0,可以析构对象了{cout << "~shared_ptr() : " << ptr_ << endl;delete ptr_;delete count_;}}T* get() const {return ptr_;}~shared_ptr(){cout << "~shared_ptr()" << endl;release();}T* operator->() const{return ptr_;}T& operator*() const{return *ptr_;}private:T* ptr_;int* count_;//计数此时管理此指针的对象};
注意在赋值构造的时候有点特殊,需要调用
release
方法,先将此对象管理的资源计数器--
,并判断释放需要析构那份资源。才能将其赋值为新的指针,并将新的计数器++
.
测试函数:
#include"SmartPtr.h"int main()
{mystd::shared_ptr<int> sp1(new int(1));cout << sp1.use_count() << endl;mystd::shared_ptr<int> sp2(sp1);cout << sp1.use_count() << endl;*sp2 += 10;*sp1 += 10;mystd::shared_ptr<int> sp4(new int(5));mystd::shared_ptr<int> sp3(sp2);cout << sp3.use_count() << endl;sp4 = sp4;sp1 = sp2;sp1 = sp4;return 0;
}
运行结果:
我们开的两个指针资源,顺利得到释放了,并且解决了多次析构同一空间的问题。
看似shared_ptr
版本的指针已经很完美了,解决了我们上述出现的所有问题,但是在特定场景还是会存在一些问题,让内存泄漏。
shared_ptr智能指针还存在的问题
也不能完全说是它的问题,准确的来说应该是解决引用计数后导致的一个问题。
看下面测试代码,通过结果思考为什么会出现这种情况:
#include"SmartPtr.h"template<class T>
struct ListNode
{T val_;mystd::shared_ptr<ListNode<int>> next_;mystd::shared_ptr<ListNode<int>> pre_;ListNode(T val = T()):val_(val){//cout << "ListNode(T val = T()):" << endl;}~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{mystd::shared_ptr<ListNode<int>> sp1(new ListNode<int>(2));mystd::shared_ptr<ListNode<int>> sp2(new ListNode<int>(2));sp1->next_ = sp2;sp2->pre_ = sp1;cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;return 0;
}
运行结果:
可以看到,我们的双向链表节点指针使用智能指针管理的话,就会出现智能指针资源无法正确释放的问题,但是如果我们
next_
、pre_
指针不使用shared_ptr
就不会出现这种问题。
这主要是因为引用计数导致的循环引用问题,我们画图来解释:
解决这种问题,就需要有一个智能指针,它不能参与资源的创建和销毁,也就是它不支持
RAII
,只有使用权,资源不属于它,它也不能管理。
weak_ptr
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f9d69b03bc1440a7a14a20f7bb94f58a.p
官方文档
模拟实现
template<class T>class weak_ptr{public:weak_ptr():ptr_(nullptr){}weak_ptr(const shared_ptr<T>& sp){ptr_ = (sp.get());}weak_ptr<T>& operator=(const shared_ptr<T>& sp){ptr_ = sp.get();return *this;}T* operator->(){return ptr_;}T& operator*(){return *ptr_;}private:T* ptr_;};
测试函数:
#include"SmartPtr.h"template<class T>
struct ListNode
{T val_;mystd::weak_ptr<ListNode<int>> next_;mystd::weak_ptr<ListNode<int>> pre_;ListNode(T val = T()):val_(val){cout << "ListNode(T val = T()):" << endl;}~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{mystd::shared_ptr<ListNode<int>> sp1(new ListNode<int>(2));mystd::shared_ptr<ListNode<int>> sp2(new ListNode<int>(2));sp1->next_ = sp2;sp2->pre_ = sp1;cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;return 0;
}
运行结果:
weak_ptr
不会增加引用计数的个数,它就不会造成循环引用问题。
delete和delete[]
有人会有这样的疑惑,我们的智能指针析构函数中都是
delete
,那万一要使用delete []
呢?
- 解决办法:在智能指针中再添加一个对象,调用这个对象去释放空间。
库里面也是这样做的,但是我们只是模拟实现,就没有实现它:
shared_ptr
智能指针的构造函数,就实现了函数模板重载的构造函数,我们可以传一个del
对象进去,这个对象可以是function
、函数指针、仿函数对象、lambda
表达式皆可。
其实不用我们实现,库里面也帮我们实现了:
下面一段代码,帮助你学会使用它(官方文档):
#include<iostream>using namespace std;int main()
{int n = 3;shared_ptr<int> sp1(new int[n]{1,2,3},default_delete<int[]>());int* ptr = sp1.get();for (int i = 0; i < n; ++i){cout << *ptr << endl;ptr++;}return 0;
}
运行结果:
C++11和boost中智能指针的关系
Boost库是一组为C++语言标准库提供扩展的C++程序库的总称,由Boost社区组织开发、维护。它是一个可移植、提供源代码的C++库,旨在通过提供丰富的模块和工具来解决常见的编程问题,从而增强和扩展标准C++库的功能。Boost库功能强大且使用广泛,但其中也有一些实验性质的内容,在实际开发中需要谨慎使用。