C++ 智能指针 std::weak_ptr 解决观察者模式
观察者模式是软件开发中最常用的行为型设计模式之一,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都能收到通知并自动更新。然而,在 C++ 中实现观察者模式时,内存管理往往是最棘手的问题。如果处理不当,会导致悬垂指针、内存泄漏或重复删除等严重 bug。std::weak_ptr 正是解决这些问题的利器。
1. 观察者模式的内存困境
1.1 经典实现的问题
在经典的观察者模式实现中,被观察者(Subject)通常持有观察者(Observer)的原始指针。这种设计看似简单,却隐藏着巨大的风险。
#include <iostream>
#include <vector>
// 观察者基类
class Observer {
public:
virtual void update(int value) = 0;
virtual ~Observer() = default;
};
// 被观察者
class Subject {
private:
std::vector<Observer*> observers_;
int value_;
public:
void attach(Observer* obs) {
observers_.push_back(obs);
}
void detach(Observer* obs) {
// 复杂的清理逻辑
observers_.erase(
std::remove(observers_.begin(), observers_.end(), obs),
observers_.end()
);
}
void setValue(int v) {
value_ = v;
notify();
}
void notify() {
for (auto* obs : observers_) {
obs->update(value_); // 悬垂指针风险!
}
}
};
考虑这样一个场景:某个 Observer 对象被外部代码销毁,但 Subject 仍然持有指向它的原始指针。当 notify() 被调用时,程序就会访问已经释放的内存,导致未定义行为。更糟糕的是,这种崩溃往往是间歇性的,难以调试。
1.2 常见的错误应对方式
面对这个问题,很多开发者会尝试以下方法,但它们都有明显的缺陷:
| 方案 | 问题 |
|---|---|
依赖外部正确调用 detach() |
脆弱的 API 设计,容易遗漏 |
使用 std::shared_ptr 替代原始指针 |
可能导致循环引用,两个对象都无法释放 |
在 notify() 中捕获异常 |
无法根本解决问题,只是不让程序立即崩溃 |
std::weak_ptr 正是为这种场景设计的。它允许你观察一个被 std::shared_ptr 管理对象,而不拥有其生命周期。
2. std::weak_ptr 的核心原理
在深入解决方案之前,需要先理解 std::weak_ptr 的工作原理。
2.1 从 std::shared_ptr 说起
std::shared_ptr 通过引用计数管理对象的生命周期。每当一个新的 shared_ptr 指向对象,引用计数增加;当 shared_ptr 被销毁或离开作用域,引用计数减少。当引用计数归零时,对象被自动销毁。
void sharedPtrDemo() {
auto ptr = std::make_shared<int>(42); // 引用计数 = 1
{
auto copy = ptr; // 引用计数 = 2
std::cout << *copy << std::endl;
} // copy 销毁,引用计数 = 1
// ptr 销毁,引用计数 = 0,对象自动释放
}
2.2 weak_ptr 的特殊性
std::weak_ptr 并不参与引用计数的增减。它就像一个"观察者",只能从 std::shared_ptr 创建,用来检查对象是否仍然存活。
void weakPtrDemo() {
auto shared = std::make_shared<int>(100);
std::weak_ptr<int> weak = shared; // 不增加引用计数
std::cout << "对象存在: " << weak.expired() << std::endl; // false
shared.reset(); // 对象被销毁
std::cout << "对象已销毁: " << weak.expired() << std::endl; // true
// 安全地访问对象
if (auto locked = weak.lock()) {
std::cout << *locked << std::endl;
} else {
std::cout << "对象已不存在" << std::endl;
}
}
这段代码的关键在于 lock() 方法:如果对象仍然存在,它返回一个有效的 std::shared_ptr;如果对象已被销毁,它返回空的 shared_ptr。这种非侵入式的检查方式,完美解决了观察者模式中的悬垂指针问题。
3. 用 weak_ptr 重构观察者模式
3.1 改造 Observer 和 Subject
现在将 std::weak_ptr 应用到观察者模式中。核心思路是:Subject 持有 Observer 的 weak_ptr,而不是原始指针或 shared_ptr。
#include <iostream>
#include <vector>
#include <memory>
class Observer : public std::enable_shared_from_this<Observer> {
public:
virtual ~Observer() = default;
// 必须在堆上创建才能使用 shared_from_this()
static std::shared_ptr<Observer> create() {
return std::shared_ptr<Observer>(new Observer());
}
// 让调用者返回 shared_ptr
std::shared_ptr<Observer> getShared() {
return shared_from_this();
}
virtual void update(int value) = 0;
};
// 具体的观察者实现
class ConcreteObserver : public Observer {
private:
std::string name_;
public:
explicit ConcreteObserver(const std::string& name) : name_(name) {}
void update(int value) override {
std::cout << "Observer [" << name_ << "] received: " << value << std::endl;
}
};
这里引入了 std::enable_shared_from_this,这是一个重要的辅助类。它允许对象安全地获取指向自己的 shared_ptr。注意,对象必须通过 shared_ptr 管理,不能在栈上创建,否则调用 shared_from_this() 会抛出异常。
3.2 Subject 的实现
class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers_;
int value_;
public:
void attach(const std::shared_ptr<Observer>& obs) {
// 将 shared_ptr 转换为 weak_ptr,不增加引用计数
observers_.push_back(obs);
}
void detach(const std::shared_ptr<Observer>& obs) {
// 移除所有指向该观察者的 weak_ptr
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[&obs](const std::weak_ptr<Observer>& wp) {
auto sp = wp.lock();
return !sp || sp.get() == obs.get();
}),
observers_.end()
);
}
void setValue(int v) {
value_ = v;
notify();
}
void notify() {
// 使用迭代器安全删除已失效的观察者
for (auto it = observers_.begin(); it != observers_.end(); ) {
auto obs = it->lock(); // 尝试获取 shared_ptr
if (obs) {
obs->update(value_); // 对象存活,安全调用
++it;
} else {
// 对象已被销毁,清理无效的 weak_ptr
it = observers_.erase(it);
}
}
}
size_t observerCount() const {
size_t valid = 0;
for (const auto& wp : observers_) {
if (!wp.expired()) {
++valid;
}
}
return valid;
}
};
这个实现解决了所有内存管理问题:weak_ptr 不会增加引用计数,观察者的生命周期完全由外部控制。当观察者被销毁后,notify() 会在遍历时自动检测并清理无效的 weak_ptr,整个过程是自清洁的。
3.3 完整使用示例
int main() {
Subject subject;
// 创建观察者(必须在堆上)
auto observer1 = ConcreteObserver::create();
auto observer2 = ConcreteObserver::create();
auto observer3 = ConcreteObserver::create();
// 注册观察者
subject.attach(observer1);
subject.attach(observer2);
subject.attach(observer3);
std::cout << "初始观察者数量: " << subject.observerCount() << std::endl;
// 修改值,触发通知
subject.setValue(10);
// 销毁 observer2
observer2.reset();
std::cout << "observer2 销毁后: " << subject.observerCount() << std::endl;
// 再次通知,observer2 不会收到(因为已被清理)
subject.setValue(20);
// 销毁所有观察者
observer1.reset();
observer3.reset();
std::cout << "所有观察者销毁后: " << subject.observerCount() << std::endl;
return 0;
}
输出结果:
初始观察者数量: 3
Observer [observer1] received: 10
Observer [observer2] received: 10
Observer [observer3] received: 10
observer2 销毁后: 2
Observer [observer1] received: 20
Observer [observer3] received: 20
所有观察者销毁后: 0
可以看到,observer2 被销毁后,Subject 自动检测到这一点并清理了无效的引用,后续的 notify() 不会对已销毁的对象进行任何操作。
4. 更复杂的场景:嵌套观察者
在某些业务场景中,观察者本身可能也持有 Subject 的引用,形成双向依赖。std::weak_ptr 同样可以优雅地解决这个问题。
class Subject;
class NestedObserver : public Observer {
private:
std::weak_ptr<Subject> subject_; // 弱引用,避免循环引用
std::string name_;
public:
NestedObserver(const std::string& name, std::shared_ptr<Subject> sub)
: name_(name), subject_(sub) {}
void update(int value) override {
auto sub = subject_.lock();
if (sub) {
std::cout << "[" << name_ << "] 观察到变化: " << value;
std::cout << ",Subject 状态: " << sub->getValue() << std::endl;
} else {
std::cout << "[" << name_ << "] 观察到 Subject 已被销毁" << std::endl;
}
}
};
class Subject : public std::enable_shared_from_this<Subject> {
private:
std::vector<std::weak_ptr<Observer>> observers_;
int value_;
public:
static std::shared_ptr<Subject> create() {
return std::shared_ptr<Subject>(new Subject());
}
void attach(const std::shared_ptr<Observer>& obs) {
observers_.push_back(obs);
}
void setValue(int v) {
value_ = v;
notify();
}
int getValue() const { return value_; }
private:
void notify() {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto obs = it->lock()) {
obs->update(value_);
++it;
} else {
it = observers_.erase(it);
}
}
}
Subject() : value_(0) {} // 私有构造函数
};
这种双向依赖结构如果使用原始指针或 shared_ptr,很容易形成循环引用(A 持有 B 的 shared_ptr,B 又持有 A 的 shared_ptr),导致两个对象都无法被销毁。使用 std::weak_ptr 作为一侧的引用,完美打破了循环,同时保持了观察的功能。
5. 最佳实践与注意事项
5.1 强制使用堆分配
由于 std::enable_shared_from_this 的要求,观察者对象必须在堆上分配,不能在栈上创建。如果在栈上创建,调用 shared_from_this() 会抛出 std::bad_weak_ptr 异常。
// 错误:栈对象
ConcreteObserver obs("bad");
subject.attach(obs); // 不能工作!
// 正确:堆对象
auto obs = ConcreteObserver::create();
subject.attach(obs);
5.2 detach 的简化
在 weak_ptr 方案中,detach() 方法不再是必需的。因为 notify() 每次都会自动清理无效的观察者,你可以选择不提供 detach() 接口,简化 API。
class Subject {
public:
// 只需要 attach,不再需要 detach
void attach(const std::shared_ptr<Observer>& obs) {
observers_.push_back(obs);
}
void notify() {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto obs = it->lock()) {
obs->update(value_);
++it;
} else {
it = observers_.erase(it);
}
}
}
};
5.3 性能考量
每次 notify() 调用时遍历整个观察者列表并调用 lock(),会带来一定的性能开销。对于高性能要求的场景,可以采用以下优化策略:
| 策略 | 适用场景 |
|---|---|
延迟清理:只在 attach() 时检查并清理无效引用 |
观察者数量大,notify() 调用频繁 |
双重结构:同时维护 weak_ptr 列表和有效计数器 |
需要快速获取观察者数量 |
| 分代清理:每隔 N 次通知才执行完整清理 | notify() 极其频繁 |
一个简单的延迟清理实现:
class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers_;
int value_;
int notifyCount_ = 0;
void cleanExpired() {
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[](const std::weak_ptr<Observer>& wp) { return wp.expired(); }),
observers_.end()
);
}
public:
void attach(const std::shared_ptr<Observer>& obs) {
observers_.push_back(obs);
cleanExpired(); // 只在添加新观察者时清理
}
void notify() {
++notifyCount_;
for (auto& wp : observers_) {
if (auto obs = wp.lock()) {
obs->update(value_);
}
}
// 每 100 次通知清理一次
if (notifyCount_ % 100 == 0) {
cleanExpired();
}
}
};
6. 总结
std::weak_ptr 为 C++ 观察者模式提供了优雅而安全的内存管理方案。它的核心优势体现在三个方面:
-
防止悬垂指针:通过
lock()检查对象是否存活,确保永远不访问已释放的内存。 -
打破循环引用:观察者可以安全地持有 Subject 的引用,而不会导致内存泄漏。
-
自清洁机制:无效的观察者引用会在
notify()过程中自动清理,无需外部干预。
在实际项目中,建议将 std::enable_shared_from_this 作为 Observer 基类的默认继承,并在 API 设计中强制要求通过工厂函数创建观察者对象,以确保内存安全。

暂无评论,快来抢沙发吧!