前言
在本篇文章中我们将会讲解内存泄漏以及解决一下c++异常处理的一些复杂场景,从而引出智能指针。
一、内存泄漏
内存泄漏:因为疏忽或者错误导致不再使用的内存没有被释放,并不是指物理内存上的丢失,而是程序分配某段内存后,因为设计错误,失去了对该段内存的控制,从而导致内存泄漏
内存泄露的危害
内存泄露会导致相应的部分不能够能使用,在长期运行的程序中,比如操作系统,服务器等,就会导致慢慢变卡的情况,严重的话甚至导致进程退出。就比如玩着王者突然卡死的情况,这是很难受的!!!
我们从代码的角度看一下
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
内存泄漏的分类
🌟 🌟 堆内存泄露
我们在使用malloc/realloc/calloc/new等申请堆上的空间时,如果没有对应的free/delete进行释放,对应的内存没有被释放,这部分空间将无法被使用,导致内存泄漏
🌟 🌟 系统资源泄露
程序使用系统分配的资源,比如套接字,文件描述符,管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可能导致系统性能减少,系统执行不稳定。
如何避免内存泄露
🌟 🌟 事先预防
我们在使用相应的函数时,一定要注意匹配使用,避免内存泄漏。良好的编码规范。如果异常释放不了,配合智能指针释放资源。
有的公司内部规范使用内部实现的私有内存管理库,这些库自带内存泄漏检测工具。
🌟 🌟 事后处理
采用一些内存泄露的工具检测。
二、智能指针
1.简单介绍
我们介绍智能指针之前,看一下这种场景如果我们采用异常的处理该如何解决??
int *p1=new int[ 10 ];
int *p2=new int[ 10 ];
int *p3=new int[ 10 ];
如果p1,p2,p3都申请到内存,正常释放,我们后续的代码应该是这样的
delete [ ]p1;
delete [ ]p2;
delete [ ]p3;
new会进行异常检查
那如果p1或者p2或者p3没有申请到内存,按照我们处理异常的方式,按照下面写法
void fun()
{
int* p1;
int* p2;
int* p3;
int* p1 = new int[10];
try
{
p2 = new int[10];
try
{
p3 = new int[10];
}
catch (...)
{
delete[]p1;
delete[]p2;
throw;
}
}
catch (...)
{
delete[]p1;
throw;
}
delete[]p1;
delete[]p2;
delete[]p3;
}
总看起来很别扭,我们可以通过一种更好的方式解决这个问题—智能指针
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术.已称为资源获取即初始化
对象构造时获取资源,在对应的生命周期内对资源进行控制和管理,对象析构时释放空间。
把一份资源的管理交给了一个对象
有两个好处
🌟 🌟 不用显式释放资源
🌟 🌟 所需的资源在其生命周期始终有效
实际上智能指针就是通过一个类来实现的,通过具有指针的功能。
namespace peng
{
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
cout << "SmartPtr(T* ptr == nullptr)" << endl;
}
~SmartPtr()
{
cout << " ~SmartPtr()" << endl;
delete _ptr;
}
//解引用
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
我们运行看一下
正常析构,释放资源。为什莫呢???
因为如果出现了异常,程序会跳到catch的地方,就出了作用域,出了作用域,生命周期结束,自动调用析构函数,资源就得到释放。
同时具有指针的功能。我们看一下
peng::SmartPtrp1(new int(10));这是一个单参数的隐式类型转换。
peng::SmartPtrp2=new int(10);正确赋值。
这里的 p4->_a 原型是p4.operator->() ->。本质是两个箭头,但是C++委员会做了特殊处理,只需要一个->就可以。
2.auto_ptr
我们还有求智能指针具有拷贝和赋值的功能,那应该如何实现呢???
我们采用深拷贝呢,还是浅拷贝???
比如list/vector等,利用资源存储管理数据,资源是自己的,拷贝时,每个对象1各自一份资源,各自管理自己的,所以深拷贝。
智能指针,迭代器,资源不是自己的马,只是代为持有,方便访问修改数据,拷贝时期望指向同一块资源
C++98研究出了auto_ptr的方法。
管理权转移
我们通过代码看一下
代码走到71行,p1获取资源进行初始化,72行p2由p1拷贝而来。
我们可以发现拷贝之后,本来p1的资源现在被转移到了p2中,同时p1被清空,这也就意味着我们之后不能再对p1的值进行操作,p1悬空。很多公司都不使用这个,因为这个很坑。
我们来模拟实现一下
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
}
~auto_ptr()
{
delete _ptr;
}
//解引用
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
//拷贝构造
//p2(p1)
//不能加const
auto_ptr( auto_ptr<T>&sp)
{
_ptr = sp._ptr;
sp._ptr = nullptr;
}
//赋值
//p2 = p1;
auto_ptr<T>& operator=(auto_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
//释放p2
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
因为有缺陷,标准委员会建议:什么情况下都不要使用 auto_ptr
2.unique_ptr
unique_ptr是C++11的东西,在c++98到c++11期间,还产生了boost,C++11中unique是在boost中scoped_ptr改装的。
以及后面讲述的shared_ptr,weak_ptr都是在boost的基础上改装的。
我们看一下unique_ptr是如何操作的
我们可以发现unique_ptr是禁止拷贝的,这仅仅适用于不需要拷贝的场景。
我们来模拟实现一下
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
delete _ptr;
}
//解引用
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
//拷贝构造
unique_ptr(unique_ptr<T>& sp) = delete;
//赋值
//p2 = p1;
unique_ptr<T>& operator=(unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
3.shared_ptr
shared_ptr支持拷贝和赋值,但是这是通过引用计数的方式实现的。
通过引用计数的方式实现多个shared_ptr对象之间资源共享
首先简单看一下
我们想一下这个模拟实现需要怎末弄呢??
多个对象共用一份资源,我们需要用到引用计数的方式,每个资源都维护一份引用计数,当引入一个共用该资源的对象时候,引用计数++,当其中一个对象析构之后,引用计数- -,如果引用计数减到0了,我们就释放这个资源。
这个引用计数应该如何设计呢???
采用静态成员遍历是否可以呢!!!静态成员适用于所有对象,我们的要求是一份资源配一个引用计数。
采用指针充当引用计数
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{
}
//引用计数减少到0,才释放资源
~shared_ptr()
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
}
//解引用
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
//拷贝构造
//p2(p1);
shared_ptr(shared_ptr<T>& sp)
{
//拷贝
_ptr = sp._ptr;
_count = sp._count;
//加加引用计数
++(*_count);
}
//赋值
//p2 = p1;
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
//判断是否自己给自己赋值,判断_ptr
if (_ptr != sp._ptr)
{
if (--(*_count) == 0)
{
delete _count;
delete _ptr;
}
拷贝
_ptr = sp._ptr;
_count = sp._count;
加加引用计数
++(*_count);
//不行
//(*_count)++;
}
return *this;
}
private:
T* _ptr=nullptr;
int* _count;
};
4.weak_ptr
我们使用shared_ptr可以解决拷贝和赋值的问题,但是它有一个小缺陷
看一下下面这个例子
struct ListNode
{
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode());
shared_ptr<ListNode> n2(new ListNode());
n1->_next = n2;
n2->_prev = n1;
return 0;
}
我们运行一下,看一下资源释放了没有
如果我们屏蔽掉 n1->_next = n2;n2->_prev = n1;二者中一个代码资源就可以正确释放
我们先来看看屏蔽一个为什莫这可以正确释放,画图理解一下
前两行代码,构造时获取资源
n2->_prev=n1;
两个资源析构,n2率先析构,n1析构,n2引用计数减为0,n1引用计数减为1。
因为n2的引用计数减为0,ListNode进行释放,_prev也要释放,_prev指向n1,n1要进行析构,n1的引用计数减为0,n1的ListNode顺利释放,同时n2的ListNode也顺利释放。不会造成内存泄漏。
n1->_next = n2;
n2->_prev = n1;
n1和n2的引用计数都变为2
n2析构,n1析构,两个的引用计数都减为1
n1要释放资源,n1靠n2的_prev管理着,所以首先n2的_prev要先进行释放,n1才可以释放。
n2的_prev什么时候释放??n2进行析构的时候,才释放。
n2的资源释放又要借助n1,因为n2是被n1的 _next管理着。
那么n1的 _next什么时候释放呢??n1释放资源的时候。
我们最终发现,绕了一圈最终又绕回去了,所以两个资源都得不到释放。
解决
上面的错误我们称为循环引用,我们这时就借助wear_ptr解决,wear_ptr里面不存储计数,不参与计数管理。
那他的底层是如何实现的呢???
我们在实现构造函数时要注意,并不用指针直接构造
weak_ptr不能单独管理资源,必须配合shared_ptr一块使用,解决shared_ptr中存在的 循环引用问题。不具备RALII的特性。
weak_ptr与shread_ptr的实现方式类似,都是通过引用计数的方式实现的,但底层实现不同,
weak_ptr不增加引用计数,不参与资源的申请和释放,不需要写析构函数
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{
}
//解引用
T& operator*()
{
return *_ptr;
}
//->
T* operator->()
{
return _ptr;
}
//拷贝构造
weak_ptr(shared_ptr<T>& sp)
{
//私有
_ptr = sp.get();
}
//赋值
//p2 = p1;
weak_ptr<T>& operator=(shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr;
};
C++11中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针
weak_ptr的唯一作用就是解决shared_ptr中存在的循环引用问题
总结
以上就是今天要讲的内容,本文仅仅详细介绍了C++智能指针的内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