在C++中,智能指针是一种自动管理内存分配和释放的机制,它帮助开发者避免内存泄漏。shared_ptr和weak_ptr是两种常见的智能指针类型,它们通过引用计数来管理对象的生命周期。然而,有时候可能会遇到内存没有被立即释放的情况,这可能会让人感到困惑。本文将深入探讨这种情况的原因,并解释shared_ptr和weak_ptr是如何相互作用的。
首先,来看一个具体的例子。假设有一个大型的托管对象,它的大小足以引起注意。这个对象可能包含其他指针或容器,这些指针或容器会分配自己的内存块,但它们对最终对象大小的贡献不大(除了几个指针)。例如,std::vector会为存储的元素分配内存。
shared_ptr<MyLargeType> sharedPtr;
{
auto sharedPtr = make_shared<MyLargeType>();
weakPtr = sharedPtr;
// ...
}
cout << "作用域结束...\n";
在上面的代码中,有两个作用域:内部作用域用于shared_ptr,外部作用域用于weak_ptr(注意这个weak_ptr只持有一个“弱”引用,它没有使用lock()来创建强引用)。当shared_ptr离开内部作用域时,它应该销毁托管对象……对吗?
当最后一个shared_ptr消失时,它会销毁对象,调用MyLargeType的析构函数(这也会释放成员分配的单独内存块,如向量、字符串等)……但是分配给对象的内存呢?能释放它吗?为了回答这个问题,让考虑第二个例子:
weak_ptr<MyLargeType> weakPtr;
{
shared_ptr<MyLargeType> sharedPtr(new MyLargeType());
weakPtr = sharedPtr;
// ...
}
cout << "作用域结束...\n";
几乎和上面的代码一样……对吗?唯一的区别在于创建shared_ptr的方法:这里使用显式的new。让看看运行这两个例子时的输出。
为了获得有用的信息,需要重写全局new和delete,并报告何时调用示例类的析构函数。
void *operator new(size_t count) {
cout << "分配 " << count << " 字节\n";
return malloc(count);
}
void operator delete(void *ptr) noexcept {
cout << "全局op delete被调用\n";
free(ptr);
}
struct MyLargeType {
~MyLargeType() { cout << "析构函数 MyLargeType\n"; }
private:
int arr[100];
};
现在让看看输出:
对于make_shared:
分配 416 字节
析构函数 MyLargeType
作用域结束...
全局op delete被调用
对于显式new的情况:
分配 400 字节
分配 24 字节
析构函数 MyLargeType
全局op delete被调用
作用域结束...
全局op delete被调用
第一个重要的观察是,正如可能已经知道的,make_shared只会执行一次内存分配。对于显式的new,有两个单独的内存分配。
需要空间来存储两样东西:对象和控制块。控制块是实现依赖的,但它持有对象的指针和引用计数。还有一些其他东西(如自定义删除器、分配器等)。
当使用显式的new时,有两个单独的内存块。所以当最后一个shared_ptr消失时,可以销毁对象并释放内存。
所以看到输出:
析构函数 MyLargeType
全局op delete被调用
析构函数和free()都被调用了 - 在作用域结束之前。
然而,当使用make_shared()创建shared指针时,托管对象位于与控制块相同的内存块中。
故事中的内存分配和清理很有趣……但这对有多大影响?可能影响不大。不应该因为这个原因而停止使用make_shared!
这种情况相当罕见。然而,了解这种行为并在实现依赖于shared和weak指针的复杂系统时记住它,这是很好的。
例如,想到了Herb Sutter在2013年的GoingNative会议上介绍的并发弱字典数据结构:
如果理解正确的话:make_shared会为控制块和widget分配一个内存块。所以当所有shared指针都死了,weak指针会活在缓存中……这也会导致整个内存块也存在。(析构函数被调用,但内存不能被释放)。
为了增强解决方案,应该实现一些额外的机制,定期清理未使用的weak指针。
整篇文章是一次引人入胜的调查!有时候,会发现自己在一些可能不是特别关键的事情上花费太多时间。尽管如此,它们仍然很吸引人。很高兴能以文章的形式分享这个。
整个调查的底线是,shared_ptr/make_shared的实现相当复杂。当控制块与托管对象分配在相同的内存块中时,需要特别小心地释放分配的内存。