什么是 ‘Circular Dependency’?利用 `std::weak_ptr` 破解智能指针内存泄漏的物理路径

各位编程爱好者,大家好!

今天我们将深入探讨一个在现代C++编程中至关重要的话题:循环依赖(Circular Dependency),以及如何利用C++11引入的智能指针家族中的一员——std::weak_ptr,来彻底破解由循环依赖导致的智能指针内存泄漏问题。这不仅仅是一个理论概念,更是我们在构建复杂、健壮系统时必须面对和解决的实际挑战。

1. 什么是循环依赖?

在软件工程中,循环依赖是指两个或多个模块、组件、类或对象彼此之间形成一个闭环的相互依赖关系。简单来说,A依赖B,B依赖C,而C又反过来依赖A。或者更直接地,A依赖B,B又依赖A。这种关系本身并非总是错误的,但在某些特定的资源管理场景下,它会导致严重的问题,尤其是与自动资源管理机制(如智能指针)结合时。

让我们以对象之间的所有权关系为例:

  • 对象A“拥有”对象B。
  • 对象B“拥有”对象A。

在人类社会中,这可能意味着一种互惠互利的关系。但在计算机内存管理的世界里,当“拥有”等同于“阻止被销毁”时,这种相互拥有就会形成一个死锁:A在等待B被销毁后才销毁自己,而B也在等待A被销毁后才销毁自己。结果是,两者都永远无法被销毁,即便它们已经不再被其他任何外部实体所使用。这就是我们所说的内存泄漏

2. std::shared_ptr 的强大与陷阱

在C++中,std::shared_ptr 是一个强大的工具,它通过引用计数(reference counting)机制实现了共享所有权(shared ownership)。每当一个新的 std::shared_ptr 指向同一个对象时,该对象的引用计数就会增加;当一个 std::shared_ptr 被销毁或重新指向另一个对象时,引用计数就会减少。当引用计数降为零时,std::shared_ptr 会自动删除所管理的对象,从而防止内存泄漏。

然而,std::shared_ptr 的引用计数机制在面对循环依赖时,恰恰成为了内存泄漏的温床。

2.1 案例分析:父子关系中的循环依赖

考虑一个经典的父子(Parent-Child)关系模型。一个父对象可以有多个子对象,而每个子对象也需要知道它的父对象是谁。如果我们将这种关系都建模为 std::shared_ptr 的共享所有权,就会出现问题。

假设我们有两个类:ParentChild

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// 前向声明,因为Child类会引用Parent类
class Parent; 

class Child {
public:
    std::string name;
    // 子对象拥有指向父对象的共享指针
    std::shared_ptr<Parent> parent; 

    Child(const std::string& n) : name(n) {
        std::cout << "Child " << name << " constructor called." << std::endl;
    }

    ~Child() {
        std::cout << "Child " << name << " destructor called." << std::endl;
    }
};

class Parent {
public:
    std::string name;
    // 父对象拥有指向子对象的共享指针集合
    std::vector<std::shared_ptr<Child>> children; 

    Parent(const std::string& n) : name(n) {
        std::cout << "Parent " << name << " constructor called." << std::endl;
    }

    ~Parent() {
        std::cout << "Parent " << name << " destructor called." << std::endl;
    }

    void addChild(std::shared_ptr<Child> child) {
        children.push_back(child);
        // 设置子对象的父指针
        child->parent = std::shared_ptr<Parent>(this); // 错误:这里会创建一个新的shared_ptr,导致问题
                                                     // 更正确的方式是 child->parent = shared_from_this();
                                                     // 但即使是shared_from_this(),如果Parent和Child相互持有shared_ptr,
                                                     // 仍然会导致循环依赖。这里为了简化问题,先用这种方式演示。
    }

    // 正确的addChild,假设Parent对象本身已经被shared_ptr管理
    void addChild_correct(std::shared_ptr<Child> child) {
        children.push_back(child);
        // 确保Parent对象本身已经被一个shared_ptr管理,才能使用shared_from_this()
        // 如果Parent对象不是通过shared_ptr创建的,shared_from_this()会抛出异常
        child->parent = shared_from_this(); 
    }
};

