C++ | 再探智能指针(shared_ptr 与 weak_ptr)
上篇博客我们模拟实现了 auto_ptr 智能指针,可我们说 auto_ptr 是一种有缺陷的智能指针,并且在C++11中就已经被摈弃掉了。那么本章我们就来探索 boost库和C++11中的智能指针以及其实现方法。
文章目录:
一、独占型智能指针 scope_ptr
二、强 智能指针shared_ptr
三、弱 智能指针 weak_ptr
注:在本文中模拟的智能指针并不与库中的智能指针的实现完全相同,只是为了通过探究其实现原理而进行的一种模拟。
一、独占型智能指针 scope_ptr
在 boost中有一种 scope_ptr 指针,可以说这是boost库中最为简单的一种智能指针了。相对于前两种智能指针而言, scope_ptr 规定,一个智能指针只能引用一块堆内存,当这个指针的作用域消失之后自动释放。 scope_ptr 实现起来很简单,只需要将拷贝构造函数和赋值函数的接口屏蔽起来即可。
template<typename T>
class Scope_ptr
{
public:
Scope_ptr(T* ptr)
{
m_ptr = ptr;
}
~Scope_ptr()
{
delete m_ptr;
m_ptr = NULL;
}
T& operator*()
{
return *m_ptr;
}
T* operator->()
{
return m_ptr;
}
private:
Scope_ptr(const Scope_ptr<T>& rhs);
Scope_ptr<T>& operator=(const Scope_ptr<T>& rhs);
T* m_ptr;
};
缺点:同样的,没有什么是完美的,虽然在类中屏蔽了拷贝构造和赋值函数的接口,可是如果人为的去进行赋值,还是会出现多个智能指针指向同一片堆内存的情况。真是防不胜防啊😓。
int main()
{
int* p = new int;
Scope_ptr<int> sp1(p);
Scope_ptr<int> sp2(p);
Scope_ptr<int> sp3(p);
return 0;
}
我们可以看到,sp1、sp2、sp3,都指向内 p 所申请的堆内存中。而在对象销毁时,sp3 先进行销毁,同时会释放堆内存,而后的 sp2,sp1 就成为了悬挂指针,在进行销毁时就会出现内存重复释放的问题。
下面我们来介绍一种比较强的智能指针 shared_ptr 智能指针。
二、强智能指针shared_ptr
之前的几种智能指针方案都存在缺陷,它们在处理自身与引用的对象间的关系时总是不够理想。既然指针自己无法完美的管理与对象之间的关系,那么,我们就单独设计一个管理类用于管理指针与对象之间的引用关系。而这种设计理念也正是我们的 shared_ptr 类智能指针,我们一起来看看它又是怎么处理的呢。
首先说明 shared_ptr 这是一种强智能指针,强是相对于弱存在的,那么应该也存在一种弱智能指针喽?
是的,我们一会还要介绍一种弱智能指针 weak_ptr,这些放在下文在做讨论。抛开这些暂且不提,先讲 shared_ptr 实现,它的内部维护一个引用计数器来判断一块堆内存被引用的次数。
我们知道在字符串的写实拷贝实现中,通过设置一个引用计数区域来判断某片空间被引用的次数。同样的,我们在设计智能指针时也可以采取类似的方法。只不过我们的智能指针类可以有多个不同对象指向不同的堆内存。因此,在 shared_ptr 类中专门为其设置了一个引用计数管理器类。
clsaa Node
{
void* addr; /* 保存堆内存地址 */
int refCount; /* 保存该堆内存引用次数 */
};
通过在引用计数管理器类设计一种由结构体或者类封装的表,表中保存着申请的堆内存和其对应的引用次数。而在智能指针类中实现向表中添加数据,在某个堆内存的引用次数为0时,对该堆内存进行释放。大致模型如下:
下面是模拟实现 Shared_ptr 智能指针的具体实现
/* 引用计数管理类 */
class RefManage
{
public:
RefManage() : length(0) {}
/* 增加引用计数 */
void addRef(void* ptr)
{
if (ptr != NULL)
{
int index = Find(ptr);
if (index < 0)
{
arr[length].addr = ptr;
arr[length].refCount++;
length++;
}
else
{
arr[index].refCount++;
}
}
}
/* 删除一个引用计数 */
void delRef(void* ptr)
{
if (ptr != NULL)
{
int index = Find(ptr);
if (index < 0)
{
throw exception("addr is not exist");
}
else
{
if (arr[index].refCount != 0)
{
arr[index].refCount--;
}
}
}
}
/* 返回当前堆内存的引用计数 */
int getRef(void* ptr)
{
if (ptr == NULL)
{
return 0;
}
int index = Find(ptr);
if (index < 0)
{
return -1;
}
else
{
return arr[index].refCount;
}
}
private:
/* 查找是否是已经存在的堆区空间 */
int Find(void* ptr)
{
for (int i = 0; i < length; ++i)
{
if (arr[i].addr == ptr)
{
return i;
}
}
return -1;
}
/* 局部类,储存引用计数信息 */
class Node
{
public:
Node()
{
memset(this, 0, sizeof(Node));
}
public:
void* addr; /* 保存堆内存地址 */
int refCount; /* 保存该堆内存引用次数 */
};
Node arr[10]; /* 用数组模拟10个空间的引用计数器*/
int length; /* 有效结点个数、当前要插入的下标*/
};
/* Shared_ptr 智能指针类 */
template<typename T>
class Shared_ptr
{
public:
Shared_ptr(T* ptr = NULL) :m_ptr(ptr)
{
AddRef();
}
Shared_ptr(const Shared_ptr<T>& rhs)
:m_ptr(rhs.m_ptr)
{
AddRef();
}
Shared_ptr<T>& operator=(const Shared_ptr<T>& rhs)
{
if (this != &rhs)
{
/* 自身引用次数减一 */
DelRef();
/* 若引用次数为0,则立刻释放 */
if (GetRef() == 0)
{
delete m_ptr;
}
m_ptr = rhs.m_ptr;
AddRef();
}
return *this;
}
~Shared_ptr()
{
DelRef();
if (GetRef() == 0)
{
delete m_ptr;
}
m_ptr = NULL;
}
T& operator*() const
{
return *m_ptr;
}
T* operator->() const
{
return m_ptr;
}
private:
void AddRef()
{
rm.addRef(m_ptr);
}
void DelRef()
{
rm.delRef(m_ptr);
}
int GetRef()
{
return rm.getRef(m_ptr);
}
T* m_ptr;
static RefManage rm;
};
/* 静态成员变量在类外初始化 */
template<typename T>
RefManage Shared_ptr<T>::rm;
运行测试:
运行程序可以看到分别对 int 类型 、char 类型、double 类型初始化了三张表。我们在程序中动态申请了 int、char、double类型的堆内存空间。
执行到程序的最后一个语句时,我们可以看到在右侧对应的引用计数表中分别写入了每块堆内存地址和引用次数。
最后,在执行 return 语句时智能指针依次被销毁,并且在引用计数管理器中引用次数也在一并减少,直至某个引用次数为0时调用 delete m_ptr
析构掉堆内存。这一过程可通过调试观察到,有兴趣的同学可以自行调试观看。
单例模式的引用计数管理器
我们发现针对 int 类型、char 类型和double 类型的智能指针最终生成了三个不同的引用计数管理器,可是我们在设计引用计数管理器类的时候把保存内存地址的数据类型设置的是 void* 类型,就是为了能够保存不同类型的堆内存地址,如果像上图这样每定义一种智能指针就构造一个引用计数管理器未免太不高效了。
我们引用计数管理类针对不同的类型可以只生成一次,并且只需要生成一次。也就是说对于 shared_ptr 只需要一个实例的RefManage 就可以满足要求,这正好符合我们的设计模式中的单例模式。因此,我们可以将该智能指针的引用计数管理器类设计成为一个单例模式的类。点这里》》》快速传送门——单例模式的应用计数管理器
缺点,没错,还是有缺点
我们先来看一段代码:
class B;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
Shared_ptr<B> pa;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
Shared_ptr<A> pb;
};
int main()
{
Shared_ptr<A> spa(new A());
Shared_ptr<B> spb(new B());
spa->pa = spb; /* 引用计数加一 */
spb->pb = spa; /* 引用计数加一 */
return 0;
}
在这段代码中就会发生内存泄漏,也就是本该析构的堆内存没有析构造成的。
如下图所示,分别为刚申请完 spa、spb时,执行完各自指针的互相指向时,已经调用完 return 后即将退出时:
我们在类A 和 类B 的析构中设计有打印函数,可以确定函数的调用情况,下面来看一下整个程序的输出情况
可以看到,在输出窗口只打印了构造函数而没有打印析构。也就是说从始至终都没有调用析构函数。
具体发生了什么让我们来看一张图就明白了。
简单来说就是,在两个堆内存对象中的智能指针互相指向对方,从而使得对方(这片堆内存被引用)的引用计数加一。但是栈上的指针指针只有两个。也就是说在程序结束时,系统自动清理栈上的两个智能指针,而两片空间的引用次数分别都为二。在栈清理完结束后,各自堆内存的引用计数只减少了一次,没有达到释放的条件,最终导致内存泄漏。
读者:(●´∀`●)你TM是不是在玩我?找茬?故意的吧,讲了这么多智能指针各个都有缺陷?再说正常写程序谁特莫互相引用着玩啊?
博主:别生气,别生气,听我慢慢给你解释。其实呢,对于我们日常中自己编写的应用,之前的那些智能指针都可以使用,甚至不用智能指针,只要你的程序逻辑够严密也不会发生内存泄漏。但是呢,作为一个库的提供者是面对所有编程人员的,每个人的编程习惯不同,使用的场景也不同,难免会产生纰漏。千里之堤毁于蚁穴,历史告诉我们千万不要不把编译器的警告不当回事,而且以上这种用法在某些场景确实是会用到的。好了,接下来我们进的 weak_ptr 就不会有什么问题了😭。
三、弱智能指针 weak_ptr
shared_ptr 是强智能指针 而 weak_ptr 是弱智能指针,那么这个‘强’ 与 ‘弱’ 又是如何定义和区分的呢?
我们可以这样简单的理解,强智能指针凡是引用就计数加一,而弱智能指针只引用不加一。并且weak_ptr的存在就是为了弥补 shared_ptr 的不足而诞生的。
weak_ptr 基于 shared_ptr ,它在引用堆内存时作为一个观察着的身份存在,在引用堆内存对象时仅仅获得资源的观测权。并且weak_ptr没有共享资源,不会引起指针引用计数的增加。
因此,对于上面的实例我们改成这样就不会出错了。
ps:我们自己写的Shared_ptr 是大写首字母,标椎库中提供的全是小写的类名,注意在这里我们使用标准库中的 shared_ptr 和 weak_ptr。
class B;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
weak_ptr<B> pa;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
weak_ptr<A> pb;
};
int main()
{
shared_ptr<A> spa(new A());
shared_ptr<B> spb(new B());
spa->pa = spb; /* 引用计数加一 */
spb->pb = spa; /* 引用计数加一 */
return 0;
}
输出正常,也就是成功的释放的对象。
Weak_ptr 具体实现
Weak_ptr 的实现也非常简单,我们在之前设计的 Shared_ptr 中添加一个返回自身指针的接口给 Weak_ptr 使用。在 Weak_ptr 中的赋值函数中把强智能指针(Shared_ptr)的 m_ptr 赋值给弱智能指针Weak_ptr 的 m_ptr 即可。
template<typename T>
class Shared_ptr
{
public:
/*
省略部分代码……
*/
T* GetPtr() const
{
return m_ptr;
}
};
template<typename T>
class Weak_ptr
{
public:
Weak_ptr(T* ptr = NULL) :m_ptr(ptr) {}
Weak_ptr(const Weak_ptr<T>& rhs) :m_ptr(rhs.m_ptr) {}
Weak_ptr<T>& operator=(const Shared_ptr<T>& rhs)
{
/* 强智能指针给弱智能指针赋值 */
m_ptr = rhs.GetPtr();
return *this;
}
~Weak_ptr()
{
m_ptr = NULL;
}
T* operator->()
{
return m_ptr;
}
T& operator*()
{
return *m_ptr;
}
private:
T* m_ptr;
};
在我们使用的代码中如果有需要内部智能指针互相指向时,选择使用 Weak_ptr 弱智能指针即可。我们再次测试以上实例。
输出:A() B() ~A() ~B()
OK,完成。
至此我们已经成功模拟了 shared_ptr 和 weak_ptr ,并且经过简单的实验我们已掌握其用法。
最后,欢迎大家评论留言,相互学习。有错误的地方请大家帮忙指出,谢谢。
附:C++ | 智能指针初探 https://blog.csdn.net/weixin_43919932/article/details/104505178