C++ `Weak Pointer` 在非循环引用场景下的高级应用与生命周期管理

哈喽,各位好!今天咱们来聊聊 C++ 里的 weak_ptr,这玩意儿啊,很多人觉得就是用来打破循环引用的,打破循环引用它确实是一把好手,但这只是它的小试牛刀而已。今天我们就深入挖掘一下,看看 weak_ptr 在非循环引用场景下,还能怎么大放异彩,以及如何用它来管理对象的生命周期,让我们的代码更加健壮和优雅。

weak_ptr:你真的了解它吗?

首先,让我们快速回顾一下 weak_ptr 的基本概念。weak_ptr 是一种智能指针,它“弱弱地”指向一个对象,不会增加对象的引用计数。这就意味着,即使有 weak_ptr 指向某个对象,这个对象也可能会被销毁。

  • 不拥有所有权: 这是 weak_ptr 最核心的特性,它观察对象,但不阻止对象被销毁。
  • 需要配合 shared_ptr 使用: weak_ptr 必须从 shared_ptr 或者另一个 weak_ptr 构造而来。
  • expired() 方法: 用来检查 weak_ptr 指向的对象是否已经被销毁。
  • lock() 方法: 尝试将 weak_ptr 提升为 shared_ptr。如果对象还活着,lock() 会返回一个指向该对象的 shared_ptr;如果对象已经销毁,lock() 会返回一个空的 shared_ptr

weak_ptr 的高级应用场景

好了,基本概念过完,我们来点干货,看看 weak_ptr 在非循环引用场景下的高级应用。

  1. 对象缓存 (Object Caching)

假设我们有一个缓存系统,用于存储一些昂贵的对象。我们希望在内存压力大的时候,能够自动释放这些对象,但又不能在使用它们的时候,发现对象已经被释放了。这时候,weak_ptr 就派上用场了。

#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>

class ExpensiveObject {
public:
    ExpensiveObject(const std::string& id) : id_(id) {
        std::cout << "ExpensiveObject " << id_ << " created." << std::endl;
    }
    ~ExpensiveObject() {
        std::cout << "ExpensiveObject " << id_ << " destroyed." << std::endl;
    }
    void doSomething() {
        std::cout << "ExpensiveObject " << id_ << " doing something." << std::endl;
    }

private:
    std::string id_;
};

class ObjectCache {
public:
    std::shared_ptr<ExpensiveObject> getObject(const std::string& id) {
        // 尝试从缓存中获取对象
        auto it = cache_.find(id);
        if (it != cache_.end()) {
            std::shared_ptr<ExpensiveObject> obj = it->second.lock(); // 尝试提升 weak_ptr
            if (obj) {
                std::cout << "Object " << id << " found in cache." << std::endl;
                return obj;
            } else {
                // 对象已经被销毁,从缓存中移除
                std::cout << "Object " << id << " in cache expired." << std::endl;
                cache_.erase(it);
            }
        }

        // 对象不在缓存中,创建并添加到缓存
        std::cout << "Creating object " << id << std::endl;
        std::shared_ptr<ExpensiveObject> obj = std::make_shared<ExpensiveObject>(id);
        cache_[id] = obj; // 将 shared_ptr 转换为 weak_ptr
        return obj;
    }

private:
    std::unordered_map<std::string, std::weak_ptr<ExpensiveObject>> cache_;
};

int main() {
    ObjectCache cache;

    // 获取对象 "A"
    std::shared_ptr<ExpensiveObject> objA = cache.getObject("A");
    objA->doSomething();

    // 释放 objA 的所有权(模拟内存压力导致对象被释放)
    objA.reset();
    std::cout << "objA reset." << std::endl;

    // 再次获取对象 "A"
    std::shared_ptr<ExpensiveObject> objA2 = cache.getObject("A");  // 由于 objA 已经 reset, 从缓存中取出的weak_ptr会expired, 从而新建对象
    if (objA2) {
        objA2->doSomething();
    } else {
        std::cout << "Object A not available." << std::endl;
    }

    return 0;
}

在这个例子中,ObjectCache 使用 std::weak_ptr 来存储缓存的对象。当客户端请求一个对象时,getObject 方法首先尝试从缓存中获取对象。如果缓存中存在该对象,并且 weak_ptr 指向的对象仍然有效(lock() 返回非空 shared_ptr),则直接返回该对象。如果对象已经被销毁,则从缓存中移除该对象,并创建一个新的对象。

  1. 观察者模式 (Observer Pattern)