// 为了演示shared_from_this(),Parent类需要继承enable_shared_from_this
class Parent_EnableShared : public std::enable_shared_from_this<Parent_EnableShared> {
public:
    std::string name;
    std::vector<std::shared_ptr<Child>> children; 

    Parent_EnableShared(const std::string& n) : name(n) {
        std::cout << "Parent_EnableShared " << name << " constructor called." << std::endl;
    }

    ~Parent_EnableShared() {
        std::cout << "Parent_EnableShared " << name << " destructor called." << std::endl;
    }

    void addChild(std::shared_ptr<Child> child) {
        children.push_back(child);
        child->parent = shared_from_this(); // 使用shared_from_this()获取指向自身的共享指针
    }
};

void demonstrate_circular_dependency() {
    std::cout << "n--- Demonstrating Circular Dependency with std::shared_ptr ---" << std::endl;

    // 创建一个父对象和子对象
    std::shared_ptr<Parent_EnableShared> parent = std::make_shared<Parent_EnableShared>("Papa Smurf");
    std::shared_ptr<Child> child = std::make_shared<Child>("Smurfette");

    std::cout << "Initial ref counts: parent=" << parent.use_count() 
              << ", child=" << child.use_count() << std::endl;

    // 建立父子关系
    parent->addChild(child);

    std::cout << "After addChild ref counts: parent=" << parent.use_count() 
              << ", child=" << child.use_count() << std::endl;

    // 当parent和child走出作用域时,它们各自的shared_ptr会被销毁。
    // 理论上,引用计数应该降到0,对象应该被销毁。
    // 但实际上,它们会互相持有对方的shared_ptr,导致引用计数永远不会降到0。
} // parent 和 child 在这里超出作用域

int main() {
    demonstrate_circular_dependency();
    std::cout << "--- End of main function ---" << std::endl;
    // 观察输出,Parent_EnableShared和Child的析构函数是否被调用
    return 0;
}

运行上述代码,你将看到以下输出(或类似):

--- Demonstrating Circular Dependency with std::shared_ptr ---
Parent_EnableShared Papa Smurf constructor called.
Child Smurfette constructor called.
Initial ref counts: parent=1, child=1
After addChild ref counts: parent=2, child=2
--- End of main function ---

问题分析:

  1. std::shared_ptr<Parent_EnableShared> parent = std::make_shared<Parent_EnableShared>("Papa Smurf");
    • Parent_EnableShared("Papa Smurf") 对象被创建。
    • parent 智能指针拥有它,parent.use_count() 为 1。
  2. std::shared_ptr<Child> child = std::make_shared<Child>("Smurfette");
    • Child("Smurfette") 对象被创建。
    • child 智能指针拥有它,child.use_count() 为 1。
  3. parent->addChild(child);
    • parent->children.push_back(child);parent 持有 child 的一个 shared_ptr 副本。child 对象的引用计数增加到 2。
    • child->parent = shared_from_this();child 持有 parent 的一个 shared_ptr 副本(通过 shared_from_this() 获取)。parent 对象的引用计数增加到 2。

现在,parent 对象的引用计数是 2 (由 main 函数中的 parent 变量和 child 对象中的 parent 成员持有)。child 对象的引用计数也是 2 (由 main 函数中的 child 变量和 parent 对象中的 children 向量持有)。

demonstrate_circular_dependency 函数结束时:

  • main 函数中的 parent 变量被销毁,parent 对象的引用计数从 2 降到 1。
  • main 函数中的 child 变量被销毁,child 对象的引用计数从 2 降到 1。

此时,parent 对象的引用计数是 1 (由 child 对象中的 parent 成员持有),child 对象的引用计数是 1 (由 parent 对象中的 children 向量持有)。两个对象的引用计数都未能降到 0。这意味着它们所指向的 Parent_EnableSharedChild 对象都不会被销毁,它们的析构函数永远不会被调用,从而导致了内存泄漏

