7、weak_ptr详解:弱引用、解决循环引用、expired与lock用法、观察者模式
聊到 weak_ptr,我得先坦白一件事——刚学 C++11 那会儿,我总觉得这玩意儿有点鸡肋。你说它不能直接访问对象,又不像 shared_ptr 那样能自动释放资源,那它到底干嘛用的?
直到我在一个实际项目中,被循环引用坑得焦头烂额……嗯,从那以后,我再也不敢小看 weak_ptr 了。
什么是 weak_ptr?说白了就是“弱引用”
weak_ptr 是 C++11 引入的一种智能指针。它不像 shared_ptr 那样“强”着要拥有对象,而是只“弱弱”地观察着对象。
什么意思呢?
- 强引用(shared_ptr):对象只要还有强引用,就不会被销毁。
- 弱引用(weak_ptr):对象是否存活,跟它没关系。它只是“看着”对象,但不阻止对象被销毁。
我个人习惯把 weak_ptr 想象成一个“观察员”。它不参与资源管理,只负责告诉你:“嘿,那个对象还在不在?”
核心要点:weak_ptr 必须从 shared_ptr 构造,不能独立存在。它不会增加引用计数。
循环引用——weak_ptr 的“救火现场”
先说说循环引用是什么。你想想看,两个对象互相用 shared_ptr 指着对方,结果呢?
引用计数永远降不到 0,内存泄漏了。我在项目中遇到过这种场景:一个父节点持有子节点的 shared_ptr,子节点又持有父节点的 shared_ptr。析构函数死活不调用,内存蹭蹭往上涨。
来看个例子:
#include <memory>
#include <iostream>
struct Child;
struct Parent {
std::shared_ptr<Child> child;
~Parent() { std::cout << "Parent destroyed\n"; }
};
struct Child {
std::shared_ptr<Parent> parent; // 问题出在这里!
~Child() { std::cout << "Child destroyed\n"; }
};
int main() {
auto p = std::make_shared<Parent>();
auto c = std::make_shared<Child>();
p->child = c;
c->parent = p; // 互相引用,形成闭环
// 程序结束,两个对象都不会被销毁!
return 0;
}
运行结果?什么输出都没有。两个析构函数都没调用。这就是循环引用的典型症状。
警告:循环引用不会导致程序崩溃,但会导致内存泄漏。而且这种泄漏很难排查,因为引用计数看起来一切正常。
用 weak_ptr 打破循环
解决方案很简单:把其中一个 shared_ptr 换成 weak_ptr。
我一般建议把“子指向父”的指针改成弱引用。因为父节点通常管理子节点的生命周期,子节点只是“借用”一下父节点的信息。
struct Child {
std::weak_ptr<Parent> parent; // 改成弱引用
~Child() { std::cout << "Child destroyed\n"; }
};
int main() {
auto p = std::make_shared<Parent>();
auto c = std::make_shared<Child>();
p->child = c;
c->parent = p; // 现在没问题了
// 程序结束,两个对象都能正常销毁
return 0;
}
输出:
Child destroyed
Parent destroyed
你看,两个析构函数都调用了。问题解决。
expired 和 lock——weak_ptr 的两个核心操作
weak_ptr 不能直接访问对象。你得先问它:“对象还在吗?”
两个方法:
- expired():检查对象是否已被销毁。返回 true 表示对象没了。
- lock():尝试获取一个
shared_ptr。如果对象还在,返回有效的shared_ptr;否则返回空指针。
我习惯这样用:
void use_weak_ptr(std::weak_ptr<int> wp) {
if (auto sp = wp.lock()) {
// 对象还在,安全使用
std::cout << "Value: " << *sp << "\n";
} else {
// 对象已销毁
std::cout << "Object is gone\n";
}
}
这里有个细节:lock() 是原子操作。它检查对象是否存活,如果存活就增加引用计数,然后返回。这保证了你在使用对象期间,它不会被销毁。
小技巧:不要先调用 expired() 再调用 lock()。因为两者之间对象可能被销毁。直接用 lock() 判断返回值即可,更安全也更简洁。
观察者模式——weak_ptr 的经典应用
观察者模式里,主题(Subject)需要通知观察者(Observer)。但观察者可能随时被销毁。如果用 shared_ptr 持有观察者,就会阻止其销毁,造成资源泄漏。
用 weak_ptr 就完美了:
#include <vector>
#include <memory>
#include <iostream>
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void addObserver(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify() {
for (auto it = observers.begin(); it != observers.end(); ) {
if (auto sp = it->lock()) {
sp->update();
++it;
} else {
// 观察者已销毁,移除
it = observers.erase(it);
}
}
}
};
class ConcreteObserver : public Observer {
int id;
public:
ConcreteObserver(int i) : id(i) {}
void update() override {
std::cout << "Observer " << id << " notified\n";
}
};
int main() {
Subject subject;
auto obs1 = std::make_shared<ConcreteObserver>(1);
auto obs2 = std::make_shared<ConcreteObserver>(2);
subject.addObserver(obs1);
subject.addObserver(obs2);
subject.notify();
obs1.reset(); // 销毁 obs1
subject.notify(); // 只有 obs2 被通知
return 0;
}
输出:
Observer 1 notified
Observer 2 notified
Observer 2 notified
你看,obs1 销毁后,主题自动跳过了它。不需要手动解注册,省心多了。
一张图看懂 weak_ptr 的核心逻辑
避坑指南
我曾经在项目里犯过一个低级错误:用 weak_ptr 去管理一个栈上对象。结果 lock() 返回了空指针,排查了半天才发现问题。
记住:weak_ptr 只能配合 shared_ptr 使用。它不能指向栈对象、全局对象或 unique_ptr 管理的对象。
另外,别想着用 weak_ptr 来“提升”性能。它不会减少内存分配,也不会加快访问速度。它的唯一价值就是解决生命周期问题。
| 特性 | shared_ptr | weak_ptr |
|---|---|---|
| 引用计数影响 | 增加强引用计数 | 不增加 |
| 直接访问对象 | 可以 | 不可以(需 lock) |
| 防止对象销毁 | 是 | 否 |
| 典型用途 | 资源管理 | 观察、打破循环 |
嗯,关于 weak_ptr 就聊这么多。它虽然功能简单,但在实际项目中用好了,能省去不少麻烦。下次写代码遇到循环引用,记得试试它。