【C++11(三)】智能指针详解--RAII思想&循环引用问题
💓博主CSDN主页:杭电码农-NEO💓
?
?专栏分类:C++从入门到精通?
?
🚚代码仓库:NEO的学习日记🚚
?
🌹关注我🫵带你学习C++
? 🔝🔝
C++11
1. 前言
相信学C++的同学或多或少的听说过
智能指针这个词,博主刚听见这个词时
,觉得它应该很复杂,并且很高大上,但不
管是多牛的东西,都是人写出来的,是可
学习的!不要怀着害怕的心理来学习它
本章重点:
本篇文章着重讲解智能指针的发展历史
中出现过的auto_ptr,unique_ptr以及主
角shared_ptr.并且会介绍什么是RAII思想
以及为什么要有智能指针这一话题,最后
会给大家分析shared_ptr的循环引用问题
以及定制删除器的基本概念
2. 为什么要有智能指针?
在写代码时,我们经常在堆上申请空间
但是偶尔会忘记释放空间,会造成内存
泄漏问题,当然,这不是最重要的,在某些
场景下即使你释放了也会有问题:
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;
}
在上面代码的这种场景中,不管是使用
new还是调用div函数都有抛异常的风险
并且程序一旦抛异常就会直接跳到catch
处,所以上面的代码一旦抛异常就代表着
delete p1和p2并不会执行,也就会出现
内存泄漏的问题!这个问题不使用智能
指针是很难解决的!!!
3. RAII思想以及智能指针的设计
RAII思想
RAII思想是一种 利用对象生命周期来控制程序资源
(如内存、文件句柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
这种做法有两种好处:
- 不需要显式地释放资源
- 对象所需的资源在其生命期内始终有效
智能指针的基本设计
现在我们来写一个类,构造函数的
时候创造资源,析构函数的时候释放
资源,当对象出了作用域会自动调用析构!
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr!=nullptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
现在我们来使用一下它:
SmartPtr<int> sp1(new int(10));
*sp = 20;
当然,重载了->是给自定义类型用的
4. C++智能指针的发展历史
首先,我们要清楚智能指针的一个大坑
那就是当一个指针赋值给另外一个指针
时,我们需要的是浅拷贝,因为我们就是想
让两个指针指向同一块空间,但是指向了
同一块空间就会有析构函数调用两次的风险
由于这一个大坑,智能指针进行了很多次迭代
- 在C++98的时候就已经在库中实现
了智能指针了,它就是auto_ptr
既然智能指针是随着历史不断发展的
就证明它前面的版本写的不咋滴[doge]
事实也是如此,auto_ptr是这样实现的,
既然有析构两次的风险,那么当我把A
指针赋值给B指针后,A指针就销毁不能用
了,对于不了解auto_ptr的人来说这无疑是
一个巨大的风险!
auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2(ap1);
//此时ap1已经失效了!
- 有了这一大坑后,C++11推出了全新
的智能指针:unique_ptr
unique_ptr的做法比auto_ptr还绝
智能指针不是拷贝有问题吗?那么
unique_ptr就禁用了拷贝和赋值,
很显然这也是一个坑,但是在实际
场景下,unique_ptr至少还能被用到
但auto_ptr是很多公司明令禁止使用的!
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(up1);//这里会直接报错
- 经过两次失败的智能指针后,C++11
还推出了今天的主角:shared_ptr
shared_ptr可堪称完美的智能指针
也是实际中使用的最多的智能指针
它采用的是引用计数的思想,当指向
这份空间的计数是1时才析构,大于1
时就将计数减一,非常的优雅!
由于智能指针在面试时让手撕的概率很大
所以我们会模拟实现它
5. shared_ptr模拟实现
我们使用引用计数的方式来实现
shared_ptr,也就是在原先代码的
基础上增加一个int*成员变量来保存
还有几个指针指向当前空间!
template<class T>
class Smart_Ptr //实现的C++11的shared_ptr版本
{
public:
Smart_Ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
~Smart_Ptr()
{
Release();
}
Smart_Ptr(const Smart_Ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
Addcount();
}
Smart_Ptr<T>& operator=(const Smart_Ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
Addcount();
}
return *this;
}
void Release()
{
if (--(*_pcount) == 0)//销毁最后一个变量时才释放资源
{
delete _ptr;
delete _pcount;
delete _pmtx;
}
}
void Addcount()
{
(*_pcount)++;
}
void Subcount()
{
Release();
private:
T* _ptr;
int* _pcount;
};
我们将计数++贺计数- -特意的提出来
这是因为很多场景下都需要这两个函数.
当计数不为1时就- -计数,当计数为一才
释放资源,并且这样写的好处是相同类型
的指针对象即使指向不同的空间也不会
出错,相反,使用static定义成员指针变量
就会出现上面的这种问题!
6. shared_ptr的循环引用问题
请看下面的代码运行会崩溃:
struct ListNode
{
int _data;
shared_ptr<ListNode> prev;
shared_ptr<ListNode> next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->next = node2;
node2->prev = node1;
return 0;
}
为啥会崩溃?下面我用画图加文字
的方式帮大家分析一下此问题:
现在来进一步分析:当main函数调用完,
node2会先析构,但是此时引用计数是2
所以不会释放空间而是将计数变为1.
然后node1再析构,同上,它的引用计数
也减为一,但是这两份空间并不会释放,
因为要node2的prev释放后,node1的空间
才会释放,那node2的prev什么时候释放?
答案是node2这份空间释放了才会释放
prev,那么node2这份空间什么时候释放?
答案是node1的next释放了它才释放,这
就形成了一个死循环,我等你释放了我才
能释放,对方也在等我释放了对方才能
释放,这就是"循环引用问题"
最好的解决方案就是在使用智能指针
的时候跳过这个坑,不用将智能指针和
这种场景一起使用!!!
7. 定制删除器
使用智能指针时可能会遇见下面的问题:
shared_ptr<int> sp1(new int[10]);
当变量出作用域销毁时即报错
因为new []对应的是delete [].
然而库中写法并不能识别有没有[]
还有一些问题:
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"));
此时智能指针管理的对象并不是堆上
开辟的空间,delete完全没法用,此时需
要使用fclose,所以定制删除器非常重要
在构造函数的地方可以传入一个定制
删除器,也就是一个函数对象,此函数
中有对应的删除方法,请看下面的代码:
shared_ptr<int> sp2(new int[10], [](int* ptr) {delete[] ptr; });
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
注:定制删除器属于了解的部分
8. 总结以及拓展
智能指针在面试中是常客,经常会被
问到发展历史和shared_ptr的手撕,
学到这里后,C++的所有重要的知识
差不多已经完结了,后面文章更新会慢一点
拓展:weak_ptr的拓展阅读
既然weak_ptr可以解决shared_ptr的
循环引用问题,那么什么是weak_ptr?
有兴趣的同学可以阅读下面这篇文章:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!