You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

循环引用为何引发shared_ptr间接内存泄漏?解析核心疑问

C++ shared_ptr循环引用内存泄漏问题解析

通过StackOverflow相关问题了解到循环引用会引发内存泄漏(尤其是间接泄漏),为探究该问题,编写了如下C++代码构建循环引用场景:

#include <iostream>
#include <memory>
using namespace std; 

class MyClass {
public:
    MyClass()                                         { cout<<"Default constructor"<<endl; }
    MyClass(MyClass const& s)                         { cout<<"Copy constructor"<<endl; }
    MyClass(MyClass&& s) noexcept                     { cout<<"Move constructor"<<endl; }
    auto operator= (MyClass const& s)     -> MyClass& { cout<<"Copy Assignment"<<endl; return *this; }
    auto operator= (MyClass&& s) noexcept -> MyClass& { cout<<"Move assignment"<<endl; return *this; }
    ~MyClass()                                        { cout<<"Destructor"<<endl; }
    shared_ptr<MyClass> next;
};

void funct(shared_ptr<MyClass> & cl1){
    shared_ptr<MyClass> cl2 = make_shared<MyClass>();
    cout<<"**init cl2**"<<endl;
    cout<<"address cl2 points to: "<<cl2.get()<<endl;
    cout<<"count reference of cl2: "<<cl2.use_count()<<endl;
    cout<<"**link cl2 <-> cl1**"<<endl;
    cl2->next = cl1;
    cl1->next = cl2;
    cout<<"address cl1 points to:  "<<cl1.get()<<endl;
    cout<<"count reference of cl1: "<<cl1.use_count()<<endl;
    cout<<"address cl2 points to:  "<<cl2.get()<<endl;
    cout<<"count reference of cl2: "<<cl2.use_count()<<endl;
    // cl2 is deleted after exit of this function
}

int main()
{
    cout<<"Size of MyClass "<<sizeof(MyClass)<<endl;

    cout<<"**init cl2**"<<endl;
    shared_ptr<MyClass> cl1 = make_shared<MyClass>();

    funct(cl1);

    cout<<"**cl2 is destroy**"<<endl;
    cout<<"count reference of cl1: "<<cl1.use_count()<<endl;
    cout<<"count reference of cl1->next "<<cl1->next.use_count()<<endl;
    cout<<"address cl1->next points to "<<cl1->next<<endl;   // print the object's address where cl2 points at.  
    return 0;
}

运行后控制台输出如下:

Size of MyClass 16
**init cl2**
Default constructor
Default constructor
**init cl2**
address cl2 points to: 0x503000000050
count reference of cl2: 1
**link cl2 <-> cl1**
address cl1 points to:  0x503000000020
count reference of cl1: 2
address cl2 points to:  0x503000000050
count reference of cl2: 2
**cl2 is destroy**
count reference of cl1: 2
count reference of cl1->next 1
address cl1->next points to 0x503000000050
Program stderr

=================================================================
==1==ERROR: LeakSanitizer: detected memory leaks
Indirect leak of 32 byte(s) in 1 object(s) allocated from:
    #0 0x55ea5a0f420d in operator new(unsigned long) /root/llvm-project/compiler-rt/lib/asan/asan_new_delete.cpp:95:3
    #1 0x55ea5a0f8717 in std::__new_allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>::allocate(unsigned long, void const*) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/new_allocator.h:147:27
    #2 0x55ea5a0f8293 in std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>>::allocate(std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>&, unsigned long) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/alloc_traits.h:482:20
    #3 0x55ea5a0f8293 in std::__allocated_ptr<std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>> std::__allocate_guarded<std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>>(std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>&) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/allocated_ptr.h:98:21
    #4 0x55ea5a0f7fd8 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<MyClass, std::allocator<void>>(MyClass*&, std::_Sp_alloc_shared_tag<std::allocator<void>>) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr_base.h:969:19
    #5 0x55ea5a0f7d94 in std::__shared_ptr<MyClass, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void>>) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr_base.h:1712:14
    #6 0x55ea5a0f7b9f in std::shared_ptr<MyClass>::shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void>>) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr.h:464:4
    #7 0x55ea5a0f6e72 in std::shared_ptr<std::enable_if<!is_array<MyClass>::value, MyClass>::type> std::make_shared<MyClass>() /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr.h:1009:14
    #8 0x55ea5a0f65de in funct(std::shared_ptr<MyClass>&) /app/example.cpp:17:31
    #9 0x55ea5a0f6ad9 in main /app/example.cpp:38:5
    #10 0x7f80ea002082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)

