C++ weak_ptr
简介
在C++内存管理中,智能指针是一种自动管理内存的工具,帮助开发者避免内存泄漏。std::weak_ptr
是C++11引入的一种特殊智能指针,它与std::shared_ptr
密切相关,但解决了shared_ptr
可能面临的循环引用问题。
weak_ptr
本质上是shared_ptr
的一个观察者,它可以观察和访问shared_ptr
所管理的对象,但不会增加对象的引用计数。这使得weak_ptr
成为解决循环引用问题的理想工具。
weak_ptr的基本用法
创建和初始化
weak_ptr
不能直接管理对象,它必须从一个shared_ptr
创建:
#include <iostream>
#include <memory>
int main() {
// 创建一个shared_ptr
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
// 从shared_ptr创建weak_ptr
std::weak_ptr<int> weakPtr = sharedPtr;
std::cout << "shared_ptr引用计数: " << sharedPtr.use_count() << std::endl;
return 0;
}
输出:
shared_ptr引用计数: 1
注意,创建weak_ptr
后,shared_ptr
的引用计数并没有增加。
访问对象
由于weak_ptr
不直接控制对象的生命周期,当我们想要访问weak_ptr
指向的对象时,需要先将其转换为shared_ptr
:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
// 检查weak_ptr是否过期
if (!weakPtr.expired()) {
// 从weak_ptr创建shared_ptr
std::shared_ptr<int> sharedPtr2 = weakPtr.lock();
if (sharedPtr2) {
std::cout << "值: " << *sharedPtr2 << std::endl;
std::cout << "shared_ptr引用计数: " << sharedPtr2.use_count() << std::endl;
}
}
return 0;
}
输出:
值: 42
shared_ptr引用计数: 2
这里,我们使用lock()
方法从weak_ptr
获取一个新的shared_ptr
。如果原始的shared_ptr
已经释放了对象,lock()
会返回一个空的shared_ptr
。
检查weak_ptr的状态
weak_ptr
提供了expired()
方法来检查它所观察的对象是否已被释放:
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> weakPtr;
{
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
weakPtr = sharedPtr;
std::cout << "在shared_ptr作用域内,weak_ptr过期? "
<< (weakPtr.expired() ? "是" : "否") << std::endl;
}
// 此时sharedPtr已销毁
std::cout << "在shared_ptr作用域外,weak_ptr过期? "
<< (weakPtr.expired() ? "是" : "否") << std::endl;
return 0;
}
输出:
在shared_ptr作用域内,weak_ptr过期? 否
在shared_ptr作用域外,weak_ptr过期? 是
解决循环引用问题
weak_ptr
最重要的用途是解决shared_ptr
循环引用导致的内存泄漏问题。
什么是循环引用?
循环引用发生在两个或多个对象通过shared_ptr
相互引用时,这会导致即使对象不再被外部使用,它们的引用计数也永远不会降为零,从而造成内存泄漏。
下面是一个展示循环引用问题的例子:
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
A() { std::cout << "A 构造" << std::endl; }
~A() { std::cout << "A 析构" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
B() { std::cout << "B 构造" << std::endl; }
~B() { std::cout << "B 析构" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // a引用b
b->a_ptr = a; // b引用a
std::cout << "a的引用计数: " << a.use_count() << std::endl;
std::cout << "b的引用计数: " << b.use_count() << std::endl;
}
std::cout << "主函数结束" << std::endl;
return 0;
}
输出:
A 构造
B 构造
a的引用计数: 2
b的引用计数: 2
主函数结束
注意,程序结束时,A和B的析构函数并没有被调用,这表明对象没有被释放,存在内存泄漏。
使用weak_ptr解决循环引用
现在,我们用weak_ptr
来解决这个问题:
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
A() { std::cout << "A 构造" << std::endl; }
~A() { std::cout << "A 析构" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 改为weak_ptr
B() { std::cout << "B 构造" << std::endl; }
~B() { std::cout << "B 析构" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // a引用b
b->a_ptr = a; // b观察a
std::cout << "a的引用计数: " << a.use_count() << std::endl;
std::cout << "b的引用计数: " << b.use_count() << std::endl;
}
std::cout << "主函数结束" << std::endl;
return 0;
}
输出:
A 构造
B 构造
a的引用计数: 1
b的引用计数: 2
B 析构
A 析构
主函数结束
通过将B类中的std::shared_ptr<A>
改为std::weak_ptr<A>
,我们打破了循环引用,使A和B对象能够在作用域结束时正确释放。
实际应用场景
观察者模式
weak_ptr
非常适合实现观察者模式,其中主题持有对观察者的弱引用,避免了内存泄漏问题:
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
// 前向声明
class Observer;
// 主题类
class Subject {
public:
void addObserver(std::shared_ptr<Observer> observer) {
// 存储weak_ptr而不是shared_ptr
observers.push_back(observer);
}
void removeObserver(std::shared_ptr<Observer> observer);
void notifyObservers();
private:
std::vector<std::weak_ptr<Observer>> observers;
};
// 观察者类
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
// 具体观察者
class ConcreteObserver : public Observer {
public:
ConcreteObserver(int id) : id_(id) {}
void update() override {
std::cout << "观察者 " << id_ << " 收到更新通知!" << std::endl;
}
~ConcreteObserver() {
std::cout << "观察者 " << id_ << " 销毁" << std::endl;
}
private:
int id_;
};
void Subject::removeObserver(std::shared_ptr<Observer> observer) {
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[observer](const std::weak_ptr<Observer>& weakObs) {
return weakObs.expired() || weakObs.lock() == observer;
}),
observers.end());
}
void Subject::notifyObservers() {
// 移除已过期的观察者
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[](const std::weak_ptr<Observer>& weakObs) {
return weakObs.expired();
}),
observers.end());
// 通知所有活跃的观察者
for (auto& weakObs : observers) {
if (auto obs = weakObs.lock()) {
obs->update();
}
}
}
int main() {
Subject subject;
{
auto observer1 = std::make_shared<ConcreteObserver>(1);
auto observer2 = std::make_shared<ConcreteObserver>(2);
subject.addObserver(observer1);
subject.addObserver(observer2);
std::cout << "通知所有观察者:" << std::endl;
subject.notifyObservers();
// observer1将在这里被销毁
}
std::cout << "再次通知观察者:" << std::endl;
subject.notifyObservers(); // 只有observer2会收到通知
return 0;
}
输出:
通知所有观察者:
观察者 1 收到更新通知!
观察者 2 收到更新通知!
观察者 1 销毁
再次通知观察者:
观察者 2 收到更新通知!
观察者 2 销毁
缓存实现
weak_ptr
适合用于实现缓存系统,可以在内存压力大时自动释放不再使用的资源:
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
// 简单的缓存实现
class Cache {
public:
std::shared_ptr<int> getData(const std::string& key) {
auto it = cache.find(key);
if (it != cache.end()) {
// 尝试从weak_ptr获取shared_ptr
if (auto data = it->second.lock()) {
std::cout << "缓存命中: " << key << std::endl;
return data;
} else {
std::cout << "缓存失效: " << key << std::endl;
}
}
// 缓存未命中,创建新数据
std::cout << "缓存未命中,创建新数据: " << key << std::endl;
auto data = std::make_shared<int>(key.length()); // 示例数据
cache[key] = data;
return data;
}
private:
std::unordered_map<std::string, std::weak_ptr<int>> cache;
};
int main() {
Cache cache;
{
std::cout << "首次访问 'key1'" << std::endl;
auto data1 = cache.getData("key1");
std::cout << "data1 = " << *data1 << std::endl;
std::cout << "\n再次访问 'key1'" << std::endl;
auto data2 = cache.getData("key1");
std::cout << "data2 = " << *data2 << std::endl;
} // data1和data2在这里超出作用域并被销毁
std::cout << "\n在数据被销毁后访问 'key1'" << std::endl;
auto data3 = cache.getData("key1");
std::cout << "data3 = " << *data3 << std::endl;
return 0;
}
输出:
首次访问 'key1'
缓存未命中,创建新数据: key1
data1 = 4
再次访问 'key1'
缓存命中: key1
data2 = 4
在数据被销毁后访问 'key1'
缓存失效: key1
缓存未命中,创建新数据: key1
data3 = 4
weak_ptr的其他功能
reset()方法
weak_ptr
的reset()
方法用于解除weak_ptr
与被观察对象的关联:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
std::cout << "weakPtr过期? " << (weakPtr.expired() ? "是" : "否") << std::endl;
weakPtr.reset();
std::cout << "reset()后weakPtr过期? " << (weakPtr.expired() ? "是" : "否") << std::endl;
return 0;
}
输出:
weakPtr过期? 否
reset()后weakPtr过期? 是
use_count()方法
weak_ptr
的use_count()
方法返回与之共享对象的shared_ptr
的数量:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr1;
std::cout << "use_count: " << weakPtr.use_count() << std::endl;
{
std::shared_ptr<int> sharedPtr2 = sharedPtr1;
std::cout << "作用域内 use_count: " << weakPtr.use_count() << std::endl;
}
std::cout << "作用域外 use_count: " << weakPtr.use_count() << std::endl;
return 0;
}
输出:
use_count: 1
作用域内 use_count: 2
作用域外 use_count: 1
性能考虑
虽然weak_ptr
是解决循环引用的有效工具,但它也带来一些性能开销。weak_ptr
需要额外的控制块来跟踪对象的生命周期,调用lock()
方法需要原子操作,可能在高性能场景下造成瓶颈。
在性能敏感的代码中,可以考虑其他替代方案,如使用裸指针配合清晰的所有权语义,或使用更轻量级的解决方案。
总结
std::weak_ptr
是C++智能指针家族中的重要成员,它提供了一种观察共享对象而不影响其生命周期的方法。主要特点和用途包括:
- 解决
shared_ptr
循环引用导致的内存泄漏问题 - 实现观察者模式
- 实现缓存系统
- 安全地处理可能被释放的对象
使用weak_ptr
时,需要记住以下要点:
weak_ptr
不能单独使用,需要配合shared_ptr
- 访问
weak_ptr
指向的对象时,必须先使用lock()
转换为shared_ptr
- 始终检查
expired()
或通过lock()
返回的shared_ptr
是否为空
通过合理使用weak_ptr
,可以编写更安全、更健壮的C++代码,有效避免内存泄漏和悬空指针问题。
练习
-
实现一个简单的对象池,使用
weak_ptr
跟踪外部对象引用,当对象不再被使用时自动回收到池中。 -
修改以下代码,解决潜在的循环引用问题:
class Parent;
class Child;
class Parent {
public:
std::shared_ptr<Child> child;
};
class Child {
public:
std::shared_ptr<Parent> parent;
};
- 设计一个事件系统,使用
weak_ptr
实现事件监听器,确保已被销毁的监听器不会收到事件通知。
进一步阅读
- C++标准库中的
std::weak_ptr
文档 - Scott Meyers的《Effective Modern C++》中关于智能指针的章节
- Herb Sutter的《Exceptional C++》系列书籍