在观察者模式中,观察者需要监听主题的状态变化。如果主题被销毁,观察者应该自动取消监听,避免访问悬空指针。weak_ptr 可以用来存储指向主题的指针,确保观察者不会持有主题的所有权,从而避免内存泄漏。

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

class Subject;  // 前向声明

class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() {}
};

class ConcreteObserver : public Observer {
public:
    ConcreteObserver(std::weak_ptr<Subject> subject, const std::string& name) : subject_(subject), name_(name) {}

    void update() override {
        if (auto ptr = subject_.lock()) {  // 尝试提升 weak_ptr
            std::cout << "Observer " << name_ << " received update from Subject." << std::endl;
        } else {
            std::cout << "Observer " << name_ << " : Subject is gone!" << std::endl;
        }
    }

private:
    std::weak_ptr<Subject> subject_;
    std::string name_;
};

class Subject {
public:
    void attach(std::shared_ptr<Observer> observer) {
        observers_.push_back(observer);
    }

    void detach(std::shared_ptr<Observer> observer) {
        for (auto it = observers_.begin(); it != observers_.end(); ++it) {
            if (*it == observer) {
                observers_.erase(it);
                break;
            }
        }
    }

    void notify() {
        for (auto& observer : observers_) {
            observer->update();
        }
    }

    ~Subject() {
        std::cout << "Subject destroyed." << std::endl;
    }

private:
    std::vector<std::shared_ptr<Observer>> observers_;
};

int main() {
    std::shared_ptr<Subject> subject = std::make_shared<Subject>();

    std::shared_ptr<Observer> observer1 = std::make_shared<ConcreteObserver>(subject, "Observer1");
    std::shared_ptr<Observer> observer2 = std::make_shared<ConcreteObserver>(subject, "Observer2");

    subject->attach(observer1);
    subject->attach(observer2);

    subject->notify();  // 通知观察者

    subject.reset(); // Subject 被销毁
    std::cout << "Subject reset." << std::endl;

    // 即使 Subject 已经被销毁,Observer 依然可以安全地调用 update()
    observer1->update(); // Observer1 会检测到 Subject 已经销毁
    observer2->update(); // Observer2 会检测到 Subject 已经销毁

    return 0;
}

在这个例子中,ConcreteObserver 使用 std::weak_ptr 来存储指向 Subject 的指针。当 update() 方法被调用时,它首先尝试使用 lock() 方法将 weak_ptr 提升为 shared_ptr。如果 Subject 对象仍然有效,则执行更新操作;否则,表示 Subject 已经被销毁,可以安全地忽略更新请求。

  1. 父子关系 (Parent-Child Relationship)

在某些场景下,我们需要维护对象之间的父子关系。子对象需要访问父对象,但又不希望持有父对象的所有权,避免父对象被子对象“意外”地延长生命周期。这时候,weak_ptr 可以用来存储指向父对象的指针。

#include <iostream>
#include <memory>

class Parent {
public:
    Parent(const std::string& name) : name_(name) {
        std::cout << "Parent " << name_ << " created." << std::endl;
    }
    ~Parent() {
        std::cout << "Parent " << name_ << " destroyed." << std::endl;
    }
    void printName() {
        std::cout << "Parent name: " << name_ << std::endl;
    }

private:
    std::string name_;
};

class Child {
public:
    Child(std::shared_ptr<Parent> parent, const std::string& name) : parent_(parent), name_(name) {
        std::cout << "Child " << name_ << " created." << std::endl;
    }
    ~Child() {
        std::cout << "Child " << name_ << " destroyed." << std::endl;
    }

    void printParentName() {
        if (auto parent = parent_.lock()) { // 尝试提升 weak_ptr
            parent->printName();
        } else {
            std::cout << "Parent is gone!" << std::endl;
        }
    }

private:
    std::weak_ptr<Parent> parent_;
    std::string name_;
};

int main() {
    std::shared_ptr<Parent> parent = std::make_shared<Parent>("Parent1");
    Child child(parent, "Child1");

    child.printParentName(); // 打印父对象的名字

    parent.reset(); // 父对象被销毁
    std::cout << "Parent reset." << std::endl;

    child.printParentName(); // 子对象检测到父对象已经被销毁

    return 0;
}

