跳到主要内容

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创建:

cpp
#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

cpp
#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()方法来检查它所观察的对象是否已被释放:

cpp
#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相互引用时,这会导致即使对象不再被外部使用,它们的引用计数也永远不会降为零,从而造成内存泄漏。

下面是一个展示循环引用问题的例子:

cpp
#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来解决这个问题:

cpp
#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非常适合实现观察者模式,其中主题持有对观察者的弱引用,避免了内存泄漏问题:

cpp
#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适合用于实现缓存系统,可以在内存压力大时自动释放不再使用的资源:

cpp
#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_ptrreset()方法用于解除weak_ptr与被观察对象的关联:

cpp
#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_ptruse_count()方法返回与之共享对象的shared_ptr的数量:

cpp
#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++智能指针家族中的重要成员,它提供了一种观察共享对象而不影响其生命周期的方法。主要特点和用途包括:

  1. 解决shared_ptr循环引用导致的内存泄漏问题
  2. 实现观察者模式
  3. 实现缓存系统
  4. 安全地处理可能被释放的对象

使用weak_ptr时,需要记住以下要点:

  • weak_ptr不能单独使用,需要配合shared_ptr
  • 访问weak_ptr指向的对象时,必须先使用lock()转换为shared_ptr
  • 始终检查expired()或通过lock()返回的shared_ptr是否为空

通过合理使用weak_ptr,可以编写更安全、更健壮的C++代码,有效避免内存泄漏和悬空指针问题。

练习

  1. 实现一个简单的对象池,使用weak_ptr跟踪外部对象引用,当对象不再被使用时自动回收到池中。

  2. 修改以下代码,解决潜在的循环引用问题:

cpp
class Parent;
class Child;

class Parent {
public:
std::shared_ptr<Child> child;
};

class Child {
public:
std::shared_ptr<Parent> parent;
};
  1. 设计一个事件系统,使用weak_ptr实现事件监听器,确保已被销毁的监听器不会收到事件通知。

进一步阅读

  • C++标准库中的std::weak_ptr文档
  • Scott Meyers的《Effective Modern C++》中关于智能指针的章节
  • Herb Sutter的《Exceptional C++》系列书籍