3. std::weak_ptr 的登场:非拥有性引用

为了解决 std::shared_ptr 的循环依赖问题,C++11 引入了 std::weak_ptrstd::weak_ptr 是一种非拥有性(non-owning)的智能指针。它指向一个由 std::shared_ptr 管理的对象,但它本身不参与对象的引用计数。

3.1 std::weak_ptr 的特性

  • 不增加引用计数: std::weak_ptr 的主要特点是它不会增加所指向对象的强引用计数(strong reference count)。这意味着 std::weak_ptr 的存在不会阻止 std::shared_ptr 在其引用计数降为零时销毁对象。
  • 无法直接访问对象: std::weak_ptr 不能像 std::shared_ptr 那样直接通过 operator*operator-> 访问其指向的对象。这是因为它所指向的对象可能已经被销毁了。
  • 安全性检查:lock() 方法: 为了访问 std::weak_ptr 指向的对象,你必须先调用其 lock() 方法。lock() 方法会尝试返回一个 std::shared_ptr
    • 如果对象仍然存在(即有至少一个 std::shared_ptr 还在管理它),lock() 会返回一个有效的 std::shared_ptr,并且会临时增加对象的引用计数,确保在你使用它的期间对象不会被销毁。
    • 如果对象已经被销毁(即所有 std::shared_ptr 都已不再管理它),lock() 会返回一个空的 std::shared_ptr (即 nullptr)。
  • 检查对象存活:expired() 方法: expired() 方法返回 true 如果所指向的对象已经被销毁,否则返回 false。这等价于 lock().expired()
  • 查看强引用计数:use_count() 方法: use_count() 方法返回当前有多少个 std::shared_ptr 在管理该对象。注意,它不包括 std::weak_ptr 的数量。
  • 生命周期: std::weak_ptr 本身有一个“弱引用计数”(weak reference count),它与强引用计数一起存储在一个名为控制块(control block)的数据结构中。当一个对象的最后一个 std::shared_ptr 被销毁时,对象本身会被销毁。但只有当对象的强引用计数和弱引用计数都降为零时,控制块才会被销毁。这意味着 std::weak_ptr 可以比它所指向的对象活得更久,但它不会阻止对象的销毁。

3.2 std::weak_ptr 物理路径:控制块

要理解 std::weak_ptr 如何工作,必须了解 std::shared_ptrstd::weak_ptr 共享的控制块

当通过 std::make_sharedstd::shared_ptr 构造函数创建一个 std::shared_ptr 时,除了动态分配的对象本身,还会额外动态分配一个控制块。这个控制块通常包含以下信息:

  • 强引用计数 (Strong Reference Count): 记录有多少个 std::shared_ptr 实例正在管理该对象。当这个计数降到0时,所管理的对象会被删除。
  • 弱引用计数 (Weak Reference Count): 记录有多少个 std::weak_ptr 实例正在观察该对象。当这个计数和强引用计数都降到0时,控制块本身才会被删除。
  • 自定义删除器 (Deleter): 如果提供了自定义删除器,它会存储在这里。
  • 分配器 (Allocator): 如果提供了自定义分配器,它会存储在这里。
  • 指向对象的指针: 实际存储被管理对象的地址。

工作原理:

  1. 当一个 std::shared_ptr 被创建时,强引用计数增加。
  2. 当一个 std::shared_ptr 被销毁时,强引用计数减少。
  3. 当一个 std::weak_ptr 被创建时(通常是从 std::shared_ptr 构造),弱引用计数增加。
  4. 当一个 std::weak_ptr 被销毁时,弱引用计数减少。

关键点:

  • 对象生命周期: 当强引用计数降到0时,std::shared_ptr 会调用删除器销毁对象
  • 控制块生命周期: 只有当强引用计数和弱引用计数都降到0时控制块才会被销毁。

