循环引用为何引发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).
疑问点
- funct函数返回后局部变量cl2已销毁,为何cl1的use_count仍为2?除cl1外,谁还持有cl1指向对象的所有权?
- 有评论称执行
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
相关产品推荐
相关产品推荐

