Bootstrap

每日C++ - shared_ptr 的循环引用问题

shared_ptr 的循环引用问题

在 C++ 开发过程中,面对浩繁的代码工作,不免会出现各种 bug,而在智能指针使用方面也存在一些难以察觉的坑。shared_ptr 循环引用造成内存泄漏就是一例。当两个内存对象分别拥有对方的非 nullptr 的 shared_ptr 类型的智能指针,导致 shared_ptr 引用计数无法归 0,从而造成系统内存泄漏。

我们来看下面的代码

class B;
class A
{
public:
    A() = default;
    ~A() { std::cout << "A dtor." << std::endl;}
    std::shared_ptr<B> b;
};
class B
{
public:
    B() = default;
    ~B() { std::cout << "B dtor." << std::endl; }
    std::shared_ptr<A> a;
};

上面就是典型的错误场景,在实际运行过程中,我们无法看到 A,B 的析构。

int main()
{
	std::shared_ptr<A> sa = std::make_shared<A>();
	std::shared_ptr<B> sb = std::make_shared<B>();
	sa->b = sb;
	sb->a = sa;
	return 0;
}

对于 shared_ptr 循环引用的问题,怎么来解决呢?

  • 在实际代码编写过程中,尽量使用 unique_ptr,避免出现循环引用的场景

    unique_ptr 只能独占使用,我们可以通过右值或裸指针将 unique_ptr 的内存对象传递给其它内存对象。

    class B;
    class A
    {
    public:
    	std::unique_ptr<B> b;
    };
    class B
    {
    public:
    	void test() {}
    };
    int main()
    {
    	std::unique_ptr<A> a = std::make_unique<A>();
    	std::unique_ptr<B> b = std::make_unique<B>();
    	a->b = std::move(b);
    	a->b->test();
    	// b->get() is nullptr
    }
    
  • 如果无法避免循环引用,则改用 weak_ptr

    weak_ptr 并不会增加内存对象的引用计数,为避免内存泄漏,循环引用使用 weak_ptr

    class B;
    class A
    {
    public:
        A() = default;
        ~A() { std::cout << "A dtor." << std::endl; }
        void test() { std::cout << "A test." << std::endl; }
        std::weak_ptr<B> b;
    };
    class B
    {
    public:
         B() = default;
         ~B() { std::cout << "B dtor." << std::endl; }
         void test() { std::cout << "B test." << std::endl; }
         std::weak_ptr<A> a;
    };
    
    我们无法通过 weak_ptr 直接调用内存对象的成员和方法,可以通过 weak_ptr 的 lock() 方法获得它的 shared_ptr,然后再调用内存对象的成员和方法。通过 weak_ptr 的 expired() 方法可以判断它所指向的内存对象是否已被释放。
    int main()
    {
          std::shared_ptr<WeakPtr::A> sa = std::make_shared<WeakPtr::A>();
          std::shared_ptr<WeakPtr::B> sb = std::make_shared<WeakPtr::B>();
          sa->b = sb;
          sb->a = sa;
    
          // shared_ptr call test
          sa->test();
          sb->test();
          // weak_ptr call test
          sa->b.lock()->test();
          sb->a.lock()->test();
    
          // released
          std::cout << "B expired = " << sa->b.expired() << std::endl;
          std::cout << "A expired = " << sb->a.expired() << std::endl;
    }
    
    上述代码运行后,A,B 都会被正常析构。
;