这意味着,即使所有 std::shared_ptr 都已离开作用域,只要还有一个 std::weak_ptr 存在,控制块就不会被销毁。std::weak_ptr 可以通过控制块判断对象是否仍然存在(通过检查强引用计数是否为零)。如果强引用计数为零,std::weak_ptr::lock() 会返回一个空的 std::shared_ptr

4. 利用 std::weak_ptr 破解循环依赖

解决循环依赖的关键在于打破所有权循环。在父子关系中,通常的约定是父对象拥有子对象,而子对象只是引用或观察父对象,但并不拥有它。这意味着父对象对子对象使用 std::shared_ptr,而子对象对父对象使用 std::weak_ptr

让我们修改之前的 Parent_EnableSharedChild 类。

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// 前向声明
class Parent_EnableShared_Fixed;

class Child_Fixed {
public:
    std::string name;
    // 子对象对父对象使用 std::weak_ptr
    std::weak_ptr<Parent_EnableShared_Fixed> parent; 

    Child_Fixed(const std::string& n) : name(n) {
        std::cout << "Child_Fixed " << name << " constructor called." << std::endl;
    }

    ~Child_Fixed() {
        std::cout << "Child_Fixed " << name << " destructor called." << std::endl;
    }

    void greetParent() {
        // 尝试获取父对象的共享指针
        if (auto p_shared = parent.lock()) {
            std::cout << "Child_Fixed " << name << " greets parent " << p_shared->name << std::endl;
        } else {
            std::cout << "Child_Fixed " << name << ": My parent is gone." << std::endl;
        }
    }
};

class Parent_EnableShared_Fixed : public std::enable_shared_from_this<Parent_EnableShared_Fixed> {
public:
    std::string name;
    // 父对象继续拥有子对象,使用 std::shared_ptr
    std::vector<std::shared_ptr<Child_Fixed>> children; 

    Parent_EnableShared_Fixed(const std::string& n) : name(n) {
        std::cout << "Parent_EnableShared_Fixed " << name << " constructor called." << std::endl;
    }

    ~Parent_EnableShared_Fixed() {
        std::cout << "Parent_EnableShared_Fixed " << name << " destructor called." << std::endl;
    }

    void addChild(std::shared_ptr<Child_Fixed> child) {
        children.push_back(child);
        // 设置子对象的父指针,通过 shared_from_this() 获得共享指针,
        // 再将其赋值给 weak_ptr
        child->parent = shared_from_this(); 
    }
};

void demonstrate_fixed_circular_dependency() {
    std::cout << "n--- Demonstrating Fixed Circular Dependency with std::weak_ptr ---" << std::endl;

    std::shared_ptr<Parent_EnableShared_Fixed> parent = std::make_shared<Parent_EnableShared_Fixed>("Papa Smurf");
    std::shared_ptr<Child_Fixed> child = std::make_shared<Child_Fixed>("Smurfette");

    std::cout << "Initial ref counts: parent=" << parent.use_count() 
              << ", child=" << child.use_count() << std::endl;

    parent->addChild(child);

    std::cout << "After addChild ref counts: parent=" << parent.use_count() 
              << ", child=" << child.use_count() << std::endl;

    child->greetParent(); // 此时父对象还在,可以访问

} // parent 和 child 在这里超出作用域

int main() {
    demonstrate_fixed_circular_dependency();
    std::cout << "--- End of main function ---" << std::endl;
    // 观察输出,Parent_EnableShared_Fixed和Child_Fixed的析构函数是否被调用
    return 0;
}

运行上述代码,你将看到以下输出(或类似):

--- Demonstrating Fixed Circular Dependency with std::weak_ptr ---
Parent_EnableShared_Fixed Papa Smurf constructor called.
Child_Fixed Smurfette constructor called.
Initial ref counts: parent=1, child=1
After addChild ref counts: parent=1, child=2
Child_Fixed Smurfette greets parent Papa Smurf
Child_Fixed Smurfette destructor called.
Parent_EnableShared_Fixed Papa Smurf destructor called.
--- End of main function ---

