std::shared_ptr 的引用计数器解析:控制块为何独立分配?
各位同仁,女士们,先生们,
欢迎来到今天的技术讲座。我们将深入探讨 C++ 智能指针家族中最为常用、也最为复杂的成员之一:std::shared_ptr。特别是,我们将聚焦于其核心机制——引用计数器,并详细解析一个关键设计决策:为什么它的控制块(Control Block)需要被单独分配。理解这一点,对于掌握 std::shared_ptr 的性能特性、内存管理细节乃至潜在的陷阱,都至关重要。
1. 引言:智能指针与资源管理
在 C++ 编程中,资源管理始终是一个核心挑战。内存、文件句柄、网络连接、数据库事务等,都属于需要严格管理以避免泄露或滥用的资源。C++ 引入了 RAII(Resource Acquisition Is Initialization)原则,通过将资源生命周期与对象生命周期绑定,在对象构造时获取资源,在对象析构时释放资源,从而实现自动化的资源管理。
原始指针虽然强大,但其手动管理模式极易出错:忘记 delete 导致内存泄露,重复 delete 导致未定义行为,或者在异常发生时未能正确释放资源。为了解决这些问题,C++ 标准库提供了智能指针,它们是 RAII 思想的典范。
std::unique_ptr 实现了独占所有权语义,确保资源只有一个拥有者。而 std::shared_ptr 则旨在解决共享所有权的问题。当多个智能指针需要共同管理同一个资源,并在所有拥有者都放弃管理时自动释放资源,std::shared_ptr 便应运而生。它的核心正是基于引用计数(Reference Counting)机制。
2. std::shared_ptr 的核心机制:引用计数
std::shared_ptr 的设计理念是允许多个智能指针实例共同拥有一个对象。当最后一个 std::shared_ptr 实例被销毁或重置时,它所管理的对象才会被自动删除。为了实现这一目标,std::shared_ptr 内部维护了一个引用计数器。
2.1 结构概览
一个 std::shared_ptr 对象在逻辑上可以看作是两个指针的封装:
- 指向被管理对象的指针 (Managed Object Pointer):这是一个原始指针,直接指向
std::shared_ptr所管理的用户对象,例如T*。 - 指向控制块的指针 (Control Block Pointer):这是一个指向内部管理结构(即控制块)的指针。控制块是
std::shared_ptr实现其功能的关键,它包含了引用计数器以及其他一些元数据。
这使得 std::shared_ptr 对象本身非常轻量级,通常只有两个原始指针的大小。
2.2 引用计数器的类型
控制块内部主要包含两种类型的引用计数:
-
强引用计数 (Strong Reference Count /
shared_count):- 表示有多少个
std::shared_ptr实例正在管理该对象。 - 当一个
std::shared_ptr被构造、拷贝或赋值时,强引用计数增加。 - 当一个
std::shared_ptr被销毁、重置或赋值给另一个对象时,强引用计数减少。 - 关键作用:当强引用计数降为零时,意味着不再有
std::shared_ptr实例拥有该对象,此时被管理的对象将被销毁(通过调用其析构函数和/或自定义删除器)。
- 表示有多少个
-
弱引用计数 (Weak Reference Count /
weak_count):- 表示有多少个
std::weak_ptr实例或std::shared_ptr实例(间接通过std::weak_ptr转换而来)正在观察该对象。 std::weak_ptr是一种非拥有型智能指针,它不增加强引用计数,因此不会阻止被管理对象的销毁。- 当一个
std::weak_ptr被构造、拷贝或赋值时,弱引用计数增加。 - 当一个
std::weak_ptr被销毁、重置或赋值给另一个对象时,弱引用计数减少。 - 关键作用:当强引用计数和弱引用计数都降为零时,意味着不仅被管理的对象已经被销毁,而且不再有任何
std::shared_ptr或std::weak_ptr实例需要访问控制块。此时,控制块本身才会被销毁。
- 表示有多少个
2.3 生命周期管理
std::shared_ptr 的生命周期管理流程如下:
- 对象生命周期:由强引用计数 (
shared_count) 管理。一旦shared_count变为 0,被管理的对象将被销毁。 - 控制块生命周期:由强引用计数 (
shared_count) 和弱引用计数 (weak_count) 共同管理。只有当shared_count和weak_count都变为 0 时,控制块才会被销毁。
这种分离的生命周期管理是理解控制块独立分配的关键原因之一。
3. 控制块 (Control Block) 的构成与职责
控制块不仅仅是一个简单的计数器,它是一个复合结构,承载着 std::shared_ptr 正常运作所需的多种信息和职责。
3.1 内部结构
尽管标准没有规定控制块的具体实现,但通常它会包含以下关键组件:
shared_count(强引用计数):一个原子整数类型,用于安全地处理并发访问。weak_count(弱引用计数):另一个原子整数类型,同样用于并发访问。deleter(删除器):一个类型擦除的对象,存储了用于销毁被管理对象的函数或函数对象。这可以是默认的delete操作,也可以是用户自定义的删除逻辑。allocator(分配器):一个类型擦除的对象,存储了用于分配和释放被管理对象内存的分配器。这允许用户指定自定义的内存分配策略。- 原始指针(可选或隐式):在某些实现中,控制块可能直接包含一个指向被管理对象的原始指针。但在
std::make_shared的优化场景下,被管理对象可能直接嵌入在控制块分配的内存中,此时控制块可能不需要单独存储一个原始指针,而是通过偏移量来访问对象。
3.2 职责总结
控制块在 std::shared_ptr 生态系统中扮演着多重角色:
- 存储引用计数:这是其最核心的职责,维护
shared_count和weak_count。 - 存储删除器:它负责记住如何正确地销毁被管理的对象。这使得
std::shared_ptr能够管理任意类型的资源,而不仅仅是new出来的堆内存。 - 存储分配器:它负责记住如何分配和释放被管理对象的内存。
- 管理被管理对象的生命周期:通过
shared_count控制对象的创建和销毁。 - 管理自身的生命周期:通过
shared_count和weak_count共同控制控制块自身的创建和销毁。 - 提供类型擦除机制:为了支持任意类型的对象、删除器和分配器,控制块内部通常会使用类型擦除技术(例如虚函数或
std::function内部机制)来存储和调用这些可变组件。
4. 核心问题:为什么控制块是单独分配的?
现在我们来到了本次讲座的核心议题。理解控制块的独立分配,是深入掌握 std::shared_ptr 设计哲学和性能特点的关键。
4.1 场景一:通过原始指针构造 std::shared_ptr
#include <iostream>
#include <memory>
#include <vector>
class MyObject {
public:
int id;
MyObject(int i) : id(i) {
std::cout << "MyObject " << id << " constructed." << std::endl;
}
~MyObject() {
std::cout << "MyObject " << id << " destructed." << std::endl;
}
};
void demo_raw_ptr_construction() {
std::cout << "--- Demo: Raw Pointer Construction ---" << std::endl;
// 第一次内存分配:为 MyObject 对象分配内存
MyObject* raw_ptr = new MyObject(101);
// 第二次内存分配:为控制块分配内存
std::shared_ptr<MyObject> s_ptr1(raw_ptr);
std::cout << "s_ptr1 strong count: " << s_ptr1.use_count() << std::endl;
std::shared_ptr<MyObject> s_ptr2 = s_ptr1;
std::cout << "s_ptr1 strong count: " << s_ptr1.use_count() << std::endl;
std::cout << "Exiting demo_raw_ptr_construction scope." << std::endl;
} // s_ptr1 and s_ptr2 go out of scope, MyObject 101 destructed
/*
预期输出:
--- Demo: Raw Pointer Construction ---
MyObject 101 constructed.
s_ptr1 strong count: 1
s_ptr1 strong count: 2
Exiting demo_raw_ptr_construction scope.
MyObject 101 destructed.
*/
在上述代码中,当使用 std::shared_ptr<MyObject> s_ptr1(new MyObject(101)); 或 std::shared_ptr<MyObject> s_ptr1(raw_ptr); 这种方式构造 std::shared_ptr 时,会发生两次独立的内存分配:
new MyObject(101):为MyObject对象本身分配内存。std::shared_ptr构造函数内部:为控制块分配内存。
那么,为什么必须是两次分配,为什么控制块不能直接成为被管理对象的一部分?
4.1.1 原因一:解耦对象分配与控制块分配 (Decoupling Allocation)
std::shared_ptr 的强大之处在于它不仅仅能管理通过 new 在堆上分配的对象。它能管理任何类型的资源,只要你能提供一个合适的删除器。
- 管理栈上对象? 理论上可以,但
shared_ptr通常用于管理堆内存。然而,你可以用shared_ptr管理一个全局变量或静态变量,或者一个自定义存储位置的对象,只要你提供一个空删除器或一个不执行delete的删除器。// 示例:管理一个不需要删除的对象 int global_int = 42; std::shared_ptr<int> s_ptr_global(&global_int, [](int*){ /* do nothing */ }); // 控制块仍然需要分配,但 global_int 不会被删除 - 管理 C 风格数组? 可以,通过提供自定义删除器。
void custom_array_deleter(int* arr) { std::cout << "Custom array deleter called." << std::endl; delete[] arr; } std::shared_ptr<int[]> s_ptr_arr(new int[10], custom_array_deleter); // 这里的 `new int[10]` 分配了数组内存,控制块单独分配 - 管理文件句柄、数据库连接等非内存资源? 同样可以,只需提供一个关闭句柄或断开连接的删除器。
FILE* f = fopen("test.txt", "w"); std::shared_ptr<FILE> file_ptr(f, [](FILE* fp){ if (fp) { std::cout << "Closing file." << std::endl; fclose(fp); } }); // FILE* f 是由 C 库函数分配的资源,控制块是 C++ 运行时分配的如果控制块必须是被管理对象的一部分,那么对于那些不是通过
new分配的对象(例如,栈对象、全局对象、或由第三方库分配的资源),std::shared_ptr将无法管理它们,因为没有地方可以“插入”控制块。独立分配的控制块允许std::shared_ptr能够包装(wrap)任何原始指针,而无需关心原始指针指向的内存是如何分配的。这极大地增强了std::shared_ptr的灵活性。
4.1.2 原因二:不同的生命周期 (Different Lifetimes)
这是最核心、也是最重要的原因。
- 被管理对象的生命周期:由
shared_count决定。当shared_count降为 0 时,被管理对象被销毁。 - 控制块的生命周期:由
shared_count和weak_count共同决定。只有当两者都降为 0 时,控制块才会被销毁。
考虑 std::weak_ptr 的存在。std::weak_ptr 不拥有对象,因此它不增加 shared_count。但它需要知道被管理的对象是否还存在,以及在对象销毁后,控制块是否仍然需要存在以响应 weak_ptr::lock() 调用。
#include <iostream>
#include <memory>
class Resource {
public:
int value;
Resource(int v) : value(v) {
std::cout << "Resource " << value << " constructed." << std::endl;
}
~Resource() {
std::cout << "Resource " << value << " destructed." << std::endl;
}
};
void demo_weak_ptr_lifetime() {
std::cout << "--- Demo: Weak Ptr and Control Block Lifetime ---" << std::endl;
std::shared_ptr<Resource> s_ptr;
std::weak_ptr<Resource> w_ptr;
{
std::shared_ptr<Resource> temp_s_ptr(new Resource(202)); // 对象和控制块都被分配
s_ptr = temp_s_ptr;
w_ptr = temp_s_ptr; // weak_ptr 增加弱引用计数
std::cout << "Inside scope: s_ptr strong count = " << s_ptr.use_count() << ", weak count = " << w_ptr.use_count() << std::endl;
} // temp_s_ptr goes out of scope, strong count for Resource 202 drops to 1
std::cout << "Outside scope, s_ptr still alive. s_ptr strong count = " << s_ptr.use_count() << ", w_ptr weak count = " << w_ptr.use_count() << std::endl;
// 模拟 s_ptr 销毁,但 w_ptr 仍然存在
s_ptr.reset(); // Resource 202 被销毁,但控制块仍然存在,因为 w_ptr 仍然引用它
std::cout << "After s_ptr.reset(): Object should be destructed." << std::endl;
std::cout << "w_ptr expired? " << w_ptr.expired() << std::endl; // 应该为 true
std::cout << "w_ptr weak count (after s_ptr reset): " << w_ptr.use_count() << std::endl; // 此时 weak_count 仍为 1
// 尝试从 weak_ptr 获取 shared_ptr
std::shared_ptr<Resource> locked_ptr = w_ptr.lock();
if (locked_ptr) {
std::cout << "Locked ptr value: " << locked_ptr->value << std::endl; // 不会执行,因为对象已销毁
} else {
std::cout << "Cannot lock weak_ptr, object has been destructed." << std::endl;
}
std::cout << "Exiting demo_weak_ptr_lifetime scope. w_ptr goes out of scope." << std::endl;
} // w_ptr goes out of scope, weak count drops to 0, controlling block is destructed.
/*
预期输出:
--- Demo: Weak Ptr and Control Block Lifetime ---
Resource 202 constructed.
Inside scope: s_ptr strong count = 2, weak count = 1
Outside scope, s_ptr still alive. s_ptr strong count = 1, w_ptr weak count = 1
Resource 202 destructed.
After s_ptr.reset(): Object should be destructed.
w_ptr expired? 1
w_ptr weak count (after s_ptr reset): 1
Cannot lock weak_ptr, object has been destructed.
Exiting demo_weak_ptr_lifetime scope. w_ptr goes out of scope.
*/
在这个例子中,当 s_ptr.reset() 被调用时,Resource 202 对象被销毁(因为 shared_count 降为 0)。但是,w_ptr 仍然存在,它需要访问控制块来判断对象是否已过期 (w_ptr.expired())。因此,控制块不能立即被销毁。它必须等待 w_ptr 也被销毁后,weak_count 降为 0,才会被销毁。
如果控制块是对象的一部分,那么当对象被销毁时,控制块也会随之销毁。这将导致 std::weak_ptr 无法判断其所观察的对象是否仍然存活,从而破坏了 std::weak_ptr 的核心功能。因此,控制块必须是独立于被管理对象而存在的实体,拥有自己独立的生命周期。
4.1.3 原因三:自定义删除器和分配器 (Custom Deleters and Allocators)
自定义删除器和分配器需要存储在某个地方,以便在适当的时候被调用。控制块是存储这些额外信息的理想场所。
- 自定义删除器:
std::shared_ptr允许你指定一个自定义函数或函数对象来销毁被管理对象,而不是简单地调用delete。- 这些删除器可能包含状态(例如,一个文件句柄关闭函数可能需要知道文件描述符)。
- 删除器的类型可能是异质的,甚至在编译时都无法确定具体类型。控制块必须能够以类型擦除的方式存储这些删除器。
- 自定义分配器:
- 你也可以为
std::shared_ptr提供一个自定义分配器,用于分配和释放控制块本身的内存。 - 虽然不如删除器常用,但在某些特殊场景(如内存池、NUMA 架构)下,自定义分配器非常有用。
- 你也可以为
如果控制块与被管理对象合并,那么如何存储这些额外且可能类型多变的删除器和分配器?这将变得非常复杂,甚至不可能实现,特别是在被管理对象类型已知但在编译时不知道删除器类型的情况下。独立分配的控制块提供了一个统一的、类型擦除的容器来存储这些元数据。
4.2 场景二:使用 std::make_shared
std::make_shared 是构造 std::shared_ptr 的推荐方式,它引入了一个重要的优化:单次分配。
#include <iostream>
#include <memory>
class AnotherObject {
public:
int value;
AnotherObject(int v) : value(v) {
std::cout << "AnotherObject " << value << " constructed." << std::endl;
}
~AnotherObject() {
std::cout << "AnotherObject " << value << " destructed." << std::endl;
}
};
void demo_make_shared() {
std::cout << "--- Demo: std::make_shared ---" << std::endl;
// 单次内存分配:为 AnotherObject 对象和控制块分配一块连续内存
std::shared_ptr<AnotherObject> s_ptr_make = std::make_shared<AnotherObject>(303);
std::cout << "s_ptr_make strong count: " << s_ptr_make.use_count() << std::endl;
std::weak_ptr<AnotherObject> w_ptr_make = s_ptr_make;
std::cout << "w_ptr_make weak count: " << w_ptr_make.use_count() << std::endl;
std::cout << "Exiting demo_make_shared scope." << std::endl;
} // s_ptr_make goes out of scope, AnotherObject 303 destructed, then control block destructed
/*
预期输出:
--- Demo: std::make_shared ---
AnotherObject 303 constructed.
s_ptr_make strong count: 1
w_ptr_make weak count: 1
Exiting demo_make_shared scope.
AnotherObject 303 destructed.
*/
std::make_shared 的工作原理是,它会一次性分配一块足够大的内存,这块内存能够同时容纳 T 类型的对象和 std::shared_ptr 的控制块。然后,它在这块内存中构造 T 对象,并将控制块放在紧邻 T 对象的位置(或者说,控制块内部预留了 T 对象的存储空间)。
4.2.1 std::make_shared 的优点:
- 性能提升:减少了内存分配的次数。从两次分配(对象一次,控制块一次)变为一次分配。这减少了系统调用的开销,并可能降低内存碎片。
- 更好的缓存局部性:对象和控制块位于同一块连续内存中,访问它们时有更好的缓存命中率。
- 异常安全:当使用
new T()和std::shared_ptr<T>(ptr)分两步构造时,如果new T()成功,但在std::shared_ptr构造之前发生异常(例如,在函数参数列表中),原始指针ptr可能会泄露。std::make_shared保证了原子性,要么全部成功,要么全部失败,从而避免了这种泄露。
4.2.2 std::make_shared 的潜在缺点(与控制块生命周期相关):
尽管 std::make_shared 带来了显著的优势,但它也引入了一个微妙的内存管理考虑:
- 内存驻留问题:由于对象
T和控制块位于同一块内存中,即使所有强引用 (shared_count) 都已消失,导致对象T被销毁,但如果仍然存在std::weak_ptr实例,控制块就不能被销毁。这意味着,包含已销毁对象T的那一大块内存将一直存在,直到最后一个std::weak_ptr也被销毁,weak_count降为 0。 - 资源浪费:如果被管理的对象
T占用大量内存,并且存在长期存活的std::weak_ptr实例,那么即使对象本身已经不再使用,其占用的内存也无法被回收。这可能导致不必要的内存驻留和浪费。
表格对比:new 与 std::make_shared 构造 std::shared_ptr
| 特性/行为 | std::shared_ptr<T>(new T(...)) |
std::make_shared<T>(...) |
|---|---|---|
| 内存分配次数 | 两次:对象一次,控制块一次 | 一次:对象和控制块在同一块内存中分配 |
| 缓存局部性 | 较差:对象和控制块可能在内存中相距较远 | 较好:对象和控制块连续存储 |
| 异常安全 | 较差:可能导致原始指针泄露(在参数列表求值顺序中) | 较好:原子操作,保证全成功或全失败 |
| 自定义删除器 | 支持:通过构造函数参数传入 | 不直接支持:std::allocate_shared 可提供自定义分配器,但自定义删除器需通过 std::shared_ptr<T>(ptr, deleter) 形式。C++20 后 std::shared_ptr<T>(ptr, deleter) 也有优化,但仍是两次分配。 |
| 自定义分配器 | 支持:通过构造函数参数传入 | 支持:通过 std::allocate_shared 传入自定义分配器 |
weak_ptr 影响 |
强引用计数归零时,对象内存被释放;弱引用计数归零时,控制块内存被释放。对象和控制块的内存生命周期相互独立。 | 强引用计数归零时,对象被析构;但若弱引用计数不为零,包含对象和控制块的整块内存将继续存在,直到弱引用计数也归零。 |
| 内存驻留 | 对象内存可早于控制块内存释放 | 对象内存释放后,其所占空间可能因控制块的存在而无法回收 |
总结来说,控制块被单独分配(或在 make_shared 优化下与对象一同分配但逻辑上独立)的原因,是为了实现 std::shared_ptr 的通用性、生命周期管理的灵活性以及对 std::weak_ptr 的支持。
5. 深入探讨:引用计数与内存管理细节
5.1 线程安全
在多线程环境中,多个 std::shared_ptr 实例可能在不同的线程中同时被创建、拷贝、赋值或销毁。这意味着对引用计数器(shared_count 和 weak_count)的增减操作必须是原子性的,以避免竞态条件和数据损坏。
std::shared_ptr 的标准实现确保了引用计数的增减操作是原子性的。这通常通过使用 std::atomic 类型或底层平台提供的原子指令(如 compare_and_swap、fetch_add 等)来实现。虽然原子操作会带来轻微的性能开销,但这是确保 std::shared_ptr 在并发环境下正确性的必要代价。
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <chrono>
struct ThreadSafeObject {
int value;
ThreadSafeObject(int v) : value(v) {
std::cout << "ThreadSafeObject " << value << " constructed by " << std::this_thread::get_id() << std::endl;
}
~ThreadSafeObject() {
std::cout << "ThreadSafeObject " << value << " destructed by " << std::this_thread::get_id() << std::endl;
}
};
void worker(std::shared_ptr<ThreadSafeObject> s_ptr) {
// 拷贝 shared_ptr,引用计数增加
std::shared_ptr<ThreadSafeObject> local_ptr = s_ptr;
std::cout << "Thread " << std::this_thread::get_id() << " - current use_count: " << local_ptr.use_count() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
// local_ptr 销毁,引用计数减少
}
void demo_thread_safety() {
std::cout << "--- Demo: Thread Safety of Reference Counts ---" << std::endl;
std::shared_ptr<ThreadSafeObject> global_ptr = std::make_shared<ThreadSafeObject>(404);
std::cout << "Initial global_ptr use_count: " << global_ptr.use_count() << std::endl;
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, global_ptr); // 传递 shared_ptr 副本,每次都会增加引用计数
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final global_ptr use_count: " << global_ptr.use_count() << std::endl;
std::cout << "Exiting demo_thread_safety scope." << std::endl;
} // global_ptr goes out of scope, ThreadSafeObject 404 destructed
/*
预期输出 (线程ID可能不同):
--- Demo: Thread Safety of Reference Counts ---
ThreadSafeObject 404 constructed by 1
Initial global_ptr use_count: 1
Thread 2 - current use_count: 2
Thread 3 - current use_count: 3
Thread 4 - current use_count: 4
Thread 5 - current use_count: 5
Thread 6 - current use_count: 6
Final global_ptr use_count: 1
Exiting demo_thread_safety scope.
ThreadSafeObject 404 destructed by 1
*/
从输出可以看出,use_count() 在不同线程中被正确地更新,保证了引用计数的同步。
5.2 删除器 (Deleters)
删除器是 std::shared_ptr 灵活性的核心。它允许 std::shared_ptr 管理任何需要特定清理操作的资源。
- 默认删除器:对于通过
new分配的内存,默认删除器是delete操作。 - 自定义删除器:通过在构造
std::shared_ptr时传入一个可调用对象(函数指针、lambda 表达式、函数对象),可以自定义删除行为。这个可调用对象必须接受一个指向被管理对象的原始指针作为参数。
#include <iostream>
#include <memory>
#include <fstream> // For FILE* example
// 1. 自定义删除器管理 C 风格数组
void custom_array_deleter(int* arr) {
std::cout << "Custom array deleter: Deleting C-style array." << std::endl;
delete[] arr;
}
// 2. 自定义删除器管理文件句柄
struct FileCloser {
void operator()(FILE* fp) const {
if (fp) {
std::cout << "FileCloser: Closing file." << std::endl;
fclose(fp);
}
}
};
void demo_custom_deleters() {
std::cout << "--- Demo: Custom Deleters ---" << std::endl;
// 示例 1: 管理 C 风格数组
std::cout << "Managing C-style array..." << std::endl;
std::shared_ptr<int> array_ptr(new int[5], custom_array_deleter);
// 控制块会存储 custom_array_deleter 函数指针
for (int i = 0; i < 5; ++i) {
array_ptr.get()[i] = i * 10;
}
std::cout << "Array element at index 2: " << array_ptr.get()[2] << std::endl;
// array_ptr 离开作用域时,custom_array_deleter 被调用
std::cout << "nManaging FILE* handle..." << std::endl;
// 示例 2: 管理文件句柄
FILE* file = fopen("example.txt", "w");
if (file) {
// 使用 lambda 表达式作为删除器
std::shared_ptr<FILE> file_ptr(file, [](FILE* fp){
if (fp) {
std::cout << "Lambda deleter: Closing file." << std::endl;
fclose(fp);
}
});
fprintf(file_ptr.get(), "Hello from shared_ptr!n");
// file_ptr 离开作用域时,lambda 删除器被调用
} else {
std::cerr << "Failed to open example.txt" << std::endl;
}
std::cout << "nManaging another FILE* handle with a function object..." << std::endl;
FILE* file2 = fopen("example2.txt", "w");
if (file2) {
// 使用函数对象作为删除器
std::shared_ptr<FILE> file_ptr2(file2, FileCloser());
fprintf(file_ptr2.get(), "Hello from shared_ptr with FileCloser!n");
} else {
std::cerr << "Failed to open example2.txt" << std::endl;
}
std::cout << "Exiting demo_custom_deleters scope." << std::endl;
}
/*
预期输出:
--- Demo: Custom Deleters ---
Managing C-style array...
Array element at index 2: 20
Custom array deleter: Deleting C-style array.
Managing FILE* handle...
Lambda deleter: Closing file.
Managing another FILE* handle with a function object...
FileCloser: Closing file.
Exiting demo_custom_deleters scope.
*/
删除器被存储在控制块中,这进一步证明了控制块独立于被管理对象的必要性。删除器可以有状态(例如 FileCloser 对象),这些状态也必须随控制块一起管理。
5.3 分配器 (Allocators)
std::shared_ptr 也支持自定义分配器,主要用于控制块本身的内存分配。这在需要精细控制内存布局或优化内存池使用的场景中非常有用。std::allocate_shared 函数允许你指定一个分配器来分配对象和控制块的内存。
#include <iostream>
#include <memory>
#include <vector>
// 简单的自定义分配器,用于演示
template <typename T>
struct MyAllocator {
using value_type = T;
MyAllocator() = default;
template <typename U>
MyAllocator(const MyAllocator<U>&) {}
T* allocate(std::size_t n) {
std::cout << "MyAllocator: Allocating " << n * sizeof(T) << " bytes." << std::endl;
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) {
std::cout << "MyAllocator: Deallocating " << n * sizeof(T) << " bytes." << std::endl;
::operator delete(p);
}
};
template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) { return true; }
template <typename T, typename U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) { return false; }
struct AllocObject {
int data;
AllocObject(int d = 0) : data(d) {
std::cout << "AllocObject " << data << " constructed." << std::endl;
}
~AllocObject() {
std::cout << "AllocObject " << data << " destructed." << std::endl;
}
};
void demo_custom_allocator() {
std::cout << "--- Demo: Custom Allocator ---" << std::endl;
// 使用自定义分配器为 AllocObject 和控制块分配内存
std::shared_ptr<AllocObject> s_ptr = std::allocate_shared<AllocObject>(MyAllocator<AllocObject>(), 505);
std::cout << "s_ptr value: " << s_ptr->data << std::endl;
std::cout << "Exiting demo_custom_allocator scope." << std::endl;
} // s_ptr goes out of scope, custom allocator deallocates memory
/*
预期输出:
--- Demo: Custom Allocator ---
MyAllocator: Allocating ... bytes. (实际大小取决于 AllocObject 和控制块的大小)
AllocObject 505 constructed.
s_ptr value: 505
Exiting demo_custom_allocator scope.
AllocObject 505 destructed.
MyAllocator: Deallocating ... bytes.
*/
自定义分配器同样需要存储在控制块中,以确保在对象和控制块销毁时使用正确的机制释放内存。
5.4 std::weak_ptr 的作用
std::weak_ptr 的存在是控制块独立生命周期最强有力的证明。它通过观察控制块的 shared_count 来判断其所观察的对象是否仍然存活,而不会影响对象的生命周期。
- 解决循环引用:这是
std::weak_ptr最重要的应用场景。在相互引用的对象中,如果都使用std::shared_ptr,将导致shared_count永远不会降为 0,从而造成内存泄露。通过将其中一个引用改为std::weak_ptr,可以打破这种循环。
#include <iostream>
#include <memory>
class Son; // 前向声明
class Father {
public:
std::shared_ptr<Son> son;
int id;
Father(int i) : id(i) { std::cout << "Father " << id << " constructed." << std::endl; }
~Father() { std::cout << "Father " << id << " destructed." << std::endl; }
};
class Son {
public:
std::weak_ptr<Father> father; // 使用 weak_ptr 避免循环引用
int id;
Son(int i) : id(i) { std::cout << "Son " << id << " constructed." << std::endl; }
~Son() { std::cout << "Son " << id << " destructed." << std::endl; }
};
void demo_circular_reference() {
std::cout << "--- Demo: Circular Reference with weak_ptr ---" << std::endl;
std::shared_ptr<Father> father_ptr;
std::shared_ptr<Son> son_ptr;
{
father_ptr = std::make_shared<Father>(606);
son_ptr = std::make_shared<Son>(707);
father_ptr->son = son_ptr;
son_ptr->father = father_ptr; // weak_ptr 不增加 strong count
std::cout << "Father strong count: " << father_ptr.use_count() << std::endl; // 1 (只有 father_ptr 拥有)
std::cout << "Son strong count: " << son_ptr.use_count() << std::endl; // 2 (father_ptr->son 和 son_ptr 拥有)
} // father_ptr 和 son_ptr 离开作用域,shared_count 减少
// 在这里,father_ptr 和 son_ptr 已经销毁,它们的 strong count 都降为 0
// Father 606 和 Son 707 都会被正确析构
std::cout << "After scope: father_ptr and son_ptr are gone." << std::endl;
// 如果 Son 的 father 成员是 shared_ptr,这里不会有析构输出,导致内存泄露
// 因为 father_ptr 和 son_ptr 的 use_count 永远不会降到 0
// father_ptr 拥有 son_ptr,son_ptr 拥有 father_ptr,形成循环
}
/*
预期输出:
--- Demo: Circular Reference with weak_ptr ---
Father 606 constructed.
Son 707 constructed.
Father strong count: 1
Son strong count: 2
Son 707 destructed.
Father 606 destructed.
After scope: father_ptr and son_ptr are gone.
*/
在上述例子中,Son 对象中的 father 成员是 std::weak_ptr<Father>。当 father_ptr 和 son_ptr 离开作用域时,它们的强引用计数分别降为 0,对象被正确销毁。如果 Son::father 也是 std::shared_ptr,那么 father_ptr 的 shared_count 会因为 son_ptr->father 而保持为 1,son_ptr 的 shared_count 会因为 father_ptr->son 而保持为 1,导致循环引用和内存泄露。std::weak_ptr 能够观察对象但又不拥有对象,正是通过控制块实现的。它不增加 shared_count,但会增加 weak_count,确保控制块在对象销毁后仍能存活,以供 weak_ptr 查询其状态。
6. 性能考量与最佳实践
6.1 std::make_shared vs. new
- 优先使用
std::make_shared:在大多数情况下,std::make_shared是创建std::shared_ptr的首选方式,因为它提供了性能、缓存局部性和异常安全方面的优势。 - 何时使用
new:- 当你需要自定义删除器时:
std::make_shared不直接支持自定义删除器(尽管 C++20 引入了std::allocate_shared的重载支持自定义删除器,但这仍然是两次分配)。 - 当你从一个已存在的原始指针构造
std::shared_ptr时。 - 当你担心
std::weak_ptr可能导致的内存驻留问题,并且被管理对象T很大时。在这种情况下,尽管牺牲了一点性能,但std::shared_ptr<T>(new T(...))能够确保对象内存与控制块内存的独立释放,从而避免大对象内存被长期锁定。
- 当你需要自定义删除器时:
6.2 内存开销
std::shared_ptr 的内存开销包括:
std::shared_ptr对象本身:通常是两个指针的大小(8 或 16 字节,取决于架构),一个指向被管理对象,一个指向控制块。- 控制块:至少包含两个原子整数(强引用计数和弱引用计数),可能还包含删除器和分配器的存储空间。这些额外的数据会增加控制块的大小。自定义删除器或分配器如果包含大量状态,会进一步增加控制块的开销。
- 被管理对象:实际用户数据的大小。
6.3 原子操作开销
引用计数器的原子操作比非原子操作略慢。在高并发、大量 std::shared_ptr 拷贝/销毁的场景中,这可能会成为性能瓶颈。对于单线程或不需要共享所有权的场景,std::unique_ptr 通常是更高效的选择。
6.4 循环引用
始终警惕循环引用问题。如果两个对象通过 std::shared_ptr 相互持有对方,它们将永远不会被销毁。std::weak_ptr 是解决此问题的标准方案。
7. 代码示例与深入分析
我们已经通过多个小段代码示例展示了 std::shared_ptr 的各种特性。这里我们将汇总并进一步分析。
#include <iostream>
#include <memory>
#include <vector>
#include <thread>
#include <chrono>
// ===================== 辅助类用于观察生命周期 =====================
class LifetimeTracker {
public:
int id;
LifetimeTracker(int i) : id(i) {
std::cout << "[Object " << id << "] Constructed." << std::endl;
}
~LifetimeTracker() {
std::cout << "[Object " << id << "] Destructed." << std::endl;
}
};
// ===================== 1. 基本使用和强弱引用计数 =====================
void demonstrate_basic_usage() {
std::cout << "n--- Demonstrate: Basic Usage and Counts ---" << std::endl;
std::shared_ptr<LifetimeTracker> sp1 = std::make_shared<LifetimeTracker>(1); // 对象1和控制块一次分配
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // 1
std::weak_ptr<LifetimeTracker> wp1 = sp1; // 弱引用,增加弱引用计数
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // 1
std::cout << "wp1 expired: " << wp1.expired() << std::endl; // false
{
std::shared_ptr<LifetimeTracker> sp2 = sp1; // 拷贝,强引用计数增加
std::cout << "sp1 use_count (inside scope): " << sp1.use_count() << std::endl; // 2
// sp2 离开作用域时,强引用计数减少
}
std::cout << "sp1 use_count (outside scope): " << sp1.use_count() << std::endl; // 1
std::cout << "Reset sp1, object should destruct now." << std::endl;
sp1.reset(); // sp1 放弃所有权,强引用计数为0,对象1被销毁
std::cout << "sp1 is null? " << (sp1 == nullptr) << std::endl; // true
std::cout << "wp1 expired: " << wp1.expired() << std::endl; // true (对象已销毁)
// 此时,控制块仍然存在,因为 wp1 还在观察它。
// wp1 离开 demonstrate_basic_usage 作用域时,控制块才会被销毁。
} // wp1 离开作用域,控制块销毁
// ===================== 2. `new` 构造与自定义删除器 =====================
void custom_deleter_func(LifetimeTracker* p) {
std::cout << "[Deleter] Custom deleter called for Object " << p->id << std::endl;
delete p; // 必须手动调用 delete
}
void demonstrate_custom_deleter() {
std::cout << "n--- Demonstrate: Custom Deleter with raw new ---" << std::endl;
// 两次分配:new LifetimeTracker(2) 一次,控制块一次
std::shared_ptr<LifetimeTracker> sp_custom(new LifetimeTracker(2), custom_deleter_func);
std::cout << "sp_custom use_count: " << sp_custom.use_count() << std::endl;
// sp_custom 离开作用域时,custom_deleter_func 被调用
} // custom_deleter_func 被调用,对象2销毁
// ===================== 3. 循环引用与 `weak_ptr` 解决 =====================
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用 weak_ptr 解决循环引用
int id;
Node(int i) : id(i) { std::cout << "[Node " << id << "] Constructed." << std::endl; }
~Node() { std::cout << "[Node " << id << "] Destructed." << std::endl; }
};
void demonstrate_circular_reference() {
std::cout << "n--- Demonstrate: Circular Reference Solved with weak_ptr ---" << std::endl;
std::shared_ptr<Node> head = std::make_shared<Node>(3);
std::shared_ptr<Node> tail = std::make_shared<Node>(4);
head->next = tail;
tail->prev = head; // weak_ptr 不增加 head 的强引用计数
std::cout << "head use_count: " << head.use_count() << std::endl; // 1
std::cout << "tail use_count: " << tail.use_count() << std::endl; // 2 (被 head->next 和 tail 拥有)
// head 和 tail 离开作用域时,强引用计数减少到 0,对象正确销毁
} // Node 4 -> 3 顺序析构
// ===================== 4. 线程安全 (简单示例) =====================
void thread_func(std::shared_ptr<LifetimeTracker> ptr) {
std::cout << "Thread " << std::this_thread::get_id() << " entered. use_count: " << ptr.use_count() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// ptr 离开作用域,强引用计数原子性减少
std::cout << "Thread " << std::this_thread::get_id() << " exiting." << std::endl;
}
void demonstrate_thread_safety() {
std::cout << "n--- Demonstrate: Thread Safety ---" << std::endl;
std::shared_ptr<LifetimeTracker> shared_data = std::make_shared<LifetimeTracker>(5);
std::cout << "Main thread initial use_count: " << shared_data.use_count() << std::endl;
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(thread_func, shared_data); // 拷贝 shared_ptr,原子增加计数
}
for (auto& t : threads) {
t.join();
}
std::cout << "Main thread final use_count: " << shared_data.use_count() << std::endl;
// shared_data 离开作用域,对象5销毁
} // shared_data 离开作用域,对象5销毁
int main() {
demonstrate_basic_usage();
demonstrate_custom_deleter();
demonstrate_circular_reference();
demonstrate_thread_safety();
std::cout << "n--- All demonstrations finished ---" << std::endl;
return 0;
}
分析要点:
demonstrate_basic_usage: 清楚地展示了make_shared的构造、weak_ptr的不拥有特性、以及reset()导致的强引用计数归零和对象销毁。控制块的延迟销毁在这里体现。demonstrate_custom_deleter: 明确了new构造和自定义删除器的情况。输出中的[Object 2] Constructed.和[Deleter] Custom deleter called for Object 2证实了对象的构造和自定义删除器的执行。这里是两次分配:对象一次,控制块(包含删除器信息)一次。demonstrate_circular_reference: 经典循环引用问题及其weak_ptr解决方案。head use_count: 1和tail use_count: 2明确显示了weak_ptr不增加强引用计数,从而允许对象在适当时候被销毁。如果prev是shared_ptr,这两个对象的析构函数都不会被调用。demonstrate_thread_safety: 模拟多线程环境,展示了shared_ptr拷贝时引用计数的原子性增加。use_count能够正确地反映当前所有者的数量。
这些示例共同描绘了 std::shared_ptr 的强大功能和其底层设计原理,特别是控制块独立分配所带来的灵活性和复杂性。
8. 展望未来:C++20 的改进
C++20 及其后续版本持续对 std::shared_ptr 及其相关组件进行改进。
std::shared_ptr<T>(ptr, deleter)的优化:虽然这种形式仍然涉及两次分配(对象一次,控制块一次),但 C++20 及其之后的标准允许编译器在某些情况下优化控制块的分配,使其更高效。std::allocate_shared的增强:std::allocate_shared允许你提供自定义分配器来分配对象和控制块的内存。C++20 还增加了std::allocate_shared的重载,允许同时指定自定义分配器和自定义删除器,但需要注意的是,即使使用了std::allocate_shared并指定了删除器,如果删除器不是空操作,通常也无法实现单次分配,因为删除器本身可能需要存储状态,并且其类型可能与被管理对象的类型无关。
这些发展旨在进一步提升 std::shared_ptr 的性能和灵活性,同时保持其核心的共享所有权语义。