指针简介
智能指针std::shared_ptr简称为共享指针,智能指针std::weak_ptr简称为弱指针。
共享指针具有元素指针和引用计数基类指针,引用计数基类指针用以指向引用计数对象,而引用计数对象所属类继承自引用计数基类,具有元素指针或者元素空间。
引用计数基类具有两个计数器,分别是引用计数器和弱引用计数器。引用计数器控制元素的销毁或者析构时机,弱引用计数器控制引用计数对象的销毁时机。
函数简介
函数std::make_shared先创建引用计数对象,由引用计数对象构造元素,再创建临时共享指针,指向元素和引用计数对象,之后返回临时共享指针。
问题实质
类的成员变量包含共享指针,假设类的两个实例由共享指针管理,分别称为此实例与彼实例,此实例的共享指针指向彼实例,而彼实例的共享指针指向此实例。
上述情况可以视为,由共享指针管理的外层对象持有内层对象,而内层对象以共享指针形式直接或者间接引用外层对象。
销毁外层对象的前提是内层对象不再引用外层对象,而内层对象在析构之时才解除引用,即必须先销毁外层对象。似此对象之间跨层级相互引用,而无法解除引用关系,形成循环引用。
经典案例
倘若双向链表的指针都采用共享指针,精简代码如下所示:
#include <cstdlib>
#include <memory>
#include <iostream>
struct Node
{
std::shared_ptr<Node> _prev;
std::shared_ptr<Node> _next;
~Node()
{
std::cout << "~Node" << std::endl;
}
};
int main()
{
auto head = std::make_shared<Node>();
auto tail = std::make_shared<Node>();
head->_next = tail;
tail->_prev = head;
return EXIT_SUCCESS;
}
问题描述
在退出函数之时,局部变量因超出作用域而结束生命周期,依次销毁共享指针tail和head。
在析构tail之时,head所指节点的后继仍指向tail所指节点及其引用计数对象,导致引用计数对象的两个计数器都未归零,即tail所指节点及其引用计数对象无法销毁。
在析构head之时,由于tail所指节点未被销毁,而节点的前驱仍指向head所指节点及其引用计数对象,同样导致引用计数对象的两个计数器未归零,即head所指节点及其引用计数对象无法销毁。
所有节点及其引用计数对象未正常销毁,最终导致资源泄漏。
解决方案
节点的后继采用共享指针,而节点的前驱采用弱指针,如此便可打破循环引用。
不过,即使采用上述方案,双向循环链表仍然存在循环引用问题。
升阶案例
Eterfree线程池ThreadPool的简化代码如下所示:
#include <cstdlib>
#include <functional>
#include <memory>
#include <iostream>
class ThreadPool final
{
struct Structure
{
std::function<void()> _fetch;
~Structure()
{
std::cout << "~Structure" << std::endl;
}
};
private:
std::shared_ptr<Structure> _data;
public:
ThreadPool() : \
_data(std::make_shared<Structure>())
{
_data->_fetch = [_data] {};
}
};
int main()
{
ThreadPool threadPool;
return EXIT_SUCCESS;
}
问题描述
构造ThreadPool对象之时,先在构造函数体外的初始化列表之中,调用函数std::make_shared,创建指向Structure实例的共享指针,用以构造成员变量_data。再于构造函数体内,以Lambda表达式初始化Structure成员变量_fetch。在Lambda的捕获列表之中,以成员变量_data复制构造非局部变量_data,二者共同指向Structure实例及其引用计数对象。
析构ThreadPool对象之时,也会析构成员变量_data,由于存在非局部变量_data,引用计数对象的两个计数器都未归零,即Structure实例及其引用计数对象都未销毁。而Structure成员变量_fetch亦未析构,因此非局部变量_data无法析构,最终导致资源泄漏。
解决方案
寻找以下案例代码:
_data->_fetch = [_data] {};
改为下述代码:
_data->_fetch = [_data = std::weak_ptr(_data)] {};
在Lambda的捕获列表之中,非局部变量_data由共享指针对象改为弱指针对象。共享指针同时维护引用计数对象的引用计数器和弱引用计数器,而弱指针只维护引用计数对象的弱引用计数器。
在析构ThreadPool成员变量_data之时,即使存在非局部变量_data,引用计数对象的引用计数器也会归零,于是能够析构成员变量_data指向的Structure实例。而析构Structure实例,自然会析构函数子_fetch,当然也会析构非局部变量_data,使得引用计数对象的弱引用计数器归零,从而销毁引用计数对象。