问题解决分析:

  1. std::shared_ptr<Parent_EnableShared_Fixed> parent = std::make_shared<Parent_EnableShared_Fixed>("Papa Smurf");
    • Parent_EnableShared_Fixed("Papa Smurf") 对象被创建。
    • parent 智能指针拥有它,parent.use_count() 为 1。
  2. std::shared_ptr<Child_Fixed> child = std::make_shared<Child_Fixed>("Smurfette");
    • Child_Fixed("Smurfette") 对象被创建。
    • child 智能指针拥有它,child.use_count() 为 1。
  3. parent->addChild(child);
    • parent->children.push_back(child);parent 持有 child 的一个 shared_ptr 副本。child 对象的引用计数增加到 2。
    • child->parent = shared_from_this();child 持有 parent 的一个 std::weak_ptr 副本。注意:std::weak_ptr 不会增加强引用计数! 所以 parent 对象的引用计数仍然是 1。

现在,parent 对象的引用计数是 1 (由 main 函数中的 parent 变量持有)。child 对象的引用计数是 2 (由 main 函数中的 child 变量和 parent 对象中的 children 向量持有)。

demonstrate_fixed_circular_dependency 函数结束时:

  • main 函数中的 parent 变量被销毁,parent 对象的引用计数从 1 降到 0。
  • 因为 parent 对象的引用计数降为 0,Parent_EnableShared_Fixed 对象被销毁,其析构函数被调用。
  • Parent_EnableShared_Fixed 的析构函数中,children 向量会被销毁。这将导致 parent 对象持有的 childshared_ptr 副本被销毁。
  • child 对象的引用计数从 2 降到 1。
  • main 函数中的 child 变量被销毁,child 对象的引用计数从 1 降到 0。
  • 因为 child 对象的引用计数降为 0,Child_Fixed 对象被销毁,其析构函数被调用。

至此,所有的对象都被正确地销毁了,内存泄漏问题得到了解决。

4.1 访问失效的 weak_ptr

Child_Fixed::greetParent() 方法中,我们看到了 std::weak_ptr::lock() 的用法。如果在调用 greetParent() 时,父对象已经被销毁了,parent.lock() 将返回一个空的 std::shared_ptr

void demonstrate_accessing_expired_weak_ptr() {
    std::cout << "n--- Demonstrating Accessing Expired std::weak_ptr ---" << std::endl;

    std::shared_ptr<Parent_EnableShared_Fixed> parent = std::make_shared<Parent_EnableShared_Fixed>("Temporary Parent");
    std::shared_ptr<Child_Fixed> child = std::make_shared<Child_Fixed>("Orphan Child");

    parent->addChild(child);

    std::cout << "Parent exists. Child tries to greet parent:" << std::endl;
    child->greetParent();

    // 手动释放父对象,模拟父对象提前被销毁
    parent.reset(); 

    std::cout << "nParent has been reset. Child tries to greet parent again:" << std::endl;
    child->greetParent();

} // child 在这里超出作用域

输出:

--- Demonstrating Accessing Expired std::weak_ptr ---
Parent_EnableShared_Fixed Temporary Parent constructor called.
Child_Fixed Orphan Child constructor called.
Parent exists. Child tries to greet parent:
Child_Fixed Orphan Child greets parent Temporary Parent

Parent has been reset. Child tries to greet parent again:
Child_Fixed Orphan Child: My parent is gone.
Child_Fixed Orphan Child destructor called.
Parent_EnableShared_Fixed Temporary Parent destructor called.

这完美展示了 std::weak_ptr 的安全性:它允许你在对象可能已经不存在的情况下安全地尝试访问它,而不会导致悬空指针(dangling pointer)问题。

5. std::weak_ptr 的实际应用场景和最佳实践

std::weak_ptr 不仅仅是解决循环依赖的工具,它在许多其他场景下也扮演着关键角色。

5.1 缓存管理

在实现缓存系统时,你可能希望缓存中的对象在没有其他地方强引用它们时能够被自动清除。std::weak_ptr 是理想的选择。缓存可以存储对象的 std::weak_ptr。当需要访问缓存中的对象时,先 lock() 尝试获取 std::shared_ptr。如果 lock() 返回 nullptr,说明该对象已经被销毁,可以从缓存中移除其 std::weak_ptr

