持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
概念
和 shared_ptr、unique_ptr 类型指针一样,weak_ptr 智能指针也是以模板类的方式实现的。weak_ptr<T>( T 为指针所指数据的类型)定义在<memory>头文件,并位于 std 命名空间中。
该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。可以视为 shared_ptr 指针的一种辅助工具。借助 weak_ptr 类型指针可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、通过expired()判断shared_ptr 指针指向的堆内存是否已经被释放等等。
weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。当 weak_ptr 指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。它不具有普通指针的行为,没有重载operator*和->,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。
weak_ptr模板类
template<class T> class weak_ptr {
public:
using element_type = remove_extent_t<T>;
// 构造函数
constexpr weak_ptr() noexcept;
template<class Y>
weak_ptr(const shared_ptr<Y>& r) noexcept;
weak_ptr(const weak_ptr& r) noexcept;
template<class Y>
weak_ptr(const weak_ptr<Y>& r) noexcept;
weak_ptr(weak_ptr&& r) noexcept;
template<class Y>
weak_ptr(weak_ptr<Y>&& r) noexcept;
// 析构函数
~weak_ptr();
// 赋值
weak_ptr& operator=(const weak_ptr& r) noexcept;
template<class Y>
weak_ptr& operator=(const weak_ptr<Y>& r) noexcept;
template<class Y>
weak_ptr& operator=(const shared_ptr<Y>& r) noexcept;
weak_ptr& operator=(weak_ptr&& r) noexcept;
template<class Y>
weak_ptr& operator=(weak_ptr<Y>&& r) noexcept;
// 其中 x 表示一个同类型的 weak_ptr 类型指针,
// 该函数可以互换 2 个同类型 weak_ptr 指针的内容。
void swap(weak_ptr& r) noexcept;
// 将当前 weak_ptr 指针置为空指针。
void reset() noexcept;
// 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
long use_count() const noexcept;
// 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
bool expired() const noexcept;
// 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;
// 反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
shared_ptr<T> lock() const noexcept;
template<class U>
bool owner_before(const shared_ptr<U>& b) const noexcept;
template<class U>
bool owner_before(const weak_ptr<U>& b) const noexcept;
};
std::weak_ptr构造
#include <iostream>
#include <memory>
int main() {
// 创建一个空 weak_ptr 指针
std::weak_ptr<int> wp1;
// 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针
// 若 wp1 为空指针,则 wp2 也为空指针;反之,
// 如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,
// 则 wp2 也指向该块存储空间(可以访问,但无所有权)。
std::weak_ptr<int> wp2 (wp1); // 拷贝构造函数
std::weak_ptr<int> wp2 = wp1; // 拷贝构造函数
// weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,
// 因为在构建 weak_ptr 指针对象时,可以利用已有的 shared_ptr 指针为其初始化。
// 由此,wp3 指针和 sp 指针有相同的指针。
std::shared_ptr<int> sp (new int);
std::weak_ptr<int> wp3 (sp); // shared_ptr直接构造
std::weak_ptr<int> wp3 = sp; // 拷贝赋值函数
}
std::weak_ptr用法
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
#include <iostream>
#include <memory>
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl; // 输出1
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl; // 赋值给weak_ptr后还是输出1
if(!wp.expired()){ // 检查sh_ptr是否还有效
std::shared_ptr<int> sh_ptr2 = wp.lock(); //将sh_ptr赋值给sh_ptr2
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl; // 输出2
}
} //delete memory
std::weak_ptr<int> wp;
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
wp = sh_ptr;
std::cout << wp.expired() << std::endl; // 输出false
} //delete memory
std::cout << wp.expired() << std::endl; // 输出true
}
判断weak_ptr指向对象是否存在
既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。这时,我们就不能使用weak_ptr直接访问对象。那么我们如何判断weak_ptr指向对象是否存在呢?C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr。
#include <iostream>
#include <memory>
int main(int argc, const char* argv[]) {
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
//sp.reset();
if (std::shared_ptr<int> pa = wp.lock()) {
std::cout << *pa << std::endl;
}
else {
std::cout << "wp指向对象为空" << std::endl;
}
sp.reset();
if (std::shared_ptr<int> pa = wp.lock()) {
std::cout << *pa << std::endl;
}
else {
std::cout << "wp指向对象为空" << std::endl;
}
}
输出:
10
wp指向对象为空
weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr示例,进而访问原始对象。
循环引用
weak_ptr的一个作用是解决share_ptr的循环引用问题。如下面代码所示,class A中含有指向class B的shared指针, class B 中含有指向class A的shared指针,这样形成了循环引用。spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法是改用weak_ptr:
#include <iostream>
#include <memory>
class B;
class A
{
public:
A() { std::cout << "A's constructor ..." << std::endl; }
~A() { std::cout << "A's destructor ..." << std::endl; }
std::shared_ptr<B> b;//class A中含有指向class B的shared指针
};
class B
{
public:
B() { std::cout << "B's constructor ..." << std::endl; }
~B() { std::cout << "B's destructor ..." << std::endl; }
std::shared_ptr<A> a; //class B 中含有指向class A的shared指针
};
int main()
{
std::shared_ptr<A> spa = make_shared<A>();
std::shared_ptr<B> spb = make_shared<B>();
spa->b = spb;// bb 计数器来到了 2
std::cout << spb.use_count() << std::endl;
spb->a = spa;// spa 计数器来到了 2
std::cout << spa.use_count() << std::endl;
return 0;
}
// 上面main函数中spa和spb计数器都加到了2,而出了main函数作用域,
// 其计数都会-1,变成了1,而不是0,所以它们都没有被析构(销毁)。
所以,这种情况我们就需要使用weak_ptr替代shared_ptr来避免循环引用导致内存泄漏问题,看下面例子:
#include <iostream>
#include <memory>
class B;
class A
{
public:
A() { std::cout << "A's constructor ..." << std::endl; }
~A() { std::cout << "A's destructor ..." << std::endl; }
std::weak_ptr<B> b;
};
class B
{
public:
B() { std::cout << "B's constructor ..." << std::endl; }
~B() { std::cout << "B's destructor ..." << std::endl; }
std::weak_ptr<A> a;
};
int main(int argc, const char* argv[]) {
std::shared_ptr<A> spa = std::make_shared<A>();
std::shared_ptr<B> spb = std::make_shared<B>();
cout << "spa = " << spa.use_count() << endl;
cout << "spb = " << spb.use_count() << endl;
spa->b = spb; //spb强引用计数为2,弱引用计数为1
spb->a = spa;
//如果spb->a为weak_ptr类型,不会增加强引用计数,
//所以spa强引用计数为1,弱引用计数为2
cout << "spa = " << spa.use_count() << endl;
cout << "spb = " << spb.use_count() << endl;
return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了
weak_ptr不会增加shared_ptr的计数器,从而离开mian函数作用域时,shared_ptr spa& spb计数器都 -1 ,成为0, 具备销毁条件,调用析构函数销毁自己和所指对象,因此,我们终于可以说weak_ptr具备避免内存泄漏的功能了。
使用场景
下面我们来介绍几个个weak_ptr的使用场景:
共享对象的线程安全问题
假设线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。
#include <iostream>
#include <memory>
#include <thread>
class Test {
public:
Test(int id) : m_id(id) {}
void showID() {
std::cout << m_id << std::endl;
}
private:
int m_id;
};
void thread1(Test* t) {
std::this_thread::sleep_for(std::chrono::seconds(2));
t->showID();
}
int main()
{
Test* t = new Test(10);
std::thread t1(thread1, t);
delete t; //仅用于假设t被析构了
std::cout << "delete t" << std::endl;
t1.join();
return 0;
}
在上面例子中,由于thread1等待了2s,此时,main线程早已经把t对象析构了。打印m_id,自然不能打印出10了。但是可以通过shared_ptr和weak_ptr来解决共享对象的线程安全问题。
#include <iostream>
#include <memory>
#include <thread>
class Test {
public:
Test(int id) : m_id(id) {}
~Test() {std::cout << "~Test" << std::endl;}
void showID() {
std::cout << "m_id: " << m_id << std::endl;
}
private:
int m_id;
};
void thread2(std::weak_ptr<Test> t) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::shared_ptr<Test> sp = t.lock();
if(sp)
sp->showID(); // 打印结果:2
else
std::cout << "sp null" << std::endl;
}
int main()
{
std::thread t2;
// {
std::shared_ptr<Test> sp = std::make_shared<Test>(2);
std::weak_ptr<Test> t(sp);
t2 = std::thread(thread2, t);
std::cout << "use_count: " << sp.use_count() << std::endl;
// }
t2.join();
return 0;
}
如果想访问对象的方法,先通过t的lock方法进行提升操作,把weak_ptr提升为shared_ptr强智能指针。提升过程中,是通过检测它所观察的强智能指针保存的Test对象的引用计数,来判定Test对象是否存活。sp如果为nullptr,说明Test对象已经析构,不能再访问;如果sp!=nullptr,则可以正常访问Test对象的方法。
如果main函数中设置了sp的作用域,当t2执行前sp智能指针离开作用域,被析构,进而把Test对象析构,此时showID方法已经不会被调用,因为在thread2方法中,提升到sp时,lock方法判定Test对象已经析构,就不会调用showID了!
带缓存功能的工厂函数
我们直接看下面的程序:
#include <iostream>
#include <unordered_map>
#include <memory>
using namespace std;
class Person {
public:
virtual ~Person() = default;
virtual void Action() = 0;
};
class Student : public Person {
public:
~Student() {
cout << "~Student() called....\n";
}
virtual void Action() override {
cout << "Student learning....\n";
}
};
class Engineer : public Person {
public:
~Engineer() {
cout << "~Engineer() called....\n";
}
virtual void Action() override {
cout << "Engineer coding....\n";
}
};
enum class InvestType {
INVEST_TYPE_STUDENT,
INVEST_TYPE_ENGINEER,
};
// 工厂函数
shared_ptr<Person> CreateFactory(InvestType type) {
// 自定义析构器, 这里以lambda表达式的形式给出
auto CustomDelete = [](Person *ptr) {
cout << "custom delete called...." << ptr << endl;
// 注意:ptr可能为空指针,比如默认为空,
// 然后调用reset赋值时,会先调用一遍析构
if (ptr) {
// TODO 自定义析构时想干的事
delete ptr;
}
};
// 待返回的指针, 初始化为空指针
shared_ptr<Person> sp(nullptr);
// 注意这里用reset来指定sp获取从new产生的对象的所有权, 不能用=赋值
switch (type) {
case InvestType::INVEST_TYPE_STUDENT:
// 注意:自定义析构器是随对象一起指定的,这里区别于unique_ptr
sp.reset(new Student, CustomDelete);
break;
case InvestType::INVEST_TYPE_ENGINEER:
// 如果不指定自定义析构器的话,则不会调用
sp.reset(new Engineer);
break;
}
// 返回智能指针
return sp;
}
// 带缓存的工厂函数
// 调用工厂函数CreateFactory成本高昂, 并且type会频繁的重复调用
shared_ptr<Person> FastCreateFactory(InvestType type)
{
// 定义一个做缓存的容器,注意这里存的内容是weak_ptr
// 使用weak_ptr的好处是,它不会影响所指涉对象的引用计数
// 如果这里改为shared_ptr的话,则函数外边永远不会析构掉这个对象,
// 因为缓存中至少保证其引用计数为1。这就背离的我们的设计
static unordered_map<InvestType, weak_ptr<Person>> s_cache;
// 将weak_ptr生成shared_ptr
auto sp = s_cache[type].lock();
// 如果缓存中没有的话,则调用工厂函数创建一个新对象,并且加入到缓存中去
if (!sp) {
cout << "create new person..\n";
sp = CreateFactory(type);
s_cache[type] = sp;
}
return sp;
}
int main() {
{
auto pInv = FastCreateFactory(InvestType::INVEST_TYPE_ENGINEER);
pInv->Action();
}
cout << "-------------------------------\n";
{
auto pInv = FastCreateFactory(InvestType::INVEST_TYPE_ENGINEER);
pInv->Action();
}
cout << "-------------------------------\n";
{
auto pInv = FastCreateFactory(InvestType::INVEST_TYPE_STUDENT);
pInv->Action();
auto pInv2 = FastCreateFactory(InvestType::INVEST_TYPE_STUDENT);
pInv2->Action();
}
}
由调用者去决定这些对象(工厂函数返回的对象)的生存期(std::shared_ptr做不到),然而函数内存的缓存管理器也需要一个指涉到这些对象的指针(std::unique_ptr做不到),并且缓存管理器的指针需要能够校验它们何时会空悬(裸指针做不到)。所以需要weak_ptr。
观察者设计模式问题
观察者模式就是,当观察者观察到某事件发生时,需要通知监听者进行事件处理的一种设计模式。
在多数实现中,观察者通常都在另一个独立的线程中,这就涉及到在多线程环境中,共享对象的线程安全问题(解决方法就是使用上文的智能指针)。这是因为在找到监听者并让它处理事件时,其实在多线程环境中,肯定不明确此时监听者对象是否还存活,或是已经在其它线程中被析构了,此时再去通知这样的监听者,肯定是有问题的。
也就是说,当观察者运行在独立的线程中时,在通知监听者处理该事件时,应该先判断监听者对象是否存活,如果监听者对象已经析构,那么不用通知,并且需要从map表中删除这样的监听者对象。其中的主要代码为:
#include <iostream>
#include <unordered_map>
#include <memory>
#include <list>
using namespace std;
class Listener {
public:
~Listener() {std::cout << "~Listener()" << std::endl;}
void Action(int id) {
std::cout << "msg id: " << id << std::endl;
}
};
// 存储监听者注册的感兴趣的事件
unordered_map<int, list<weak_ptr<Listener>>> listenerMap;
// 观察者观察到事件发生,转发到对该事件感兴趣的监听者
void dispatchMessage(int msgid) {
auto it = listenerMap.find(msgid);
if (it != listenerMap.end()) {
for (auto it1 = it->second.begin(); it1 != it->second.end(); ++it1) {
// 智能指针的提升操作,用来判断监听者对象是否存活
shared_ptr<Listener> ps = it1->lock();
// 监听者对象如果存活,才通知处理事件
if (ps != nullptr) {
std::cout << "handle msg: " << msgid << std::endl;
ps->Action(msgid);
} else {
// 监听者对象已经析构,从map中删除这样的监听者对象
std::cout << "rm msg: " << msgid << std::endl;
it1 = it->second.erase(it1);
}
}
} else {
std::cout << "not found msg: " << msgid << std::endl;
}
}
int main()
{
std::shared_ptr<Listener> sp = std::make_shared<Listener>();
weak_ptr<Listener> wp(sp);
list<weak_ptr<Listener>> lwp;
lwp.push_back(wp);
listenerMap[1] = lwp;
dispatchMessage(1);
}
总结
智能指针三兄弟就学习到这里啦,虽然它们性格各异,unque_ptr是独来独往,shared_ptr是左拥右抱,而weak_ptr生来就不是为了单打独斗,了解之后你会发现他总是和shared_ptr出双入对的。
最后,弱指针是另一种智能指针,它们不是直接指向资源,而是指向另一个指针(弱或共享)。弱指针无法直接访问对象,但是它们可以判断该对象是否仍然存在或是否已过期。弱指针可以临时转换为共享指针以访问指向的对象(前提是它仍然存在)。
std::weak_ptr在多线程环境中也是一个了不起的工具,因为它不拥有对象,因此不能阻止在其他线程中删除。而且std::weak_ptr::lock()是原子操作,多线程下是安全的。
参考: