正式介绍循环引用问题之前,我们必须先要掌握weak_ptr。
1 std::weak_ptr基本概念
std::weak_ptr
是C++标准库中的一种智能指针,它是为了解决std::shared_ptr
可能产生的循环引用问题而设计的。与std::shared_ptr
不同,std::weak_ptr
对对象的引用不会增加对象的引用计数,也不会影响对象的生命周期。这意味着,当最后一个std::shared_ptr
指向的对象被销毁时,任何指向该对象的std::weak_ptr
都会自动变为空。
std::weak_ptr
的主要操作和成员函数:
操作/函数 | 描述 |
---|---|
构造函数 | |
weak_ptr() | 构造一个空的 weak_ptr ,不指向任何对象。 |
weak_ptr(const weak_ptr& other) | 拷贝构造函数,构造一个新的 weak_ptr ,与 other 共享对象所有权状态。 |
weak_ptr(const shared_ptr<T>& sp) | 从 shared_ptr 构造 weak_ptr ,共享对象所有权状态但不增加引用计数。 |
成员函数 | |
shared_ptr<T> lock() const | 尝试获取所指向对象的 shared_ptr 。如果对象仍然存在,则返回一个有效的 shared_ptr ;否则返回一个空的 shared_ptr 。 |
bool expired() const | 检查 weak_ptr 是否已经过期(即它指向的对象是否已经被销毁)。如果已过期,则返回 true ;否则返回 false 。 |
long use_count() const | 返回与 weak_ptr 共享对象所有权的 shared_ptr 的数量(即引用计数)。注意:这个函数在标准库中并不存在,但经常被提及作为一个假设性或扩展性的功能。实际上,标准库并没有提供直接获取引用计数的函数。 |
void reset() | 重置 weak_ptr ,使其变为空,不再指向任何对象。 |
void swap(weak_ptr& other) | 交换两个 weak_ptr 对象的所有权状态。 |
操作符 | |
weak_ptr& operator=(const weak_ptr& other) | 赋值操作符,将 other 的所有权状态赋给当前 weak_ptr 。 |
weak_ptr& operator=(const shared_ptr<T>& sp) | 赋值操作符,从 shared_ptr 赋值,共享对象所有权状态但不增加引用计数。 |
注意,
-
use_count()
函数在 C++ 标准库中并不存在。这是因为直接暴露引用计数可能会导致不安全的代码实践。通常,使用expired()
或lock()
函数来检查对象是否存在是更安全、更推荐的方式。 -
weak_ptr
没有提供直接访问所指向对象的成员函数或数据成员的操作符(如->
或*
)。要访问对象,必须先通过lock()
获取一个有效的shared_ptr
,然后使用该shared_ptr
进行访问。如果对象已经不存在,lock()
将返回一个空的shared_ptr
,访问将无法进行。
std::weak_ptr
基本用法,代码示例:
#include <iostream>
#include <memory>
class Example
{
public:
~Example()
{
std::cout << "Example::~Example() called." << std::endl;
}
};
int main()
{
std::shared_ptr<Example> sp1 = std::make_shared<Example>(); // 创建一个 shared_ptr.
{
// 从 sp1 创建一个 weak_ptr.
std::weak_ptr<Example> wp = sp1;
std::cout << "wp points to " << (wp.expired() ? "null" : "an object") << std::endl;
// 通过 weak_ptr 访问对象.
// 必须先将其转换为 shared_ptr. 如果原 shared_ptr 不再存在,
// 那么这个调用将返回一个新的空的 shared_ptr.
if (auto sp2 = wp.lock())
{
std::cout << "sp2 points to " << (sp2.get() ? "an object" : "null") << std::endl;
}
else
{
std::cout << "sp2 is null" << std::endl;
}
}
std::cout << "sp1 points to " << (sp1.get() ? "an object" : "null") << std::endl;
std::cout << "sp1 use_count() = " << sp1.use_count() << std::endl;
return 0;
}
综合案例:实现了一个简单的观察者模式。
#include <iostream>
#include <list>
#include <memory>
// 观察者接口
class Observer
{
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
// 具体观察者
class ConcreteObserver : public Observer
{
public:
void update() override
{
std::cout << "ConcreteObserver::update() called" << std::endl;
}
};
// 主题接口
class Subject
{
public:
virtual void registerObserver(std::shared_ptr<Observer> observer) = 0;
virtual void removeObserver(std::shared_ptr<Observer> observer) = 0;
virtual void notifyObservers() = 0;
virtual ~Subject() = default;
};
// 具体主题
class ConcreteSubject : public Subject
{
private:
std::list<std::weak_ptr<Observer>> observers;
public:
void registerObserver(std::shared_ptr<Observer> observer) override
{
observers.push_back(observer);
}
void removeObserver(std::shared_ptr<Observer> observer) override
{
observers.remove_if([observer](const std::weak_ptr<Observer>& wp) {
return !wp.expired() && wp.lock() == observer;
});
}
void notifyObservers() override {
for (auto it = observers.begin(); it != observers.end();)
{
if (auto observer = it->lock())
{
observer->update();
++it;
} else
{
it = observers.erase(it);
}
}
}
};
int main()
{
std::shared_ptr<ConcreteSubject> subject = std::make_shared<ConcreteSubject>();
std::shared_ptr<Observer> observer1 = std::make_shared<ConcreteObserver>();
std::shared_ptr<Observer> observer2 = std::make_shared<ConcreteObserver>();
subject->registerObserver(observer1);
subject->registerObserver(observer2);
subject->notifyObservers();
// 释放 observer1,但不影响 subject 和 observer2
observer1.reset();
subject->notifyObservers(); // observer1 不再接收通知
return 0;
}
2 循环引用问题
循环引用问题出现在两个或多个对象相互使用std::shared_ptr
来引用对方时。这种情况下,每个对象的引用计数都至少为1(因为被另一个对象引用),所以即使外部的所有std::shared_ptr
都不再指向这些对象,它们的引用计数也永远不会下降到0。这意味着这些对象将永远不会被销毁,从而导致内存泄漏。
例如,考虑两个类A和B,它们各自包含一个指向对方的std::shared_ptr
成员。如果A的一个实例引用B的一个实例,而B的那个实例又引用A的那个实例,就形成了一个循环引用。即使没有其他代码引用这两个对象,它们的引用计数也永远不会是0,因此它们所占用的内存将不会被释放。
为了解决这个问题,C++提供了std::weak_ptr
。std::weak_ptr
是一种不增加引用计数的智能指针,它允许你观察(但不能控制)一个由std::shared_ptr
管理的对象的生命周期。当最后一个std::shared_ptr
被销毁时,对象将被删除,而任何指向该对象的std::weak_ptr
都将自动变为空。通过使用std::weak_ptr
来替代其中一个方向上的std::shared_ptr
,可以打破循环引用,从而允许对象在不再需要时被正确销毁。
#include <iostream>
#include <memory>
#include <string>
// 前向声明
class Child;
class Parent
{
public:
std::shared_ptr<Child> m_child_ptr; // 指向Child对象指针
// 析构函数
~Parent()
{
std::cout << "调用Parent析构函数" << std::endl;
}
};
class Child
{
public:
std::shared_ptr<Parent> m_parent_ptr; // 指向父类对象指针
// 析构函数
~Child()
{
std::cout << "调用Child析构函数" << std::endl;
}
};
int main()
{
// 创建Parent和Child对象指针
std::shared_ptr<Parent> psptr = std::make_shared<Parent>();
std::shared_ptr<Child> csptr = std::make_shared<Child>();
// 核心目的使其成环
// 设置父类对象里的child指针指向子类
psptr->m_child_ptr = csptr;
// 设置子类对象里的parent指针指向父类
csptr->m_parent_ptr = psptr;
std::cout << "超出作用域,注意看各个对象的析构函数打印信息" << std::endl;
return 0;
}
解决方案,至少一个对象采用std::weak_ptr
#include <iostream>
#include <memory>
#include <string>
// 前向声明
class Child;
class Parent
{
public:
std::weak_ptr<Child> m_child_ptr; // 指向Child对象指针
// 析构函数
~Parent()
{
std::cout << "调用Parent析构函数" << std::endl;
}
};
class Child
{
public:
std::weak_ptr<Parent> m_parent_ptr; // 指向父类对象指针
// 析构函数
~Child()
{
std::cout << "调用Child析构函数" << std::endl;
}
};
int main()
{
// 创建Parent和Child对象指针
std::shared_ptr<Parent> psptr = std::make_shared<Parent>();
std::shared_ptr<Child> csptr = std::make_shared<Child>();
// 核心目的使其成环
// 设置父类对象里的child指针指向子类
psptr->m_child_ptr = csptr;
// 设置子类对象里的parent指针指向父类
csptr->m_parent_ptr = psptr;
std::cout << "超出作用域,注意看各个对象的析构函数打印信息" << std::endl;
return 0;
}