#include <map>
// ... (其他头文件)

class CacheItem {
public:
    std::string data;
    CacheItem(const std::string& d) : data(d) {
        std::cout << "CacheItem(" << data << ") constructor." << std::endl;
    }
    ~CacheItem() {
        std::cout << "CacheItem(" << data << ") destructor." << std::endl;
    }
    void use() { std::cout << "Using cached item: " << data << std::endl; }
};

class Cache {
private:
    std::map<std::string, std::weak_ptr<CacheItem>> items;
public:
    std::shared_ptr<CacheItem> get(const std::string& key) {
        auto it = items.find(key);
        if (it != items.end()) {
            if (auto item = it->second.lock()) { // 尝试获取强引用
                std::cout << "Cache hit for " << key << std::endl;
                return item;
            } else {
                // 对象已失效,从缓存中移除
                std::cout << "Cache item for " << key << " expired, removing." << std::endl;
                items.erase(it);
            }
        }
        std::cout << "Cache miss for " << key << std::endl;
        return nullptr;
    }

    void add(const std::string& key, std::shared_ptr<CacheItem> item) {
        items[key] = item; // 存储弱引用
        std::cout << "Added " << key << " to cache." << std::endl;
    }

    void cleanup() {
        // 清理所有过期的弱引用
        for (auto it = items.begin(); it != items.end(); ) {
            if (it->second.expired()) {
                std::cout << "Cleaning up expired cache item: " << it->first << std::endl;
                it = items.erase(it);
            } else {
                ++it;
            }
        }
    }
};

void demonstrate_cache() {
    std::cout << "n--- Demonstrating Cache with std::weak_ptr ---" << std::endl;
    Cache my_cache;

    // 创建一个强引用
    std::shared_ptr<CacheItem> item1 = std::make_shared<CacheItem>("DataA");
    my_cache.add("keyA", item1);

    // 另一个强引用,但不是通过缓存
    std::shared_ptr<CacheItem> item2_owner = std::make_shared<CacheItem>("DataB");
    my_cache.add("keyB", item2_owner);

    // 从缓存获取
    if (auto cached_item_a = my_cache.get("keyA")) {
        cached_item_a->use();
    }

    // 释放 item1,现在只有 cache 拥有 keyA 的弱引用
    item1.reset(); 

    // 再次尝试获取 keyA,它应该还在(因为 item2_owner 还存在)
    std::cout << "Attempting to get keyA after item1 reset (should be expired):" << std::endl;
    if (auto cached_item_a = my_cache.get("keyA")) {
        // 这段代码不会执行,因为item1是唯一的shared_ptr,它reset后,CacheItem("DataA")就被销毁了
        // 这里说明,如果 CacheItem("DataA") 还有其他强引用,它就不会被销毁。
        // 在本例中,item1是唯一的,所以它reset后,CacheItem("DataA")就会被销毁
        cached_item_a->use();
    } else {
        std::cout << "keyA is truly gone." << std::endl;
    }

    // 释放 item2_owner
    item2_owner.reset(); 

    // 再次尝试获取 keyB,它应该已经失效
    std::cout << "Attempting to get keyB after item2_owner reset (should be expired):" << std::endl;
    if (auto cached_item_b = my_cache.get("keyB")) {
        cached_item_b->use();
    } else {
        std::cout << "keyB is truly gone." << std::endl;
    }

    my_cache.cleanup();
}

5.2 观察者模式

在观察者模式中,主题(Subject)通知观察者(Observer)事件。如果主题持有观察者的 std::shared_ptr,而观察者又持有主题的 std::shared_ptr(或者观察者在生命周期上比主题更长),就可能导致循环依赖。让主题持有观察者的 std::weak_ptr,可以确保当观察者被销毁时,主题不会阻止它的销毁。

// 假设有一个简单的事件系统
class EventPublisher;

class IEventListener {
public:
    virtual ~IEventListener() = default;
    virtual void onEvent(const std::string& event_data) = 0;
};

class EventPublisher {
public:
    // 发布者持有监听者的弱引用
    std::vector<std::weak_ptr<IEventListener>> listeners;

    void addListener(std::shared_ptr<IEventListener> listener) {
        listeners.push_back(listener);
        std::cout << "Listener added." << std::endl;
    }

    void publishEvent(const std::string& data) {
        std::cout << "Publishing event: " << data << std::endl;
        // 遍历监听者,并清理已失效的
        for (auto it = listeners.begin(); it != listeners.end(); ) {
            if (auto listener = it->lock()) { // 尝试获取强引用
                listener->onEvent(data);
                ++it;
            } else {
                // 监听者已失效,移除
                std::cout << "Removing expired listener." << std::endl;
                it = listeners.erase(it);
            }
        }
    }
};

class MyListener : public IEventListener {
public:
    std::string name;
    MyListener(const std::string& n) : name(n) {
        std::cout << "MyListener(" << name << ") constructor." << std::endl;
    }
    ~MyListener() {
        std::cout << "MyListener(" << name << ") destructor." << std::endl;
    }
    void onEvent(const std::string& event_data) override {
        std::cout << "Listener " << name << " received event: " << event_data << std::endl;
    }
};

void demonstrate_observer() {
    std::cout << "n--- Demonstrating Observer Pattern with std::weak_ptr ---" << std::endl;
    EventPublisher publisher;

    { // 监听者的作用域
        std::shared_ptr<MyListener> listener1 = std::make_shared<MyListener>("ListenerA");
        publisher.addListener(listener1);
        publisher.publishEvent("Hello from A");

        std::shared_ptr<MyListener> listener2 = std::make_shared<MyListener>("ListenerB");
        publisher.addListener(listener2);
        publisher.publishEvent("Hello from A and B");

        listener1.reset(); // ListenerA 强引用被释放
        std::cout << "nAfter listener1.reset():" << std::endl;
        publisher.publishEvent("Event after A reset"); // ListenerA 应该被移除
    } // listener2 强引用在这里被释放

    std::cout << "nAfter listener2 leaves scope:" << std::endl;
    publisher.publishEvent("Final event"); // 所有监听者都应该被移除
}

5.3 避免悬空指针

std::weak_ptr 本身无法直接导致悬空指针。因为你必须通过 lock() 方法获取一个 std::shared_ptr 才能访问对象。如果对象已经不存在,lock() 会返回 nullptr,你可以安全地检查这个 nullptr。这比裸指针或 std::unique_ptr 更加安全,因为它们不提供这样的运行时检查。

5.4 Doubly Linked List (双向链表)

在实现双向链表时,一个节点通常会指向其前一个节点和后一个节点。如果 NodeA 拥有 NodeB (通过 shared_ptr<NodeB> next;),而 NodeB 又拥有 NodeA (通过 shared_ptr<NodeA> prev;),则会形成循环依赖。解决方案是让 prev 指针使用 std::weak_ptr<NodeA> prev;

template <typename T>
class DoubleLinkedListNode {
public:
    T data;
    std::shared_ptr<DoubleLinkedListNode<T>> next;
    std::weak_ptr<DoubleLinkedListNode<T>> prev; // 使用 weak_ptr 避免循环

    DoubleLinkedListNode(T val) : data(val) {
        std::cout << "Node(" << data << ") constructor." << std::endl;
    }
    ~DoubleLinkedListNode() {
        std::cout << "Node(" << data << ") destructor." << std::endl;
    }
};

