目录
🚩何谓智能指针
指针我们都不陌生,我们在存放或指向一个地址空间时都需用到指针。而今天我们要谈的智能指针主要将问题集中到了空间(内存)的安全使用的方面。我们在堆上申请空间后,被告知一定要进行空间的释放,否则会造成内存泄漏的问题。长期的内存泄漏会造成进程卡死的情况,这要是在一款投入使用的软件上发生了,那造成的损失就不是个人可以承担的了(亲,可以拍拍屁股走人了哈🤔)。
那么有没有可能有一种比较智能的指针,在程序结束的时候就自动将指针指向的空间给释放掉,用户根本不用操心各种情况导致的内存泄漏了?基于这样的诉求,智能指针的概念就被提出来了。
🚩哪些场景需要智能指针
大的来讲,就是防止内存泄漏呗。引起内存泄露的原因倒是有2种:
1.用户申请空间之后忘了去释放。
2.用户记得释放空间,但是由于进行释放操作之前的异常抛出,程序没有执行释放操作语句而是跳到了捕捉异常的代码块,造成空间泄漏。
其中第一种情况倒是还好,用户忘了这种因素可以通过用户自己再添加对应的空间释放语句来修补。而第二种情况就比较棘手了,因为异常地抛出用户没法控制,而且处理起来也比较麻烦(有些情况根本处理不了),因此只能指望智能指针喽~
🚩智能指针的指导思想--RAII
对于RAII,百度百科是这样解释的:
RAII [1] (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
不得不赞叹这种解决思路:所有申请过的资源统一由某一对象来管理,外部不再关心资源的释放问题,只要生命期结束(比如main函数结束),调用析构函数的时候将指针指向的空间统一释放。而在使用的时候,通过对象使用就行了。方便、快捷、安全、智能!
🚩标准库中的多种智能指针
在开始细致讲解智能指针之前,博主这里只强调一点:
智能指针像指针而不是指针!智能指针像指针而不是指针!智能指针像指针而不是指针!
重要的事情说三遍!这里其实是通过对象来实现指针的各种功能。下面我们开始探索之旅~
在C++98的标准中,更新了智能指针auto_ptr,这是第一代智能指针。当然,这也是有缺陷的智能指针,等下细谈。而在C++11的标准中,陆续出现了unique_ptr(靠谱),shared_ptr(更靠谱),weak_ptr(补充shared_ptr)。这些智能指针都能实现资源的管理,只不过比的就是谁更像指针罢了,下面我们细谈。
auto_ptr
//Smart_ptr.h
#pragma once
namespace jia //博主个人命名空间:jia
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp) //拷贝构造,转移空间
:_ptr(sp._ptr)
{
sp._ptr=nullptr;
}
~auto_ptr() //析构时释放空间
{
if (_ptr)
{
cout << "delete " << _ptr << endl; //表示调用析构函数
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() //模拟指针解引用
{
return *_ptr;
}
T* operator->() //模拟指针->
{
return _ptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& sp) //赋值,空间转移
{
_ptr = sp._ptr;
sp._ptr = nullptr;
return *this;
}
T* get() //获取原生指针
{
return _ptr;
}
private:
T* _ptr; //原生指针
};
}
#include<iostream>
using namespace std;
#include"Smart_ptr.h"
int main()
{
jia::auto_ptr<int> p1(new int);
jia::auto_ptr<int> p2(new int);
return 0;
}
可以看出在调用析构函数的时候,确实实现了资源的释放。但这里有一个巨大的坑等着初学者往里跳:拷贝构造把资源的管理权限交给新的对象来管理了,这就造成了被拷贝的指针成空指针被悬空了,一旦去解引用---啊哈,程序崩溃喽~ 对空指针是不能进行解引用操作滴!
比如这样:
#include<iostream>
using namespace std;
#include"Smart_ptr.h"
int main()
{
jia::auto_ptr<int> p1(new int);
jia::auto_ptr<int> p2(p1);
*p1=10;
*p2=20;
cout << *p1 <<" " << *p2 << endl;
return 0;
}
程序异常退出了!
那么这有没有解决方案方案呢?肯定是有的,只不过已经不叫auto_ptr了,改叫unique_ptr了。
unique_ptr
模板参数T我们很好理解,就是指针的类型。但是后面的缺省参数D是个什么东西呢?
这里我们得清楚一件事,我们要通过对象释放空间,而由空间既可以是malloc出来的,也可以是new出来的,更可以是fopen(打开文件)来的,不同的空间释放的方式不同,因此还得额外传一个参数来解决这个问题,而仿函数为了解决这个问题就自然而然地被使用了。由此又产生了一个新的名词--定制删除器
话说回来,unique_ptr是通过禁止使用拷贝构造函数和=重载,感觉是不是有点掩耳盗铃的感觉?虽说不太像指针了,但也确实在避免拷贝问题的情况下,完成了空间的管理,只不过不太方便了而已。
#pragma once
namespace jia
{
template<class T>
struct SingleDelete //new单个元素删除
{
void operator()(T* ptr)
{
if (ptr)
{
cout << "SingleDelete " << ptr << endl;
delete ptr;
}
}
};
template<class T>
struct ArrayDelete //new一维数组删除
{
void operator()(T* ptr)
{
if (ptr)
{
cout << "ArrayDelete " << ptr << endl;
delete[] ptr;
}
}
};
template<class T>
struct Free //malloc删除
{
void operator()(T* ptr)
{
if (ptr)
{
cout << "Free " << ptr << endl;
free(ptr);
}
}
};
template<class T> //文件指针关闭
struct FileClose
{
void operator()(T* ptr)
{
if (ptr)
{
cout << "FileClose " << ptr << endl;
fclose(ptr);
}
}
};
template<class T, class D=SingleDelete<T>>//默认单个删除
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
D del;
del(_ptr); //利用仿函数删除
}
}
unique_ptr(unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& sp) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
};
}
jia::unique_ptr<int> p1(new int);
jia::unique_ptr<int,jia::ArrayDelete<int>> p2(new int[10]);
可以看出与预想中的一样,可以说是受限制的指针使用了。而为了更像指针,推出了shared_ptr智能指针,这是接看起来最近指针的版本了。
shared_ptr
不难发现这里的参数模板已经只剩T了,难道shared_ptr不支持定制删除器了吗?当然不是!
这是shared_ptr与unique_ptr不同的地方之一,实际上删除器仿函数的传参是在构造函数那实现的,但具体怎么在析构函数也能用到构造函数的模板参数,这个博主也不是特别清楚,有大佬知道的话可以在评论区留言哦。所以博主这里就先以delete为默认删除方式写一个简易版的shared_ptr给大伙理解一下原理就成。
与前面的智能指针都不同的是,shared_ptr支持了拷贝构造和赋值重载,走的是计数析构的思路。只要有拷贝构造操作,就认为多了一个指针来管理被拷贝的物理空间,计数加一,这里拷贝的对象的计数指针变量也要跟着拷贝。想要析构一个对象,就必须检查计数是否为1(是否只有一个对象管理对应的物理空间),是1就可以进行空间释放操作了。值得注意的是:赋值的时候得先检查一下对象原先指向的物理空间是不是只有它自己管理了,毕竟被赋值之后,这最后一个管理者也会消失,那么该空间也得被释放。
呼~,说了那么多,还是代码最为清晰,上代码!
#pragma once
namespace jia
{
template<class T>
class shared_ptr
{
public:
void Release() //析构会调用此函数,使计数减一并判断是否要释放空间
{
if (--(*_pCount) == 0 && _ptr)
{
cout << "delete " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
shared_ptr(T* ptr)
:_ptr(ptr)
,_pCount(new int(1)) //最初构造对象时只有一个管理者刚刚创建,计数为1
{}
~shared_ptr()
{
Release();
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount) //计数器一定要同步拷贝
{
++(*_pCount);
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release(); //管理其他空间之前要看看是否要释放之前管理的空间
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() //管理者的个数
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount; //计数使用,使用指针使得多个对象拷贝之后仍拥有统一的计数器
};
}
#include<iostream>
using namespace std;
#include"Smart_ptr.h"
int main()
{
jia::shared_ptr<int> p1(new int);
jia::shared_ptr<int> p2(p1);
jia::shared_ptr<int> p3(new int);
p2 = p3;
return 0;
}
可以看出无论是拷贝还是赋值都对空间的释放没有影响了。
weak_ptr (解决循环引用)
既然shared_ptr已经够好了,为什么还要再引入一个智能指针weak_ptr呢?原因在于在某些特殊情况下使用shared_ptr会导致一个死循环的析构等待--循环引用
来看下面的代码:
#include<iostream>
using namespace std;
#include"Smart_ptr.h"
template<class T>
struct ListNode
{
ListNode()
:_next(nullptr)
,_prev(nullptr)
,_data(T())
{}
jia::shared_ptr<ListNode<T>> _next; //注意这里的前后节点的类型都是智能指针
jia::shared_ptr<ListNode<T>> _prev;
T _data;
};
int main()
{
jia::shared_ptr<ListNode<int>> p1(new ListNode<int>);
jia::shared_ptr<ListNode<int>> p2(new ListNode<int>);
p1->_next = p2;
p2->_prev = p1;
return 0;
}
该输出两次的delete语句没了,也就代表根本没有完成空间的释放!
下面的分析有点绕,一遍看不懂得建议多看几遍好好咀嚼一下😅
刚开始由于p1和p2的互相链接,导致各自的计数器都为2,程序结束时两个节点各自析构一次,计数器各自减1。
我们知道自定义类型ListNode真正析构时才会调用内部的自定义类型的析构函数!(循环诱因之一)
此时如果我们想要真正的再次析构一次p1,使其计数器为0,就要通过释放p2的_prev来实现(此时只有p2的_prev等价于p1),而想要释放p2的_prev,就要通过释放p1的_next来实现(此时只有p1的_next等价于p2),而要释放p1的_next,就要真正的去析构p1...
死循环就此构成啦,原因就在于_next和_prev的指向本不该使得p1和p2的管理者增加的,一旦增加就不好解决了。因此得引入一个智能指针使得指向shared_ptr所指向的空间并不会增加管理者的数量,weak_ptr由此诞生!
weak_ptr的简单实现:
#pragma once
namespace jia
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const weak_ptr& sp)
:_ptr(sp._ptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp.get())
{
_ptr = sp.get();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
public:
T* _ptr; //没有计数器
};
}
可以看出只是简简单单的一个只能传shared_ptr和weak_ptr参数来进行构造的智能指针。理论上应该能解决刚才的循环引用问题。
#include<iostream>
using namespace std;
#include"Smart_ptr.h"
template<class T>
struct ListNode
{
ListNode()
:_next(nullptr)
,_prev(nullptr)
,_data(T())
{}
jia::weak_ptr<ListNode<T>> _next; //使用weak_ptr智能指针
jia::weak_ptr<ListNode<T>> _prev;
T _data;
};
int main()
{
jia::shared_ptr<ListNode<int>> p1(new ListNode<int>);
jia::shared_ptr<ListNode<int>> p2(new ListNode<int>);
p1->_next = p2;
p2->_prev = p1;
return 0;
}
理论与结果相符,完美解决!
🚩总结
智能指针终于说个七七八八了,信息量还是比较大的。智能指针的进化史磕磕绊绊,我们可以看出一个知识点的不断完善,其中的一些思想确实惊艳了我。虽然不是特别复杂,但是却被大佬们玩出了花👍。好啦,今天的分享就到此为止了👋