在这个例子中,Child 对象使用 std::weak_ptr 来存储指向 Parent 对象的指针。当 printParentName() 方法被调用时,它首先尝试使用 lock() 方法将 weak_ptr 提升为 shared_ptr。如果 Parent 对象仍然有效,则打印父对象的名字;否则,表示 Parent 已经被销毁,打印一条消息。

  1. 避免悬空指针 (Dangling Pointers)

当多个对象需要访问同一个资源时,如果其中一个对象释放了该资源,其他对象可能会访问到悬空指针,导致程序崩溃。weak_ptr 可以用来检测资源是否仍然有效,避免访问悬空指针。

#include <iostream>
#include <memory>

class Resource {
public:
    Resource(const std::string& name) : name_(name) {
        std::cout << "Resource " << name_ << " created." << std::endl;
    }
    ~Resource() {
        std::cout << "Resource " << name_ << " destroyed." << std::endl;
    }
    void use() {
        std::cout << "Using resource " << name_ << std::endl;
    }

private:
    std::string name_;
};

class User {
public:
    User(std::weak_ptr<Resource> resource, const std::string& name) : resource_(resource), name_(name) {
        std::cout << "User " << name_ << " created." << std::endl;
    }
    ~User() {
        std::cout << "User " << name_ << " destroyed." << std::endl;
    }

    void accessResource() {
        if (auto resource = resource_.lock()) { // 尝试提升 weak_ptr
            resource->use();
        } else {
            std::cout << "Resource is not available for user " << name_ << std::endl;
        }
    }

private:
    std::weak_ptr<Resource> resource_;
    std::string name_;
};

int main() {
    std::shared_ptr<Resource> resource = std::make_shared<Resource>("Resource1");
    User user1(resource, "User1");
    User user2(resource, "User2");

    user1.accessResource();
    user2.accessResource();

    resource.reset(); // 资源被释放
    std::cout << "Resource reset." << std::endl;

    user1.accessResource(); // 用户检测到资源已经被释放
    user2.accessResource(); // 用户检测到资源已经被释放

    return 0;
}

在这个例子中,User 对象使用 std::weak_ptr 来存储指向 Resource 对象的指针。当 accessResource() 方法被调用时,它首先尝试使用 lock() 方法将 weak_ptr 提升为 shared_ptr。如果 Resource 对象仍然有效,则访问该资源;否则,表示 Resource 已经被释放,打印一条消息。

weak_ptr 与对象生命周期管理

weak_ptr 在对象生命周期管理方面,可以发挥重要的作用。

  • 延迟销毁: 通过 weak_ptr,我们可以延迟对象的销毁时间,直到所有观察者都停止观察该对象。
  • 避免资源泄漏: 通过 weak_ptr,我们可以避免资源泄漏,确保资源在不再被使用时能够及时释放。
  • 简化对象关系: 通过 weak_ptr,我们可以简化对象之间的关系,避免复杂的对象依赖关系。

weak_ptr 的一些注意事项

  • 性能开销: weak_ptrlock() 操作需要进行原子操作,因此会有一定的性能开销。在性能敏感的场景下,需要谨慎使用。
  • 线程安全: weak_ptrlock() 操作是线程安全的,但对 weak_ptr 本身的操作(例如赋值、构造)可能不是线程安全的。在多线程环境下,需要进行适当的同步。
  • 不要过度使用: weak_ptr 是一种强大的工具,但不要过度使用。在简单的场景下,使用 shared_ptr 或者原始指针可能更加合适。

总结

特性 shared_ptr weak_ptr
所有权 拥有所有权,增加引用计数 不拥有所有权,不增加引用计数
生命周期 决定对象的生命周期 观察对象的生命周期
使用场景 需要确保对象在被使用时一直存在 需要观察对象,但不阻止对象被销毁
主要方法 get(), reset(), use_count() expired(), lock()
循环引用解决方案 容易导致循环引用,需要配合 weak_ptr 使用 用于打破循环引用
适用范围 对象需要被共享和自动管理生命周期 对象可能被销毁,需要安全地访问或检测其有效性

weak_ptr 不仅仅是打破循环引用的工具,它在对象缓存、观察者模式、父子关系、避免悬空指针等方面都有着广泛的应用。通过合理地使用 weak_ptr,我们可以编写出更加健壮、高效、易于维护的 C++ 代码。

希望今天的分享对大家有所帮助!下次再见!

发表回复

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