前言
大家好吖,欢迎来到 YY 滴C++系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁
主要内容含:
欢迎订阅 YY滴C++专栏!更多干货持续更新!以下是传送门!
目录
一.前言:智能指针出现解决内存泄漏问题
通俗语言介绍:
- 在抛异常捕获异常的过程中 ,有可能会影响 执行流 ;
- try+catch 程序的流程是:运行到try块中,如果有异常抛出,则转到catch块去处理。然后执行catch块 后面 的语句
int main()
{
try
{
pair<string, string>* p1 = new pair<string, string>;
f();//如果在f函数中抛出异常,会直接跳到catch块后面的语句,导致p1资源没有被释放,造成内存泄漏
delete p1;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
//捕获异常后跳转到的位置
return 0;
}
二.RAII
通俗语言介绍:
- 设置一个类 ,可以构造和析构,交给这个类的对象来 管理 指针。保证其一定会释放资源,不会内存泄漏。
简介:
- RAII(Resource Acquisition Is Initialization):即 资源获取时即初始化
- RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。- 在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
- 借此,我们实际上把管理一份资源的 责任 托管给了一个对象。
RALL的两大好处:
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
三.实现一个简单的智能指针
- 一个智能指针需要具备以下两种功能
- RAII管控资源释放 (资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源),即有构造有析构
- 像指针一样使用 ,即有 解引用功能 和 “->”功能
- 为了能够匹配更多类型,我们可以采用类模板的方式处理
template<class T>
class SmartPtr
{
public:
// RAII
// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
// 1、RAII管控资源释放
// 2、像指针一样
//RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{
}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _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 f()
{
// 21:15继续
SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
//div();
SmartPtr<pair<string, string>> sp2(new pair<string, string>);
SmartPtr<pair<string, string>> sp3(new pair<string, string>);
SmartPtr<string> sp4(new string("xxxxx"));
//像指针一样
cout << *sp4 << endl;
cout << sp1->first << endl;
cout << sp1->second << endl;
div();
//有了智能指针来管理,不需要我们手动释放资源
//delete p1;
//cout << "delete:" << p1 << endl;
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
四.简单的智能指针会遇到的问题:(浅)拷贝问题
- 当我们用三设计的智能指针进行拷贝时,我们会发现程序会出现一个问题
- 因为我们没有写默认的拷贝,所以类会生成一个默认的拷贝(浅)拷贝
- 浅拷贝只是把指针指向那块空间,因此析构时便会对同一块空间析构两次,且原来开的空间没有释放造成内存泄漏
函数运行结果如下所示:可以发现其析构了两次
int main()
{
SmartPtr<string> sp1(new string("xxxxx"));
SmartPtr<string> sp2(new string("yyyyy"));
sp1 =sp2;
return 0;
}
- 让我们接下来看看boost库和后来的C++11是如何解决这个问题的
五.库中的几种智能指针
※几种智能指针简述
1.智能指针演变史
- 最初的智能指针是C++98中的auto_ptr,但它有很明显的缺陷(下文会介绍)
- 主流智能指针(unique,shared,weak)一开始是由boost库中创建,后面被C++11沿用
- scpoe-ptr换成了auto_ptr,两者本质差不多
2.智能指针简述
- auto_ptr:(管理权直接转移,导致被拷贝对象悬空,访问就会出问题)(auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr)
- unique_ptr:(很粗暴,直接不允许拷贝,不需要拷贝的场景建议使用)
- share_ptr:(引用计数支持拷贝,需要拷贝的场景使用;要小心构成【循环引用】导致内存泄漏)
- weak_ptr:(专门解决share_ptr【循环引用】问题)
1)auto_ptr
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
//private:
int _a;
};
int main()
{
// C++98 一般实践中,很多公司明确规定不要用这个
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(new A(2));
// 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
// 隐患:导致被拷贝对象悬空,访问就会出问题
auto_ptr<A> ap3(ap1);
// 崩溃
//ap1->_a++;
ap3->_a++;
return 0;
}
2)unique_ptr
- unique_ptr的实现原理:简单粗暴的防拷贝
- 顾名思义:unique-唯一的
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
//private:
int _a;
};
int main()
{
// C++11 简单粗暴,不让拷贝
unique_ptr<A> up1(new A(1));
unique_ptr<A> up2(new A(2));
unique_ptr<A> up3(up1);//无法拷贝
return 0;
}
3)shared_ptr
引入:
- 我们如果在拷贝时直接让他指向资源,那么当程序结束时就会进行两次析构; 如果我们设置一个计数 , 控制 这个析构过程;问题就解决了
- C++11中开始提供更靠谱的并且 支持拷贝 的shared_ptr
- shared_ptr的原理:是通过 引用计数 的方式来实现多个shared_ptr对象之间 共享 资源
- shared_ptr在其内部, 给每个资源都维护了着一份计数 ,用来记录该份资源被几个对象共享;
- 在对象被销毁时(也就是析构函数调用),,就说明自己不使用该资源了,对象的引用计数减一;
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了;
- 一个基本shared_ptr的实现如下所示
template<class T>
class shared_ptr
{
public:
// RAII
// 像指针一样
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))//动态开辟,不可以静态,因为有可能有多个智能指针类管理不同的资源
{
}
~shared_ptr()
{
if (--(*_pcount) == 0) //析构前要判断引用计数
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// sp3(sp1)
shared_ptr(const shared_ptr<T>& sp) //拷贝时,让新的对象的指针成为这个智能指针类管理的指针
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
//解决自己拷贝自己时,引用计数会增加的问题——————>加入一个判断
// sp1 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
//涉及到后面与weak_ptr的搭配问题,不让他直接指向资源(防止引用计数增加)
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //动态的引用计数
};
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
//private:
int _a;
};
int main()
{
// C++11
shared_ptr<A> sp1(new A(1));
shared_ptr<A> sp3(sp1);//拷贝以后
sp1->_a++;
sp3->_a++;//程序不会崩溃
return 0;
}
- 引用计数的实现如下图所示:
六.解决shared_ptr的循环引用问题而生:weak_ptr
1)了解【循环引用】问题
- 我们在使用share_ptr时,有时会遇到以下这种场景
- 例如:双向链接两个节点
template<class T>
class shared_ptr
{
public:
// RAII
// 像指针一样
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))//动态开辟,不可以静态,因为有可能有多个智能指针类管理不同的资源
{
}
~shared_ptr()
{
if (--(*_pcount) == 0) //析构前要判断引用计数
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// sp3(sp1)
shared_ptr(const shared_ptr<T>& sp) //拷贝时,让新的对象的指针成为这个智能指针类管理的指针
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
//解决自己拷贝自己时,引用计数会增加的问题——————>加入一个判断
// sp1 = sp1
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
int use_count() const
{
return *_pcount;
}
//涉及到后面与weak_ptr的搭配问题,不让他直接指向资源(防止引用计数增加)
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //动态的引用计数
};
struct Node
{
A _val;
//不能如此使用,因为后面要让_next&_prev指向的都是智能指针的对象,sp1->_next = sp2;会出现类型不匹配的问题
//Node* _next;
//Node* _prev;
//要把节点指针也用智能指针包装一下才可以
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main()
{
// 循环引用
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << endl;//1
cout << sp2.use_count() << endl;//1
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;//1
cout << sp2.use_count() << endl;//1
//进入死循环
return 0;
}
- 我们要注意到,因为为了能
sp1->_next
能够实现,我们让Node的_next&_prev指针都由智能指针托管- 所以当
sp1->_next
或者sp1->_prev
时,本质上是一种拷贝,会导致资源的引用计数增加
- 程序执行以后,我们会发现析构时,引用计数仍然为1,变成死循环了,具体过程如下图所示
2)利用weak_ptr 解决【循环引用】问题
- 在shared_ptr中封装一层函数
use_count()
&get()
- 设置一个weak_ptr,在拷贝构造&赋值运算符重载环节引入上一步骤中封装的函数
- 原理:
- 在设置Node节点的指针时,用weak_ptr来处理, 使得它和share_ptr是同一类型 ;
- weak_ptr 不增加引用计数 ,可以访问资源,不参与资源释放的管理
template<class T>
class shared_ptr
{
//...略去的内容
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //动态的引用计数
};
struct Node
{
A _val;
//shared_ptr<Node> _next;
//shared_ptr<Node> _prev;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
// weak_ptr不参与资源释放的管理的RAII智能指针,专门用来解决shared_ptr循环引用问题
// weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
};
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;
};
int main()
{
// 循环引用
bit::shared_ptr<Node> sp1(new Node);
bit::shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
七.shared_ptr的【定制删除器】:管理不同方式产生的对象
引入:
- 我们在查看智能指针的文档时,会发现图中这种形式,这就是库里的定制删除器
- 如果不是new出来的对象如何通过智能指针管理呢?定制删除器就是用来解决这个问题
- 我们 只要往后面传一个可调用对象(删除方法)即可 : 仿函数(函数对象),lambda,函数指针都可以作为参数
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
//仿函数
shared_ptr<A> sp1(new A[10], DeleteArray<A>());
//lambda表达式
shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {
free(ptr); });
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
fclose(ptr);
});
//默认是delete
shared_ptr<A> sp4(new A);
return 0;
}