目录
产生原因
产生原因:抛异常等原因导致的内存泄漏
cpp
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
RAII思想
基本概念:智能指针(Smart Pointer)是C++中一种用于自动管理动态分配内存的对象,旨在防止内存泄漏和指针悬挂问题
核心理念:RAII,即一种利用对象生命周期来控制程序资源(如内存、网络连接等)的技术
工作原理:使用一份资源时,先用该资源构造一个智能指针,智能指针会保证在指向资源仍存在时始终有效(该资源的释放在智能指针的析构函数中)****,智能指针是由一个匿名对象构建得,为了能像指针一样使用,**智能指针类中还会重载*和->
cpp
#include<iostream>
using namespace std;
//智能指针类
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)//接收一份资源的地址
:_ptr(ptr)//_ptr指向一份资源
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;//智能指针对象释放时才释放_ptr指向的资源
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
//创建一个int* 类型的智能指针
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
*sp1 += 10;//像指针一样可以*
SmartPtr<pair<string, int>> sp3(new pair<string, int>);//智能指针sp3指向得是一个pair类型的匿名对象
sp3->first = "apple";
sp3->second = 1;//等价于sp3.operator->()->second = 1;
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
C++11的智能指针
基本概念:C++98时就已经提供了一个叫auto_ptr的智能指针,但使用该智能指针进行拷贝时,会出现管理权的转移,原对象的资源管理权交给了拷贝得到的新对象,这导致指向原对象的指针变为空指针,再次使用可能会报错;所以C++11在boost库的基础上引入了unique_ptr 和 shared_ptr等新的智能指针类(** C++11的智能指针均包含在<memory>头文件中,需要显示引用)
智能指针的拷贝与赋值
1、unique_ptr 类不支持拷贝构造和赋值重载***(通过只声明不定义实现,禁止了拷贝构造最好将赋值重载也禁掉,不禁掉则赋值重载是默认提供的会进行浅拷贝)***,适用于不需要拷贝的场景
2、shared_ptr 类有拷贝构造和赋值重载,并使用引用计数解决释放的问题***(不使用静态成员进行计数是因为静态成员记录的是所有与被管理资源同类型的资源被使用的次数,即同类型的两个不同资源new出来的两个智能指针不会使得计数器++,而是重置变为1,当然我们设计得拷贝构造仍会使计数器++,但是new时可能会导计数器重置)***
shared_ptr的拷贝构造
基本概念:智能指针shared_ptr在创建时,除了有指向资源的指针ptr,还有该资源独属得计数器指针int * _count,初始时*(_count ) = 1
cpp
// 构造函数
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
// 拷贝构造: sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
shared_ptr的赋值重置
基本概念:shared_ptr的赋值重载需要考虑无效赋值的情况
cpp
//赋值重置, sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp),防止自我赋值的情况,基础版
if (_ptr != sp._ptr)//避免管理统一资源的两个智能指针对象间的互相赋值,升级版
{
release();//先进行释放函数,防止内存泄漏
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
//释放函数
void release()
{
// 先进行计数器--,如果--后智能指针管理的资源的计数器变为0,就释放指向该资源的指针和计数器指针,然后再进行其它操作
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
shared_ptr的其它成员函数
基本概念:shared_ptr的析构函数并会直接将其管理的资源直接释放,还是要调用一个release函数进行--_count判断,即使是shared_ptr析构后,如果之前存在对该资源管理权的拷贝或者赋值,则用于记录该资源的计数器指针仍然不会被销毁,因为在拷贝或赋值时又有一个新的智能指针备份了该资源的信息(计数器指针指向的对象是new出来的int类型的对象除非主动释放,否则不会消失)**
cpp
//析构函数
~shared_ptr()
{
// 析构时,--计数,计数减到0,
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const//为别人提供自己的指针,且该函数中不能修改自己的指针
{
return _ptr;
}
shared_ptr的缺陷
基本概念:当两个**** 或多个对象通过 shared_ptr
相互引用,从而形成了一个循环。此时,即使所有外部 shared_ptr
都被销毁,由于循环引用中的 shared_ptr
仍然存在,引用计数永远不会降为零,导致这些对象无法被释放,造成内存泄漏
1、防止循环链表两个的结点因抛异常导致的无法释放,我们使用share_ptr管理这两个结点:
cpp
struct ListNode
{
int _val;
//④使得下面的n1->next = n2之类的操作不会因为双方类型不同导致无法互相赋值
//struct ListNode* _next;
//struct ListNode* _prev;
//|
//v
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
ListNode(int val = 0)
:_val(val)
{}
};
int main()
{
//①ListNode* n1 = new ListNode(10);
//ListNode* n2 = new ListNode(20);
//|
//v
std::shared_ptr<ListNode> n1 = ((new) ListNode(10);
std::shared_ptr<ListNode> n2 = ((new) ListNode(20);
//②中间可能出现抛异常
//|
//v
//不用担心抛异常了
n1->next = n2;
n2->prev = n1;
//④delete n1;
//delete n2;//此时不需要在这里delete了,在智能指针内部会delete
return 0;
}
2、但是此时又会出现下面三种情况:
两个结点互相用自己的智能指针管理对方时,管理两个结点的原始智能指针都析构后,记录两个结点的计数器均为1不会变为0,即两个结点均不会释放,此时出现内存泄漏问题,此时如果想要先释放右结点,那么就会出现以下的循环:
- 这就是shared_ptr在特殊场景下的缺陷......
weak_ptr
基本概念:为了解决shared_ptr在特殊场景下的缺陷,C++11还引入了weak_ptr,该智能指针不增加引用计数,不管理对象的生命周期
注意事项:
1、weak_ptr不支持RAII(下面是便于理解的简化实现)**
cpp
// 不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)//将传入的指针直接置空即可,不参与资源的管理
{}
weak_ptr(const shared_ptr<T>& sp)
{
_ptr = sp.get();//获取shared_ptr的指针
}
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;
};
2、weak_ptr也有use_count函数,用于检测此时与weak_ptr指向同一资源的shared_ptr的个数,如果shared_ptr为0(该资源已经被释放了)则weak_ptr变为野指针,应及时置空
3、weak_ptr的expired函数用于检查weak_ptr是否过期,过期返回真,未过期返回假,相比use_count会更方便
总结:使用智能指针不一定会避免内存泄漏(上述结点时使用的纯shared_ptr),正确的使用智能指针才能避免内存泄漏(完善后的shared_ptr + weak_ptr)**
定制删除器
基本概念: C++11并没有像boost库中的那样提供shared_array等用于管理和释放由[ ]创建的多个对象的智能指针,所以在使用shared_ptr管理和释放由[ ]开辟的多个对象时,会因为delete与new的方式不匹配而报错(shared_ptr的new和delete均为(),但如是[],释放时仍为()就可能导致出错)**
cpp
std::shared_ptr<ListNode> p1(new ListNode(10));//正常情况
std::shared_ptr<ListNode> p1(new ListNode[10]);//有[]的情况,无法正确释放
因此C++11的智能指针还提供了接受自定义删除器的构造方式:
cpp
template <class U, class D> shared_ptr (U* p, D del);
D del
:自定义删除器,可以是函数指针、函数对象、Lambda 表达式等
1、自定义删除器是函数对象
cpp
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
}
std::shared_ptr<ListNode> p2(new ListNode[10],DeleteArray<ListNode>());
2、自定义删除器是Lambda表达式
cpp
std::shared_ptr<ListNode> p3(fopen("Test.cpp","r"),[](FILE* ptr){fclose(ptr); });
简单实现
cpp
template<class T>
class shared_ptr
{
public:
template<class D>
shared_ptr(T* ptr, D del)//接收自定义删除器的构造函数
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)//新定义一个成员用于存放删除器
{}
//删除函数
void release()
{
// 说明最后一个管理对象析构了,可以释放资源了
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//delete _ptr;
_del(_ptr);//将执行资源的指针也给删除器一份,从而使删除器开始运行
delete _pcount;
}
}
private:
D _val;//shared_ptr的官方实现的模板中没有第二个模板参数,所以这种写法是错误的
}
cpp
//利用function将删除器的类型进行封装
//删除器的返回类型肯定是void,接收的参数类型肯定是T*
function<void(T*)> _del;
//为了防止使用无自定义删除器的构造函数时,没有del的传入导致的_del为空的情况,所以我们要提供一个默认的删除方式:
function<void(T*)> _del = [](T* ptr) {delete ptr; };
~over~