【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)){}// 拷贝构造:即创建一个新对象指针指向该资源,代表又有一个对象管理该资源,因此计数+1shared_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){}// 拷贝构造:即创建一个新对象指针指向该资源,代表又有一个对象管理该资源,因此计数+1shared_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';});
}