MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ std::weak_ptr 监测对象生命周期

2023-12-076.3k 阅读

C++ std::weak_ptr 监测对象生命周期

一、std::weak_ptr 简介

在 C++ 中,std::weak_ptr 是智能指针家族的一员,它主要用于解决 std::shared_ptr 带来的循环引用问题,同时还能用于监测对象的生命周期。std::weak_ptr 指向由 std::shared_ptr 管理的对象,但它并不拥有对象的所有权,不会影响对象的引用计数。这意味着当最后一个 std::shared_ptr 释放对象时,即使存在指向该对象的 std::weak_ptr,对象也会被销毁。

std::weak_ptr 提供了一种观察 std::shared_ptr 所管理对象的手段,我们可以通过 std::weak_ptr 来判断其所指向的对象是否还存在,并且在对象存在时获取一个 std::shared_ptr 来访问该对象。

二、std::weak_ptr 的创建

  1. 从 std::shared_ptr 创建 最常见的方式是从 std::shared_ptr 创建 std::weak_ptr。例如:
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr(sharedPtr);

    return 0;
}

在上述代码中,首先创建了一个 std::shared_ptr 指向一个动态分配的 int 对象,值为 42。然后通过这个 std::shared_ptr 创建了一个 std::weak_ptr,此时 weakPtr 指向了 sharedPtr 所管理的对象。

  1. 默认构造 std::weak_ptr 也可以默认构造,默认构造的 std::weak_ptr 不指向任何对象。
std::weak_ptr<int> weakPtr;

三、std::weak_ptr 的成员函数

  1. expired() expired() 函数用于检查 std::weak_ptr 所指向的对象是否已经被销毁。如果对象已被销毁(即对应的 std::shared_ptr 的引用计数为 0),则返回 true,否则返回 false
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr(sharedPtr);

    std::cout << "Is the object expired? " << (weakPtr.expired()? "Yes" : "No") << std::endl;

    sharedPtr.reset();
    std::cout << "Is the object expired? " << (weakPtr.expired()? "Yes" : "No") << std::endl;

    return 0;
}

在上述代码中,首先创建了 sharedPtrweakPtr,此时 weakPtr.expired() 返回 false。然后通过 reset() 函数释放 sharedPtr 对对象的所有权,此时 weakPtr.expired() 返回 true

  1. lock() lock() 函数用于获取一个指向 std::weak_ptr 所指向对象的 std::shared_ptr。如果对象已被销毁(即 expired() 返回 true),则 lock() 返回一个空的 std::shared_ptr
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr(sharedPtr);

    std::shared_ptr<int> lockedPtr = weakPtr.lock();
    if (lockedPtr) {
        std::cout << "The value is: " << *lockedPtr << std::endl;
    } else {
        std::cout << "The object has been destroyed." << std::endl;
    }

    sharedPtr.reset();
    lockedPtr = weakPtr.lock();
    if (lockedPtr) {
        std::cout << "The value is: " << *lockedPtr << std::endl;
    } else {
        std::cout << "The object has been destroyed." << std::endl;
    }

    return 0;
}

在上述代码中,第一次调用 lock() 时,对象还存在,所以能成功获取到 std::shared_ptr 并输出对象的值。第二次调用 lock() 时,sharedPtr 已释放对象,lock() 返回空的 std::shared_ptr,输出对象已被销毁的信息。

四、解决循环引用问题

  1. 循环引用示例 假设我们有两个类 AB,它们相互引用,如下:
#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b;
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> a;
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b = b;
    b->a = a;

    return 0;
}

在上述代码中,A 类和 B 类都包含一个指向对方类型的 std::shared_ptr。当 ab 超出作用域时,由于循环引用,AB 对象的引用计数都不会降为 0,导致内存泄漏。

  1. 使用 std::weak_ptr 解决循环引用 我们可以将其中一个引用改为 std::weak_ptr 来打破循环引用。例如,将 B 类中的 a 改为 std::weak_ptr
#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b;
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> a;
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->b = b;
    b->a = a;

    return 0;
}

在这种情况下,当 ab 超出作用域时,A 对象的引用计数为 1(a 指向它),B 对象的引用计数也为 1(a->b 指向它)。当 a 被销毁时,A 对象的引用计数降为 0,A 对象被销毁,此时 b->a 所指向的对象已不存在。接着 b 被销毁,B 对象的引用计数降为 0,B 对象也被销毁,从而避免了内存泄漏。

五、监测对象生命周期的应用场景

  1. 缓存系统 在缓存系统中,我们可能希望缓存一些对象,但又不想因为缓存而延长对象的生命周期。可以使用 std::weak_ptr 来存储缓存对象的引用。当需要使用缓存对象时,通过 lock() 函数获取 std::shared_ptr,如果对象还存在则使用,否则重新创建。
#include <iostream>
#include <memory>
#include <unordered_map>

class ExpensiveObject {
public:
    ExpensiveObject(int value) : data(value) {
        std::cout << "ExpensiveObject created with value: " << data << std::endl;
    }
    ~ExpensiveObject() {
        std::cout << "ExpensiveObject destroyed with value: " << data << std::endl;
    }
    int data;
};