Indirect leak of 32 byte(s) in 1 object(s) allocated from:
    #0 0x55ea5a0f420d in operator new(unsigned long) /root/llvm-project/compiler-rt/lib/asan/asan_new_delete.cpp:95:3
    #1 0x55ea5a0f8717 in std::__new_allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>::allocate(unsigned long, void const*) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/new_allocator.h:147:27
    #2 0x55ea5a0f8293 in std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>>::allocate(std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>&, unsigned long) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/alloc_traits.h:482:20
    #3 0x55ea5a0f8293 in std::__allocated_ptr<std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>> std::__allocate_guarded<std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>>(std::allocator<std::_Sp_counted_ptr_inplace<MyClass, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>>&) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/allocated_ptr.h:98:21
    #4 0x55ea5a0f7fd8 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<MyClass, std::allocator<void>>(MyClass*&, std::_Sp_alloc_shared_tag<std::allocator<void>>) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr_base.h:969:19
    #5 0x55ea5a0f7d94 in std::__shared_ptr<MyClass, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void>>) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr_base.h:1712:14
    #6 0x55ea5a0f7b9f in std::shared_ptr<MyClass>::shared_ptr<std::allocator<void>>(std::_Sp_alloc_shared_tag<std::allocator<void>>) /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr.h:464:4
    #7 0x55ea5a0f6e72 in std::shared_ptr<std::enable_if<!is_array<MyClass>::value, MyClass>::type> std::make_shared<MyClass>() /opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/bits/shared_ptr.h:1009:14
    #8 0x55ea5a0f6acd in main /app/example.cpp:36:31
    #9 0x7f80ea002082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082) (BuildId: 1878e6b475720c7c51969e69ab2d276fae6d1dee)

SUMMARY: AddressSanitizer: 64 byte(s) leaked in 2 allocation(s).

疑问点

  1. funct函数返回后局部变量cl2已销毁,为何cl1的use_count仍为2?除cl1外,谁还持有cl1指向对象的所有权?
  2. 有评论称执行cl1->next = cl2时会复制智能指针,cl2生命周期结束后该副本仍存活并指向同一内存,如何证明这一点?

问题解答

问题1解答

当执行cl2->next = cl1时,cl2指向的对象的next成员会复制cl1这个shared_ptr,这会让cl1指向对象的引用计数+1。此时cl1指向的对象有两个所有者:一个是main函数里的cl1,另一个是cl2指向对象的next成员。

当funct函数返回,局部变量cl2销毁时,它的引用计数从2减到1(因为cl1->next还持有对cl2指向对象的引用),但cl1的引用计数不会减少——因为cl2指向对象的next成员依然持有cl1指向对象的引用。所以funct结束后,cl1的use_count还是2,另一个持有者就是cl2指向对象的next智能指针。

问题2解答

可以通过三种方式验证:

  • 观察引用计数变化:在执行cl1->next = cl2前后打印cl2的use_count,执行前是1,执行后会变成2,说明有新的shared_ptr副本指向了同一个对象,这个副本就是cl1->next。
  • 对比指针地址:在funct结束后,打印cl1->next.get(),会发现它和funct里cl2.get()的地址完全一致,证明两者指向同一块内存。
  • 查看析构函数输出:程序结束时,控制台没有输出Destructor,说明两个MyClass对象都没被释放——因为它们互相持有对方的shared_ptr,引用计数永远无法降到0,间接证明了副本存活且持续占用内存。

内容的提问来源于stack exchange,提问作者Minh Pham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 19:53:09