void demonstrate_doubly_linked_list() {
    std::cout << "n--- Demonstrating Doubly Linked List with std::weak_ptr ---" << std::endl;

    std::shared_ptr<DoubleLinkedListNode<int>> node1 = std::make_shared<DoubleLinkedListNode<int>>(10);
    std::shared_ptr<DoubleLinkedListNode<int>> node2 = std::make_shared<DoubleLinkedListNode<int>>(20);
    std::shared_ptr<DoubleLinkedListNode<int>> node3 = std::make_shared<DoubleLinkedListNode<int>>(30);

    // 建立链接
    node1->next = node2;
    node2->prev = node1; // weak_ptr 不增加引用计数

    node2->next = node3;
    node3->prev = node2; // weak_ptr 不增加引用计数

    // 此时引用计数:
    // node1: 1 (由 main 作用域持有)
    // node2: 2 (由 main 作用域持有, 由 node1->next 持有)
    // node3: 2 (由 main 作用域持有, 由 node2->next 持有)

    // 访问链表
    std::cout << "Node1 data: " << node1->data << std::endl;
    if (auto p_node2 = node1->next) {
        std::cout << "Node2 data: " << p_node2->data << std::endl;
        if (auto p_node1_from_2 = p_node2->prev.lock()) {
            std::cout << "Node2's prev node data: " << p_node1_from_2->data << std::endl;
        }
    }

    // 释放 node1
    node1.reset(); 
    std::cout << "nAfter node1.reset():" << std::endl;
    // node1 强引用计数降为0,node1 对象被销毁。
    // node2 的 prev (weak_ptr) 现在指向一个已销毁的对象。

    // 尝试从 node2 访问 prev
    if (auto p_node2_from_main = node2) {
        if (auto p_node1_from_2 = p_node2_from_main->prev.lock()) {
            std::cout << "Node2's prev node data: " << p_node1_from_2->data << std::endl;
        } else {
            std::cout << "Node2's prev node is gone." << std::endl;
        }
    }
    // node2 和 node3 的强引用仍然存在,所以它们不会被销毁,直到 main 作用域结束
} // node2 和 node3 在这里超出作用域

6. std::shared_ptr, std::unique_ptr, std::weak_ptr 对比

为了更好地理解 std::weak_ptr 的定位,我们将其与另外两个常用智能指针进行对比。

特性 std::unique_ptr std::shared_ptr std::weak_ptr
所有权模型 独占所有权 (Exclusive Ownership) 共享所有权 (Shared Ownership) 非拥有性引用 (Non-owning Reference)
引用计数 无 (Zero Overhead) 有 (Strong Reference Count) 有 (Weak Reference Count, 不影响强引用计数)
生命周期 unique_ptr 被销毁时,对象被销毁。 当最后一个 shared_ptr 被销毁时,对象被销毁。 不控制对象生命周期。不阻止对象被销毁。
内存开销 与裸指针相同(通常)。 比裸指针大(需要额外的控制块)。 比裸指针大(需要额外的控制块)。
拷贝/赋值 不可拷贝,可移动。 可拷贝,可赋值。 可拷贝,可赋值。
用途 明确的单一所有者,工厂函数返回,PIMPL,资源句柄。 多个所有者,复杂对象图,缓存。 解决循环依赖,观察者模式,缓存,打破所有权循环。
安全性 自动删除,无悬空指针(移动后原指针为空)。 自动删除,无悬空指针。 自动检查对象存活状态,无悬空指针(lock() 返回 nullptr)。
循环依赖 不会直接导致循环依赖(因其独占)。 易导致循环依赖,进而内存泄漏。 解决方案,用于打破 shared_ptr 循环依赖。

7. 总结

std::weak_ptr 是C++11智能指针家族中一个不可或缺的成员,它通过提供一种非拥有性的引用机制,有效地解决了 std::shared_ptr 在处理循环依赖时可能导致的内存泄漏问题。理解其工作原理,尤其是与控制块和引用计数的关系,对于编写健壮、高效的现代C++代码至关重要。

在设计复杂的对象关系时,我们应该仔细考虑所有权语义:哪个对象拥有哪个对象?如果一个对象只是观察或需要临时访问另一个对象,而不需要拥有它,那么 std::weak_ptr 往往是比 std::shared_ptr 更合适的选择。通过恰当地使用 std::weak_ptr,我们可以构建出既灵活又内存安全的对象图。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注