首页 > 编程笔记 > C++笔记 阅读:5

C++ std::weak_ptr智能指针详解(附带实例)

C++ 的 std::shared_ptr 出现之前,如果有两个指针指向同一个对象,当一个指针被销毁时,对象也会被销毁,但还有另一个指针指向这个对象,如果再次销毁对象就会出现错误。std::shared_ptr 通过引用计数解决了这个问题,但这也带来了新的问题。

当一个 std::shared_ptr 被销毁时,如果它是指向一个被另一个 std::shared_ptr 所拥有的对象,则这个对象的引用计数就会减一,由于此计数变为 0,所以这个对象会被销毁。这就是所谓的“悬垂指针”(Dangling Pointer)。

另外,如果有两个对象互相引用对方,并且都使用 std::shared_ptr 来管理,则这两个对象的引用计数永远都不会为0,即使它们实际上已经不再被使用。这是因为每次一个对象被销毁时,它都会减少另一个对象的引用计数,这将导致另一个对象的引用计数永远不会为 0。

先来看一个错误使用 shared_ptr 的示例,代码如下:
#include <iostream>
#include <memory>
class B;
class A
{
private:
    std::shared_ptr<B> b_ptr;
public:
    void setB(const std::shared_ptr<B> & b)
    {
        b_ptr = b;
    }
    void doSomething()
    {
        std::cout << "Doing something in A" << std::endl;
    }
};

class B
{
private:
    std::shared_ptr<A> a_ptr;
public:
    void setA(const std::shared_ptr<A> & a)
    {
        a_ptr = a;
    }
    void doSomething()
    {
        std::cout << "Doing something in B" << std::endl;
    }
};

int main()
{
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    //交叉引用,释放后会造成指针的错误释放
    a->setB(b);
    b->setA(a);
    a->doSomething();
    b->doSomething();
    return 0;
}
在这个示例中,类 A 和类 B 相互引用,并且使用 std::shared_ptr 来管理资源。创建了两个 std::shared_ptr 对象 a 和 b,并将它们分别传递给类 A 和类 B 的成员函数 setB() 和 setA()。这样,a 和 b 之间就形成了循环引用。

在 main() 函数中,分别调用 a 和 b 的成员函数 doSomething(),输出相应的结果,然而,当 a 和 b 的引用计数都降为 0 时,它们无法被正确地释放,从而导致内存泄漏。

为了解决这个问题,需要使用 std::weak_ptr 来打破循环引用。

std::weak_ptr 用于共享对象,但不会增加引用计数,因此,它不会影响对象的生命周期。std::weak_ptr 指向的对象由 std::shared_ptr 管理。当最后一个 std::shared_ptr 对象被销毁时,如果没有其他 std::weak_ptr 对象指向该对象,则该对象会被销毁,否则该对象将继续存在。

std::weak_ptr 不支持对所指对象进行直接访问,需要通过 lock() 函数获取一个 std::shared_ptr 对象访问所指对象。lock() 函数会检查所指对象是否存在,如果存在,则返回一个指向该对象的 std::shared_ptr,否则返回一个空的 std::shared_ptr。

使用 std::weak_ptr 的一个常见场景是解决循环引用问题。当两个或多个对象相互引用且其中至少一个对象使用 std::shared_ptr 来管理资源时,可能会出现循环引用。这会导致资源无法释放,从而造成内存泄漏。

将其中一个对象的句柄使用 std::weak_ptr 而不是 std::shared_ptr,在销毁其中的一个对象时,它的 std::shared_ptr 计数将减少,但不会阻止对象的销毁。这样可以打破循环引用,从而释放资源。

需要注意的是,由于 std::weak_ptr 不会增加引用计数,所以在使用 lock() 函数获取 std::shared_ptr 时,一定要对返回的指针进行有效性检查以确保所指对象仍然存在。

以下是一个简单的示例,代码如下:
#include <iostream>
#include <memory>

class B; // 前向声明

class A
{
private:
    std::weak_ptr<B> b_ptr;

public:
    void setB(const std::shared_ptr<B>& b)
    {
        b_ptr = b;
    }

    void doSomething(); // 先声明,稍后定义(放在 B 完整定义之后)
};

class B
{
private:
    std::weak_ptr<A> a_ptr;

public:
    void setA(const std::shared_ptr<A>& a)
    {
        a_ptr = a;
    }

    void doSomething()
    {
        std::shared_ptr<A> a = a_ptr.lock();
        if (a) {
            std::cout << "Doing something in B" << std::endl;
        } else {
            std::cout << "A does not exist anymore" << std::endl;
        }
    }
};

// ?? 这里是 A::doSomething() 的定义,放到 B 的定义之后
void A::doSomething()
{
    std::shared_ptr<B> b = b_ptr.lock();
    if (b) {
        std::cout << "Doing something in A" << std::endl;
        b->doSomething(); // 现在可以访问 B 的成员函数了
    } else {
        std::cout << "B does not exist anymore" << std::endl;
    }
}

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

    a->setB(b);
    b->setA(a);

    a->doSomething(); // 输出 "Doing something in A" 和 "Doing something in B"

    a = nullptr;      // 销毁 A
    b->doSomething(); // 输出 "A does not exist anymore"

    b = nullptr;      // 销毁 B

    return 0;
}
在这个示例中,类 A 和类 B 相互引用,并且使用 std::shared_ptr 来管理资源。通过在类 A 和类 B 中使用 std::weak_ptr 来引用对方,可以打破循环引用。当其中的一个对象被销毁时,另一个对象可以安全地访问 std::weak_ptr,以此来判断对方是否还存在,并进行相应操作。

相关文章