【C++ 第二十章】模拟实现 shared_ptr(可以拷贝的智能指针)
本文主要讲解如果简单模拟实现库中的 shared_ptr
而不会过多的对库中的 shared_ptr 讲解
1. 初始版本
智能指针的基本框架
namespace my
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~shared_ptr() {
delete _ptr;
_ptr = nullptr;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
2. 添加 引用计数器
因为一个资源需要对应一个 计数器
不能设置成:
普通成员:一个对象就有一个,达不到共同管理一个资源的目的
静态成员:所有对象共用一个,但达不到 不同资源对应不同计数器 的目的
解决办法:
(1)可以在创建管理这块资源的第一个对象时,创建新的计数器
(2)同时将 计数器 存储在堆区:即 new int(1),使其不会随一个对象的释放而销毁
引用计数的规则
(1)如果引用计数是 0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
(计数=0,则表示自己是最后一个人,可以释放空间了)
(2)如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
(计数不等于0,则表示自己不是是最后一个人,没有释放资源的权力)
namespace my
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
: _pCount(new int(1)) // 将 引用计数 开辟在堆里面
{}
// 每次调用析构都 计数-1, 如果计数=0,说明该对象是最后一个管理该资源的对象,可以直接释放该资源
~shared_ptr() {
if (--(*_pCount) == 0) {
delete _ptr;
delete _pCount;
_pCount = nullptr;
_ptr = nullptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
int* _pCount; // 引用计数
};
}
3. 添加 赋值重载 + 拷贝构造
实现思路:
拷贝构造:就是将自己的指针指向 传递过来的对象 管理的资源,同时 计数+1
赋值重载:思路差不多
namespace my
{
template<class T>
class shared_ptr
{
typedef shared_ptr Self;
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pCount(new int(1))
{}
// 拷贝构造:即创建一个新对象指针指向该资源,代表又有一个对象管理该资源,因此计数+1
shared_ptr(Self& s) {
_ptr = s._ptr;
_pCount = s._pCount;
(*_pCount)++;
}
// 赋值重载:
Self& operator=(Self& s) {
if (_ptr != s._ptr) { // 防止自己赋值给自己:浪费
// 先处理旧关系,再处理新关系
Realse(); // 和之前管理的资源断开关系,其实这里就是一次析构的过程,但是不建议直接调用析构函数(可能会引发一些未知错误), 可以将析构函数的逻辑提取出来设计 Realse 函数
// 管理新资源
_ptr = s._ptr;
_pCount = s._pCount;
(*_pCount)++;
}
return *this;
}
void Realse() {
if (--(*_pCount) == 0) {
delete _ptr;
delete _pCount;
_pCount = nullptr;
_ptr = nullptr;
}
}
~shared_ptr() {
Realse();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
int* _pCount;
};
}
测试代码
int main() {
my::shared_ptr<int> mySp1(new int(2024));
my::shared_ptr<int> mySp2 = mySp1;
my::shared_ptr<int> mySp3;
mySp3 = mySp1;
cout << *mySp1 << '\n';
cout << *mySp2 << '\n';
cout << *mySp3 << '\n';
}
上面的代码还存在一个问题:析构函数中 delete _ptr 释放指向的空间资源,按照C++语法,
如果 _ptr = new int(),则 delete _ptr;
如果 _ptr = new int[10],则 delete[] _ptr
如果不对应使用 正确的delete,会导致内存泄漏及其他一些问题
因此析构函数中的 realse 函数就不能固定写死成:delete _ptr
可以使用仿函数解决此问题:定制删除器
4.添加 定制删除器
看C++库中,shared_ptr 的删除器(仿函数),并不是在函数模板参数处传递,而是直接作为 构造函数的参数传递,这意味着它需要多写一个 函数模板的构造函数
namespace my
{
template<class T>
class shared_ptr
{
typedef shared_ptr Self;
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pCount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D Delete) // 重载函数别写 T* ptr = nullptr,默认参数只能写在一个地方,否则这里报错奇奇怪怪
: _ptr(ptr)
, _pCount(new int(1))
, _Delete(Delete)
{}
// 拷贝构造:即创建一个新对象指针指向该资源,代表又有一个对象管理该资源,因此计数+1
shared_ptr(Self& s) {
_ptr = s._ptr;
_pCount = s._pCount;
(*_pCount)++;
}
// 赋值重载:
Self& operator=(Self& s) {
if (_ptr != s._ptr) { // 防止自己赋值给自己:浪费
// 先处理旧关系,再处理新关系
Realse(); // 和之前管理的资源断开关系,其实这里就是一次析构的过程,但是不建议直接调用析构函数(可能会引发一些未知错误), 可以将析构函数的逻辑提取出来设计 Realse 函数
// 管理新资源
_ptr = s._ptr;
_pCount = s._pCount;
(*_pCount)++;
}
return *this;
}
void Realse() {
if (--(*_pCount) == 0) {
_Delete(_ptr); // 使用定制删除器
// delete _ptr;
delete _pCount;
_pCount = nullptr;
_ptr = nullptr;
}
}
~shared_ptr() {
Realse();
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
int* _pCount;
function<void(T*)> _Delete = [](const T* ptr) {delete ptr; }; // 默认删除器:对非数组类型直接 delete
};
}
测试代码
int main() {
// 因为 shared_ptr 内部默认的删除器使用的是 delete,下面这里需要删除int数组,因此需要 delete[],则自己传仿函数对象
my::shared_ptr<int> mySp1(new int[10]{ 1, 2, 3 }, [](const int* ptr) {
delete[] ptr;
cout << "delete[] ptr; " << '\n';
});
// malloc 需要使用 free 释放,需要自己传定制的删除器
my::shared_ptr<int> mySp2((int*)malloc(40), [](int* ptr) {
free(ptr);
cout << "free(ptr);" << '\n';
});
}