class Cache {
public:
    std::shared_ptr<ExpensiveObject> getObject(int key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            std::shared_ptr<ExpensiveObject> obj = it->second.lock();
            if (obj) {
                return obj;
            } else {
                cache.erase(it);
            }
        }
        std::shared_ptr<ExpensiveObject> newObj = std::make_shared<ExpensiveObject>(key);
        cache[key] = newObj;
        return newObj;
    }
private:
    std::unordered_map<int, std::weak_ptr<ExpensiveObject>> cache;
};

int main() {
    Cache cache;
    std::shared_ptr<ExpensiveObject> obj1 = cache.getObject(1);
    std::shared_ptr<ExpensiveObject> obj2 = cache.getObject(1);

    return 0;
}

在上述代码中,Cache 类使用 std::unordered_map 来存储 std::weak_ptrgetObject 函数首先检查缓存中是否存在对象,如果存在且对象未被销毁,则返回缓存的对象,否则创建新对象并缓存。

  1. 观察者模式 在观察者模式中,观察者可能希望观察被观察对象,但又不希望阻止被观察对象的销毁。可以使用 std::weak_ptr 来存储对被观察对象的引用。当被观察对象状态改变时,观察者通过 lock() 函数获取 std::shared_ptr 来访问被观察对象,如果对象已被销毁,则可以进行相应处理。
#include <iostream>
#include <memory>
#include <vector>

class Subject;

class Observer {
public:
    virtual void update(const Subject& subject) = 0;
    virtual ~Observer() = default;
};

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 (const auto& observer : observers) {
            std::shared_ptr<Observer> lockedObserver = observer.lock();
            if (lockedObserver) {
                lockedObserver->update(*this);
            }
        }
    }
private:
    std::vector<std::weak_ptr<Observer>> observers;
};

class ConcreteObserver : public Observer {
public:
    void update(const Subject& subject) override {
        std::cout << "ConcreteObserver received update" << std::endl;
    }
};

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

    subject->attach(observer);
    subject->notify();

    observer.reset();
    subject->notify();

    return 0;
}

在上述代码中,Subject 类使用 std::vector<std::weak_ptr<Observer>> 来存储观察者。当 Subject 通知观察者时,首先通过 lock() 函数获取 std::shared_ptr,如果观察者对象还存在则调用其 update 方法,避免了悬空指针问题。

六、std::weak_ptr 的实现原理

std::weak_ptr 的实现通常依赖于控制块(control block)。当使用 std::make_sharedstd::shared_ptr 的构造函数创建一个 std::shared_ptr 时,会同时创建一个控制块。控制块中包含了对象的引用计数(shared count)和弱引用计数(weak count)。

std::shared_ptr 增加或减少引用计数时,会操作控制块中的共享引用计数。而 std::weak_ptr 的创建、销毁和赋值操作则会影响控制块中的弱引用计数。当共享引用计数降为 0 时,对象被销毁,但只要弱引用计数不为 0,控制块就会一直存在,直到最后一个 std::weak_ptr 被销毁,此时控制块也会被释放。

std::weak_ptrlock() 函数实现原理是检查控制块中的共享引用计数是否大于 0,如果大于 0,则创建一个新的 std::shared_ptr 指向对象,并增加共享引用计数,然后返回这个新的 std::shared_ptr;如果共享引用计数为 0,则返回一个空的 std::shared_ptr

七、注意事项

  1. 线程安全 std::weak_ptr 的大部分操作(如创建、销毁、expired()lock() 等)在多线程环境下不是线程安全的。如果在多线程中使用 std::weak_ptr,需要进行适当的同步操作,例如使用互斥锁来保护对 std::weak_ptr 的访问。
  2. 空指针检查 在使用 std::weak_ptrlock() 函数获取 std::shared_ptr 后,一定要检查返回的 std::shared_ptr 是否为空,以避免空指针解引用错误。

八、总结

std::weak_ptr 是 C++ 中一个非常有用的工具,它不仅能解决 std::shared_ptr 带来的循环引用问题,还能用于监测对象的生命周期。在实际编程中,特别是在处理复杂的数据结构和对象关系时,合理使用 std::weak_ptr 可以有效避免内存泄漏和悬空指针等问题,提高程序的稳定性和可靠性。通过深入理解 std::weak_ptr 的创建、成员函数、应用场景、实现原理以及注意事项,开发者能够更好地利用这一特性,编写出高质量的 C++ 代码。无论是在缓存系统、观察者模式还是其他需要监测对象生命周期的场景中,std::weak_ptr 都能发挥重要作用,为开发者提供更加灵活和强大的编程能力。在多线程环境中使用时,虽然需要额外注意线程安全问题,但只要采取合适的同步机制,仍然可以安全有效地使用 std::weak_ptr。希望通过本文的介绍,读者对 std::weak_ptr 有了更深入的理解,并能在实际项目中熟练运用。