✨前言✨
📘 博客主页:to Keep博客主页
🙆欢迎关注,👍点赞,📝留言评论
⏳首发时间:2024年6月4日
📨 博主码云地址:博主码云地址
📕参考书籍:《C++ Primer》《C++编程规范》
📢编程练习:牛客网+力扣网
**由于博主目前也是处于一个学习的状态,如有讲的不对的地方,请一定联系我予以改正!!!
C++中智能指针
- 1 智能指针的引入
- 2 内存泄露的概念
- 3 智能指针的概念
- 3.1 RALL特性(智能指针核心思想)
- 3.2 重载operator*与重载operator->
- 3.3 auto_ptr(已弃用了解即可)
- 4 常见的三种智能指针
- 4.1 unique_ptr
- 4.2 shared_ptr
- 4.3 循环引用的问题
- 4.4 weak_ptr
- 5 删除器(了解)
本文实现的全部代码
1 智能指针的引入
所谓的智能指针就是帮助我们解决内存中的管理问题,结合所学过的异常,我们可以来看一下这样的一个场景:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
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;
}
我们来想一想这样的三个问题
1、如果p1这里new 抛异常会如何?
2、如果p2这里new 抛异常会如何?
3、如果div调用这里又会抛异常会如何?
其实p1这里抛异常不会有任何的影响,但是p2要是抛出了异常,就必须被捕获,所以就会跳出Func函数,就会导致用new申请的p1没有被释放,就会造成内存泄露!同理如果是div调用抛出的异常,那么就会导致p1与p2都没有释放,会导致内存泄漏!所以为了解决这样的一个问题,C++中就引入了智能指针的概念!
2 内存泄露的概念
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
简单来说就是我们申请的资源没有得到及时的释放,而失去了对这块资源的控制,从而造成了内存泄漏!长期的内存泄露就会导致服务器相应很慢!
3 智能指针的概念
在简单了解完了以上两个概念,下面我们在来介绍一下什么是智能指针!
3.1 RALL特性(智能指针核心思想)
RAII(英文全称:Resource Acquisition Is Initialization(资源获取初始化))是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
看到这里,也许你还是很懵逼,这是啥?没关系,接下来,我们就以具体的例子来理解一下这个思想!在智能指针引入的代码基础上,我们来写一个智能指针的类!
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "delete" << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
对于Func中的资源申请可以改为如下:
void Func()
{
SmartPtr<int> s1(new int);
SmartPtr<int> s2(new int);
/*int* p1 = new int;
int* p2 = new int;*/
cout << div() << endl;
/*delete p1;
delete p2;*/
}
此时运行结果如下:
我们可以发现,即使div会抛出异常,此时申请的资源也可以正常的释放了!也就是说利用这样的一个SmartPtr的类,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样子做有两种好处:
不需要显示的去释放资源
采用这种方式,对象所需的资源在其生命期内始终保持有效。
3.2 重载operator*与重载operator->
要想上述的类像指针一样就需要重载以上两个运算符,拥有像指针一样的行为
T operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
综上所述,智能指针必须满足以上两个要求,拥有RALL特性和重载了operator*与operator->两个运算符!
3.3 auto_ptr(已弃用了解即可)
其实在C++98中就提出了智能指针,auto_ptr就是当时提出来的,但是它是一个大坑,auto_ptr的实质就是将资源的管理权限进行转让,为什么这么说呢?我们来看一下这样的一段代码,库里面的智能指针包含在memory这个头文件中:
我们发现如果使用拷贝构造,此时就会把a1中资源的管理转交给a2,那么此时如果我们在对a1进行解引用等操作就是非法行为,所以库里面的这个智能指针要慎用(现在已经被禁用了),现在一般都是使用C++11中提出的那三种常见的智能指针!如果不像auto_ptr这样处理拷贝构造,我们能否直接进行浅拷贝呢?答案其实也是不可以的,因为这样子做就会导致一个对象指向的资源被析构两次(下图所示)!那么如何解决这种拷贝的问题呢?我们可以接着往下看!
自行模拟实现auto_ptr的简易版本,代码如下:
//模拟实现auto_ptr
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造 实现资源管理权的转移
//a1(a2)
auto_ptr(const auto_ptr<T>& p)
{
_ptr = p._ptr;
p._ptr = nullptr;
}
//赋值拷贝
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
//自己没给自己赋值情况下才进行处理
if (this != &p) {
_ptr = p._ptr;
p._ptr = nullptr;
return *this;
}
}
T operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete" << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
4 常见的三种智能指针
4.1 unique_ptr
unique_ptr就采用禁止拷贝构造以及赋值拷贝来解决以上所说的拷贝问题!具体的实现代码如下所示:
//模拟实现unique_ptr
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造与赋值拷贝构造禁止使用!
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& p) = delete;
T operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete" << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
4.2 shared_ptr
shared_ptr就采用了引用计数原理来解决拷贝问题,示意图如下所示:
也就是说,智能指针对象中有两个指针,一个是指向资源的指针,另一个就是指向计数的一个指针!注意:别看我示意图画的都是指向同一个计数,就认为我们使用的就是静态的成员变量来计数的!我们应该采用每一个对象中都会有一个初始计数,值为1,然后通过一系列的操作,改变指针的指向,从而使得指向同样的一块计数!
具体实现代码如下所示:
//模拟实现shared_ptr
//利用引用计数
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr)
:_ptr(ptr),
_Count(new int(1))
{}
//拷贝构造与赋值拷贝构造禁止使用!
//s1(s2)
shared_ptr(const shared_ptr<T>& p)
{
_ptr = p._ptr;
_Count = p._Count;
(*_Count)++;
}
//s1 = s2
shared_ptr<T>& operator=(shared_ptr<T>& p)
{
//只有不是指向同一块资源的才可以进行赋值操作,对计数进行改变
if (_ptr != p._ptr)
{
//如果被赋值对象仅有它自己指向一块空间
//那么此时就必须先对资源进行清理
//在进行赋值
destory();
_ptr = p._ptr;
_Count = p._Count;
(*_Count)++;
}
}
//计数,引用计数的个数
int use_Count()
{
return *_Count;
}
T operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~shared_ptr()
{
destory();
}
private:
void destory()
{
if (--(*_Count)==0)
{
cout << "delete" << endl;
delete _ptr;
delete _Count;
}
}
T* _ptr;
int* _Count;
};
4.3 循环引用的问题
shared_ptr在某种特定的情况下就会出现循环引用的问题!我们结合以下场景来理解这样的一个问题:
//循环引用例子
struct ListNode {
int _val;
test::shared_ptr<ListNode> _next;
test::shared_ptr<ListNode> _prev;
ListNode(int val)
:_val(val),
_next(nullptr),
_prev(nullptr)
{}
};
int main()
{
test::shared_ptr<ListNode> a1(new ListNode(2));
test::shared_ptr<ListNode> a2(new ListNode(3));
a1->_next = a2;
a2->_prev = a1;
return 0;
}
先来说明一下,为什么这里的节点是使用智能指针,因为如果是节点类型,我们就不能将节点连接起来了,a2是智能指针是属于自定义类型,而我们的a1->_next就是一个指针,属于内置类型,所以会出错,不能对节点进行连接了!
其实当只有a1->_next = a2与a2->_prev = a1只有一个的时候不会出现任何问题的,当时当两句同时出现的时候就有问题了!为什么会这么说呢?下面我用图解的方式来说明:
以上就是实现了那两句代码之后的示意图,shared_ptr中有两个指针,一个是指向计数的,一个是指向对应资源的指针!下图就是造成循环引用的示意图了!
4.4 weak_ptr
weak_ptr就可以很好的解决循环引用的问题,其原理就是不对计数处理,只要处理指针的指向,也就是说weak_ptr并不是参与资源管理的智能指针,不具备RALL特性!模拟实现代码如下所示:
//模拟实现weak_ptr
template<class T>
class weak_ptr {
public:
weak_ptr(T* ptr)
:_ptr(ptr)
{}
weak_ptr(const shared_ptr<T>& p)
{
_ptr =p.getPtr();
}
//s1 = s2
weak_ptr<T>& operator=(shared_ptr<T>& p)
{
_ptr = p.getPtr();
return *this;
}
T operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
//解决循环引用
struct ListNode {
int _val;
test::weak_ptr<ListNode> _next;
test::weak_ptr<ListNode> _prev;
ListNode(int val)
:_val(val),
_next(nullptr),
_prev(nullptr)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
test::shared_ptr<ListNode> a1(new ListNode(2));
test::shared_ptr<ListNode> a2(new ListNode(3));
a1->_next = a2;
a2->_prev = a1;
return 0;
}
我们可以借助析构函数,来看一下是否进行了释放,包括上一小节介绍的shared_ptr也可以利用析构来查看节点是否进行了释放!
5 删除器(了解)
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题!这里我就简单的介绍一下就行了,就是利用包装器原理,用户在构造的时候,选择利用函数指针,仿函数,lambda表达式构造对象进行删除就可以了!本文的全部实现代码已经上传码云了!