目录
一、为什么需要智能指针?
在其他的面向对象的语言中是没有智能指针这样的概念的,因为他们有GC(GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一)。他们这些语言是不太害怕异常安全的问题的。在普通场景下,C++中需要手动的释放下资源也可以接受,但是在某些场景下,就很麻烦了。所以C++就出现了智能指针
#include<iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p = new int;
cout << div() << endl; // 异常安全的问题
cout << "delete:" << p << endl;
delete p;
}
int main()
{
try
{
func();
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}
return 0;
}
对于这样的一份代码,如果div()没抛异常代码就没问题;如果div()抛异常后,那么就会造成内存泄露的问题。
二、内存泄漏
什么是内存泄漏,内存泄漏的危害
内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
- 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何检测内存泄漏
可以看看这几位大佬的博客
在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_CHENG Jian的博客-CSDN博客_linux c++ 内存泄漏分析工具https://blog.csdn.net/gatieme/article/details/51959654
在windows下使用第三方工具:
VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库_波波在学习的博客-CSDN博客https://blog.csdn.net/GZrhaunt/article/details/56839765其他工具:内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)
https://www.cnblogs.com/liangxiaofeng/p/4318499.html
如何避免内存泄漏
- 1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 2. 采用RAII思想或者智能指针来管理资源。
- 3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
三、智能指针的使用及原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
针对刚开始提出的代码
#include<iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void func()
{
int* p = new int;
cout << div() << endl; // 异常安全的问题
cout << "delete:" << p << endl;
delete p;
}
int main()
{
try
{
func();
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}
return 0;
}
方案一,在func()中重新捕获,捕获了以后把资源释放掉,再重新抛出。
但是这种方式并没有从根源上解决问题。
这种情况下,div()会抛异常,new也会抛异常,如果是p1抛异常不用管(因为new抛异常了肯定是申请空间失败,用不着去释放空间);如果是p2抛异常,你需要释放p1;如果是p3抛异常,你需要释放p1,p2;如果是div()抛异常,你需要释放p1,p2,p3 ;我们可以通过以下代码解决
void func() { int* p1 = new int; int* p2 = nullptr, *p3 = nullptr; try { p2 = new int; p3 = new int; cout << div() << endl; } catch (...) { if (p2 == nullptr) //说明是p2抛异常了 { delete p1; } if (p3 == nullptr) { delete p1; delete p2; } if (p2 != nullptr && p3 != nullptr) { delete p1; delete p2; delete p3; } } delete p1; delete p2; delete p3; }
但是这样的代码非常的不好,会让你处理的焦头烂额。
方案二,借助RAII的思想我们自己实现一个智能指针
我们自己实现一个智能指针
template<class T> class SmartPtr { public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { cout << "delete:" << _ptr << endl; delete _ptr; } private: T* _ptr; }; int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void func() { int* p1 = new int; SmartPtr<int> sp1(p1); int* p2 = new int; SmartPtr<int> sp2(p2); int* p3 = new int; SmartPtr<int> sp3(p3); cout << div() << endl; // 异常安全的问题 } int main() { try { func(); } catch (const exception& e) { cout << e.what() << endl; } return 0; }
我们可以看出不管div()有没有抛异常,都会正常释放p1,p2,p3。因为不管是否抛异常,func()的栈帧都会正常结束,我们自己模拟的智能指针的对象都会出作用域然后调用析构函数。另外,如果是p2抛异常了,p1也会正常释放;p3抛异常了,p2,p1也都会正常释放。
ps:我们创建空间还可以这样写,让代码变的简单且去清晰
智能指针的原理
我们现在的智能指针还有些不足之处,还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,目前是无法解引用的,我们要让它像指针一样去使用,所以我们实现operator->和operator* 的重载
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
现在的这个智能指针还是存在问题的:
这里的拷贝会崩溃,原因就是我们现在没有写拷贝构造,所以他现在是浅拷贝,会被析构两次,所以就崩溃了。有人说直接深拷贝不就解决了吗?话虽如此,但是我们现在是智能指针,它的行为就要像指针一样,对于原生指针把p2给给p1就是浅拷贝,就是让两个指针指向同一块资源。此外还有一个原因就是你给给我的指针是new出来的还是malloc出来的都是无法确定的,也无法深拷贝。
如何解决智能指针拷贝的问题呢?
解决智能指针的拷贝又牵扯出C++的发展历史。智能指针在C++98就已经存在了,C++98解决拷贝问题的方法叫做管理权转移,C++98就设计出一个智能指针auto_ptr。设计这个智能指针的大佬这样想,两个对象指向同一块空间会析构两次,那么永远只有一个人指向这个空间就不会出现这样的问题。所以我们就可以做一个管理权转移。具体做法就是我们写一个拷贝构造,还是浅拷贝,然后让被拷贝的对象置空,这样就转移了资源。
std::auto_ptr
//C++98 管理权转移 auto_ptr
namespace pxl
{
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 != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
pxl::auto_ptr<int> sp1(new int);
pxl::auto_ptr<int> sp2(sp1); //管理权转移
}
这样的话确实解决了拷贝的问题,但是与此同时新的问题诞生了,sp1悬空了,用不了了。
eg:假如你完全不熟悉这种方式,你就很有可能去用sp1,就出现了空指针的问题
官方库中的auto_ptr同样是存在这样的问题的
所以auto_ptr是一个失败的设计,很多公司明确要求不能使用auto_ptr。
C++11库才更新了智能指针的实现,在中间的13年中则出现了Boost库,Boost库由Boost社区组织开发、维护。其目的是为C++程序员提供免费、同行审查的、可移植的程序库。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。 在C++11出来之前,大家都要去用Boost库。即使现在大家还在用Boost库。
当时Boost库就补充了几个很重要的智能指针:scoped_ptr,shared_ptr,weak_ptr。所以大家在中间这13年中用的就是这几个智能指针。
C++11出来以后就把Boost库中智能指针的精华部分吸收了过来,并且做了一些微调。C++11就出现了:unique_ptr,shared_ptr,weak_ptr。
std::unique_ptr
//unique_ptr
//原理:简单粗暴--防止拷贝(不让你拷贝)
namespace pxl
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
int main()
{
pxl::unique_ptr<int> sp1(new int);
pxl::unique_ptr<int> sp2(sp1);
}
库中的也是一样(库中的要包含#include<memory>这个头文件)
但是这样也还是太极端,对于需要拷贝的场景这个unique_ptr就没办法解决了。因此就又出现了shared_ptr。
std::shared_ptr
eg:
namespace pxl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
{
_refCount = 1;
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
{
++_refCount;
}
~shared_ptr()
{
if (--_refCount == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
static int _refCount;
};
template<class T>
int shared_ptr<T>::_refCount = 0;
}
int main()
{
pxl::shared_ptr<int> sp1(new int);
pxl::shared_ptr<int> sp2(sp1);
pxl::shared_ptr<int> sp3(sp1);
}
对于这样的情况是完全没问题的
当前写法的大问题在于这样,如果出现了一个新的资源,那么引用计数就变成1了,原因就是我们之间是共享这个引用计数的。而且最后也只会释放sp4这块资源,同时造成了sp1的内存泄漏。
所以这个静态的引用计数是不行的,我们应该一个资源配一个引用计数。在新资源调用构造函数的时候就在堆上去new一个计数。这个资源被拷贝的时候把这个计数一同拷贝过去
namespace pxl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
++(*_pRefCount);
}
~shared_ptr()
{
if (--(*_pRefCount) == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
};
}
int main()
{
pxl::shared_ptr<int> sp1(new int);
pxl::shared_ptr<int> sp2(sp1);
pxl::shared_ptr<int> sp3(sp1);
pxl::shared_ptr<int> sp4(new int);
}
这个时候就把两块资源全都释放掉了
凡是做浅拷贝几乎都要用引用计数。
PS:对于这个_pRefCount必须在堆上开空间,不能在栈上。因为如果开在栈上,每个对象的_pRefCount都是独立的,就会造成计数的不准确。
eg: 每次拷贝sp1的时候,_pRefCount的值在sp1里面都是1。
operator = 的实现
情况一:
对于这种情况,需要将sp1指向sp4的空间,而且将sp1原来指向的空间的引用计数--,sp4指向的空间的引用计数++。
情况二:
对于这种情况,除了要做情况1的工作以外,还需要将sp1原来指向的空间销毁 。
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
if (this != &sp) //确保不是自己赋值给自己
{
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
}
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
++(*_pRefCount);
}
return *this;
}
eg:
ps:我们销毁空间的代码是不适合去调用析构函数的,因为在有些场景下去统计调用析构函数的次数的话是会造成影响的。
目前我们的代码还是可以进行优化的,比如出现以下情况:
sp1赋值给sp1是没啥问题的,sp2赋值给sp1虽然两者都指向同一块空间,但是sp1和sp2的地址是不同的,所以按照现在写的逻辑还是会全部走一次,虽然结果没变但是中间的事全部白干了。所以我们直接用指向的空间进行判断更加合适
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
//if (this != &sp) //确保不是自己赋值给自己
if (_ptr != sp._ptr)
{
if (--(*_pRefCount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
}
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
++(*_pRefCount);
}
return *this;
}
为了待会方便进行加锁,我们将析构和++封装成两个函数(不进行封装也是可以的)
namespace pxl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
{
AddRef();
}
void Release()
{
if (--(*_pRefCount) == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
}
}
void AddRef()
{
++(*_pRefCount);
}
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
//if (this != &sp) //确保不是自己赋值给自己
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pRefCount;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
};
}
多线程问题
share_ptr真正面临的问题是多线程下的问题:
指针指向的堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。但是引用计数的线程安全问题是智能指针需要去处理的。
eg:对于这段代码,智能指针对象管理的一块资源而且有一个引用计数,我们用智能指针p去进行拷贝构造。多个线程中对同一个智能指针对象进行拷贝和析构的时候,++和--引用计数,引用计数存在经典等的线程安全问题。
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(pxl::shared_ptr<Date>& sp, size_t n)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
pxl::shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。
//所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
pxl::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 100;
thread t1(SharePtrFunc, std::ref(p), n);
thread t2(SharePtrFunc, std::ref(p), n);
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
cout << p.use_count() << endl;
return 0;
}
这段代码的逻辑就是两个线程都进行拷贝智能指针,把年月日都进行++,智能指针出了作用域以后进行析构销毁,所以如果没有线程安全的问题的话,那么最终统计的引用计数一定为1;如果存在线程安全,那么程序崩溃。
运行结果直接崩溃,证明存在线程安全问题 。
所以我们要对我们自己写的shared_ptr中引用计数的加减进行加锁。
因为锁不支持拷贝,所以用指针保证每个智能指针对象访问到的都是同一把锁。
namespace pxl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
_pmtx->lock();
if (--(*_pRefCount) == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
}
_pmtx->unlock();
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
//if (this != &sp) //确保不是自己赋值给自己
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pRefCount;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx; //因为锁不支持拷贝,所以用指针保证访问到的都是同一把锁
};
}
这个时候即使年月日出现了线程安全的问题,我们智能指针的引用计数也是没有线程安全的,都是有保障的。
目前还是存在问题的,因为我们的锁是new出来的,所以要进行释放,但是释放的时候进很尴尬。
如果直接在这进行释放,下面还要进行解锁,这就完全不符合逻辑了,所以不能在这进行释放。
我们可以用一个flag去进行标识从而释放锁
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
_ptr = nullptr;
_pRefCount = nullptr;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
这个flag是没有线程安全的,因为flag是局部对象,这个Release最终是只有一个人会进去的,只有最后一个人会--到0,然后进行解锁,最终把锁释放。
我们智能指针中的mutex是否能保护指针指向的资源?
不能也保护不了,引用计数的线程安全是可以进行保护的,但是指针指向的资源是不能进行保护的。 解引用的时候是访问资源的,我在解引用的地方加锁是没有用的,人家调用operator*,operator->是在调用这个函数的外面进行访问的,我的锁是锁不住你的。所以对于外面的资源要自己进行加锁。
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(pxl::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
pxl::shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。
//所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
unique_lock<mutex> lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
int main()
{
pxl::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 100000;
mutex mtx;
thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
t1.join();
t2.join();
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
cout << p.use_count() << endl;
return 0;
}
现在我们的代码是绝对线程安全的。
ps:控制锁的粒度
比如470行以后还有很多代码,但是我只想要我的锁控制468,469,470这三行的代码,保证它们的线程安全,又不想保证之后代码的线程安全,我该怎么做呢?
我们可以扩一个匿名的局部域,这样这把锁的生命周期就缩小到这个域中了,出了这个域锁就自动解锁了。我们就可以通过这样的域想控制哪块就控制哪块。
同时我们还可以用unique_lock的lock和unlock接口去手动解决,但是这样远不如这个域好。
综上,shared_ptr智能指针是线程安全的,引用计数的加减是加锁保护的,但是指向的资源不是线程安全的。
完整代码:
namespace pxl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pRefCount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
_ptr = nullptr;
_pRefCount = nullptr;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
//if (this != &sp) //确保不是自己赋值给自己
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pRefCount;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx; //因为锁不支持拷贝,所以用指针保证访问到的都是同一把锁
};
}
shared_ptr的循环引用
到现在我们的shared_ptr就相对完善了,但是现在还存在一个问题就是shared_ptr的循环引用,普通的场景下是不会有循环引用的问题的,但是在一些特殊的场景下就存在循环引用的问题。
这个程序是一个很正常的程序,基于我们所学的智能指针,我们可以进行改造。因为需要进行释放,所以防止中间抛异常没有进行释放的问题,我们就可以运用智能指针让他自动的进行释放
PS:库里的shared_ptr是不允许隐式类型准换的,因为有些地方偷偷转换成智能指针就会自动的去析构,这样非常的不好。
所以我们用拷贝构造。
这个时候仍然编译不过,因为_next和_prev是原生指针,n1和n2是智能指针,智能指针是不能赋值给原生指针的
所以我们干脆把_next和_prev也改成智能指针。这个时候我们发现编译通过了但是析构函数不被进行调用了
析构没调用说明没进行释放,这个问题的原因就是此处发生了经典的循环引用问题。
n2后定义,n2先析构,引用计数减为1;n1再析构,引用计数也减为1。然后,n2这个空间要释放取决于n1中的next(因为现在n1中的next管理这个节点),也就是next的生命周期到了,n2就释放了。但是next又是n1的成员,只有n1调用了析构函数,才会去调用next的析构函数,所以next的释放取决于n1的释放。n1现在又由n2中的prev管理,n2中的prev释放了n1才会释放,但是prev的释放又取决于n2这个节点。n2的释放又取决于n1中的next,next的释放取决于n1,n1的释放取决于n2中的prev,prev的释放又取决于n2,n2的释放又取决于n1中的next......所以就会造成如此反复循环,永远是一个死循环。
这两个节点永远不能被释放,这样的问题就叫做循环引用。
PS:指向的语句中存在任意一句都是不会存在这样的问题的,只有两句都存在才会有循环引用的问题
这个时候n2先析构引用计数减减为1,n1再析构,引用计数减减为0,随着n1的析构,n1中的next也要进行析构,从而也让n2进行析构。此时两块空间全部进行释放。
我们就要认识到用智能指针管理对象,忌讳你里面有一个智能指针管我,我里面有一个智能指针管你。
如何解决循环引用的问题呢?
正因为如此,弱智针随之出现,解决这种循环引用的问题。
weak_ptr不是常规意义的智能指针,它没有接受一个原生指针的构造函数,也不符合RALL。而且weak_ptr的use_count返回的是shared_ptr的引用计数
我们这里改成weak_ptr的 好处就是它不增加引用计数,我们就是因为增加的引用计数才出现的循环引用。weak_ptr的next和prev对象可以去访问指向的节点资源,但是不参与节点资源的释放管理,其实就是不增加引用计数。
从而就解决了循环引用的问题。
weak_ptr赋值前后都为1,shared_ptr赋值前为1,赋值后为2.
weak_ptr最大的特点就是它可以像智能指针一样去访问资源,但是不参与管理。
模拟实现weak_ptr
上面我们使用的都是库里面的weak_ptr和shared_ptr,接下来我们进行模拟实现weak_ptr。
namespace pxl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pRefCount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
,_pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
_ptr = nullptr;
_pRefCount = nullptr;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr<T>& operator = (const shared_ptr<T>& sp)
{
//if (this != &sp) //确保不是自己赋值给自己
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
~shared_ptr()
{
Release();
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pRefCount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx; //因为锁不支持拷贝,所以用指针保证访问到的都是同一把锁
};
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get(); //因为是私有,所以去调用get
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
//int* _pRefCount; //库里面是有计数的,但我们是简易版本不写也行
};
}
struct ListNode
{
int _val;
pxl::weak_ptr<ListNode> _next;
pxl::weak_ptr<ListNode> _prev;
//pxl::shared_ptr<ListNode> _next;
//pxl::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
pxl::shared_ptr<ListNode> n1(new ListNode);
pxl::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
这个时候我们自己模拟实现的也是可以解决问题的。
- 1. RAII特性
- 2. 重载operator*和opertaor->,具有像指针一样的行为。
四、智能指针管理多对象
虽然这里的new[ ] 和delete 不匹配使用,但是既不报错也不内存泄漏。
因为new的底层是malloc,delete的底层是free,free是不会区分你malloc了1个还是多个,只要你malloc出来的,我通通会free掉,所以既不报错也不内存泄漏。但是我们建议匹配使用,因为有时候又会出现问题。
这段代码只要写了析构函数就会报错
原因:A的一个对象有8个字节,new了10个就是80个字节,如果有析构函数就不会只开80个字节,而是会在头上多开4个字节去存new了的个数(对应我们的代码就是存了10)
delete[ ]的时候,并没有明确指明要delete多少个,这个时候就直接去头上去那4个字节,取到里面的值发现是10个,就去调用10次析构函数;如果你没有显示的写出析构函数,那么就不会多开4个字节去存储这个个数。所以你直接delete的时候,那么就会认为只有1个对象,这个时候是不会取头上4个字节的,但是new[ ]又多开了4个字节,所以位置就不对了,所以报错。因此一定要匹配使用。
同理对于智能指针也会有这样的问题
因为智能指针底层默认都是delete,582行没问题,583行就有问题了。
定制删除器
默认情况下,智能指针底层都是delete资源,那么如果你的资源不是new出来的呢?比如:new[] malloc,fopen,这个时候就需要定制删除器来解决。
定制删除器是一个可调用的对象即可。
这个地方就可以传定制删除器,默认的定制删除器就是delete
我们就可以自己写一个定制删除器
我们可以改成模板的更加通用
对于我们自己实现的智能指针我们也可以让其支持定制删除器,我们只需要在unique_ptr多增加一个模板参数,然后用这个类对象去进行析构即可。
namespace pxl { template<class T> class defalut_delete { public: void operator()(const T* ptr) { cout << "delete:" << ptr << endl; delete ptr; } }; //释放方式由defalut_delete决定 template<class T, class D = defalut_delete<T>> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {} ~unique_ptr() { if (_ptr != nullptr) { //cout << "delete:" << _ptr << endl; //delete _ptr; D del; del(_ptr); } } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } unique_ptr(const unique_ptr<T>& sp) = delete; unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete; private: T* _ptr; }; } class A { public: ~A() { cout << "~A()" << endl; } private: int _a1 = 0; int _a2 = 0; }; template<class T> struct DeleteArray { void operator()(const A* ptr) { cout << "delete[]:" << ptr << endl; delete[] ptr; } }; struct DeleteFile { void operator()(FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); } }; int main() { pxl::unique_ptr<A> sp1(new A); pxl::unique_ptr<A, DeleteArray<A>> sp2(new A[10]); pxl::unique_ptr<FILE, DeleteFile> sp3(fopen("test.txt", "w")); return 0; }
shared_ptr同样可以这样去做,但是它又有一些不一样,shared_ptr不是在类这个地方给参数,它是在构造函数这里给
原因就是他们的底层结构不一样,unique_ptr就支持不了在构造的时候给定制删除器
如果在构造函数给了,没有成员去保存这个定制删除器,你只有把这个删除器保存下来,你析构函数才能去释放。我们自己写的unique_ptr,shared_ptr的这样的架构都是支持不了在构造函数给的。
库中的shared_ptr和我们模拟实现是完全不一样的,库中的是由5个类构成的,它底层管理引用计数和管理释放的由专门的一个叫做shared_count的一个类去管理的,库中的可以把定制删除器传给下一层,下一层就可以推演出这个定制删除器的类型就可以让构造函数保证这个资源存在
eg: 构造函数把指针和定制删除器传给shared_count这个类对象,这个类就可以声明定制删除器