各位同学,大家好。今天我们将深入探讨C++中一个至关重要的概念——智能指针,尤其是shared_ptr。在现代C++编程中,手动管理内存是一项艰巨且容易出错的任务,它常常导致内存泄漏、悬空指针、二次释放等经典问题。为了解决这些问题,C++标准库引入了智能指针,它们以RAII(Resource Acquisition Is Initialization)原则为基础,将资源的生命周期管理自动化。
shared_ptr是智能指针家族中的一员,它实现了共享所有权语义。这意味着多个shared_ptr可以共同管理同一个对象,当最后一个shared_ptr离开作用域或被重置时,它所管理的对象才会被自动销毁。这种机制极大地简化了复杂数据结构和并发编程中的内存管理。
今天的实战任务是手写一个简化版的shared_ptr。这不仅仅是为了满足好奇心,更是为了深刻理解shared_ptr背后的设计思想、实现细节以及它如何解决C++中长期存在的内存管理难题。我们将着重关注两个核心特性:线程安全的引用计数和自定义删除器。通过这次实践,你将掌握智能指针的精髓,并能够更好地运用它们,甚至在需要时设计自己的智能指针。
第一讲:理解 shared_ptr 的核心机制
在着手编写代码之前,我们必须对shared_ptr的工作原理有一个清晰的认识。shared_ptr之所以能够实现共享所有权和自动内存管理,其核心在于一个被称为“控制块”(Control Block)的独立结构。
1.1 为什么需要控制块?
想象一下,如果你只有原始指针T*和一个简单的计数器,那么当一个shared_ptr被复制时,你需要递增计数器;当它被销毁时,你需要递减计数器。但是,这个计数器应该存放在哪里呢?
- 如果放在被管理对象内部? 这要求被管理对象必须知道
shared_ptr的存在,并修改其内部结构,这破坏了封装性,也限制了shared_ptr管理任意类型对象的能力(例如,管理一个int)。 - 如果放在
shared_ptr对象内部? 那么每个shared_ptr实例都会有自己的计数器,它们之间无法共享同一个计数状态。
这两种方案都不可行。因此,一个独立的“控制块”应运而生。控制块是一个动态分配的辅助对象,它独立于被管理的对象,但又与被管理对象紧密关联。所有指向同一个被管理对象的shared_ptr实例,都会共享同一个控制块。
控制块通常包含以下信息:
- 共享引用计数(Shared Reference Count):记录有多少个
shared_ptr实例正在管理这个对象。 - 弱引用计数(Weak Reference Count):记录有多少个
weak_ptr实例正在观察这个对象。weak_ptr不拥有对象,因此不影响对象的生命周期,但它需要知道控制块何时被销毁。 - 原始指针(Raw Pointer):指向被管理对象的原始指针。
- 删除器(Deleter):一个可调用对象,用于在引用计数归零时正确地销毁被管理对象。
- 分配器(Allocator):一个可选的分配器,用于在需要时自定义被管理对象和控制块的内存分配。
1.2 shared_ptr 的生命周期管理
shared_ptr的生命周期管理是一个两阶段的过程:
- 对象销毁阶段:当共享引用计数降为零时,意味着没有
shared_ptr再拥有该对象。此时,shared_ptr会调用其内部存储的删除器来销毁被管理对象。 - 控制块销毁阶段:当共享引用计数和弱引用计数都降为零时,意味着既没有
shared_ptr也没有weak_ptr再关心这个对象或其控制块。此时,控制块本身才会被销毁。
这种两阶段销毁机制非常重要,它允许weak_ptr在对象被销毁后仍然能够安全地判断其是否有效(通过检查共享引用计数),直到最后一个weak_ptr也消失后,控制块才被回收。
1.3 线程安全的需求
在多线程环境中,多个线程可能同时对同一个shared_ptr进行复制、赋值或销毁操作。这意味着对共享引用计数和弱引用计数的增减操作必须是原子性的,否则将导致竞态条件,造成计数错误,进而引发内存泄漏或过早释放等严重问题。C++标准库中的shared_ptr正是通过std::atomic或其他底层原子操作来保证引用计数的线程安全。
1.4 自定义删除器
shared_ptr的强大之处还在于它允许用户提供自定义的删除器。默认情况下,shared_ptr使用delete操作符来释放通过new分配的对象。但很多时候,我们管理的资源并非通过new分配,或者需要特殊的清理逻辑:
- 使用
malloc分配的内存需要free来释放。 - 文件句柄需要
fclose来关闭。 - 互斥量需要
pthread_mutex_destroy来销毁。 - 数据库连接需要特定的API来关闭。
自定义删除器使得shared_ptr能够管理任何需要明确释放的资源,而不仅仅是堆上的C++对象。
第二讲:设计我们的 MySharedPtr 及其控制块
现在,我们开始设计自己的MySharedPtr。为了清晰地分离关注点,我们将首先定义控制块的结构,然后是MySharedPtr类本身。
2.1 抽象基类 ControlBlockBase
为了支持自定义删除器和类型擦除(Type Erasure),我们的控制块需要一个抽象基类。这样,无论具体的被管理对象类型T和删除器类型Deleter是什么,MySharedPtr都可以通过一个统一的ControlBlockBase*指针来操作。
#include <atomic> // For std::atomic for thread-safe reference counting
#include <utility> // For std::forward, std::move
#include <functional> // For std::function (optional, but good for deleters)
#include <iostream> // For demonstration output
// -----------------------------------------------------------------------------
// ControlBlockBase: 抽象基类,用于类型擦除,管理引用计数和虚拟析构/删除
// -----------------------------------------------------------------------------
class ControlBlockBase {
public:
// 共享引用计数:有多少个 MySharedPtr 实例拥有对象
std::atomic<long> shared_count_;
// 弱引用计数:有多少个 MyWeakPtr 实例观察对象 (即使我们不实现 MyWeakPtr,也应预留)
std::atomic<long> weak_count_;
public:
// 构造函数:初始化引用计数
ControlBlockBase() : shared_count_(1), weak_count_(0) {}
// 虚析构函数:确保派生类能正确析构
virtual ~ControlBlockBase() = default;
// 纯虚函数:销毁被管理对象。具体如何销毁由派生类实现(调用自定义删除器或 delete)
virtual void destroy() = 0;
// 纯虚函数:销毁控制块自身。具体如何销毁由派生类实现(delete this)
virtual void delete_this() = 0;
};
设计要点分析:
std::atomic<long>: 这是实现线程安全的关键。std::atomic保证了对shared_count_和weak_count_的增减操作是原子性的,从而避免了竞态条件。shared_count_(1): 当一个MySharedPtr被创建并关联到一个新的对象时,它立即成为该对象的第一个所有者,因此共享引用计数从1开始。weak_count_(0): 初始时没有weak_ptr观察该对象。- 虚析构函数
virtual ~ControlBlockBase() = default;: 这是多态基类的标准做法,确保通过基类指针删除派生类对象时能调用正确的析构函数。 virtual void destroy() = 0;: 这个纯虚函数负责调用实际的删除器来销毁被管理对象。它抽象了delete操作符或自定义删除器的具体调用方式。virtual void delete_this() = 0;: 这个纯虚函数负责销毁控制块自身。同样,它抽象了delete this的具体行为,允许未来的扩展(例如,使用自定义分配器来释放控制块)。
2.2 模板派生类 ControlBlock
现在我们来创建ControlBlockBase的派生类。这个派生类将是模板化的,因为它需要知道被管理对象的类型T和删除器类型Deleter。
// -----------------------------------------------------------------------------
// ControlBlock: 派生类,存储原始指针和自定义删除器
// -----------------------------------------------------------------------------
template <typename T, typename Deleter>
class ControlBlock : public ControlBlockBase {
private:
T* ptr_; // 指向被管理对象的原始指针
Deleter deleter_; // 存储自定义删除器实例
public:
// 构造函数:初始化原始指针和删除器
// 注意:Deleter 可以是函数指针、lambda 或 functor
ControlBlock(T* p, Deleter d) : ptr_(p), deleter_(std::move(d)) {}
// 析构函数:无特殊操作,因为 destroy() 会处理对象销毁
~ControlBlock() override {
// 通常这里不需要做任何事情,因为 destroy() 已经在 shared_count_ 归零时被调用了
// 并且 delete_this() 会负责释放 ControlBlock 本身
}
// 实现 destroy():调用存储的删除器来销毁对象
void destroy() override {
if (ptr_ != nullptr) {
deleter_(ptr_); // 调用自定义删除器
ptr_ = nullptr; // 防止二次删除
}
}
// 实现 delete_this():销毁当前控制块实例
void delete_this() override {
delete this; // 释放控制块自身的内存
}
};
设计要点分析:
- *`T ptr_
:** 存储了实际被MySharedPtr`管理的对象指针。 Deleter deleter_: 存储了删除器对象。Deleter可以是任何可调用对象,包括函数指针、lambda表达式或实现了operator()的仿函数(functor)。std::move(d)确保删除器被高效地转移进来。destroy() override: 这个方法现在有了具体的实现。它检查ptr_是否为nullptr(尽管在正常流程下,只有有效的指针才会被管理),然后调用deleter_(ptr_)来销毁对象。将ptr_设置为nullptr是一个良好的实践,以防止在某些极端情况下(例如,删除器抛出异常后再次尝试删除)发生二次删除。delete_this() override: 这个方法简单地调用delete this来释放当前ControlBlock实例所占用的内存。
2.3 MySharedPtr 类的结构
有了控制块,我们现在可以定义MySharedPtr类了。它将包含一个指向被管理对象的原始指针和一个指向其控制块的指针。
// -----------------------------------------------------------------------------
// MySharedPtr: 我们的自定义共享指针类
// -----------------------------------------------------------------------------
template <typename T>
class MySharedPtr {
private:
T* ptr_; // 指向被管理对象的原始指针
ControlBlockBase* control_block_; // 指向控制块的基类指针
// 辅助函数:释放当前 MySharedPtr 拥有的资源
void release() {
if (control_block_ != nullptr) {
// 递减共享引用计数
if (control_block_->shared_count_.fetch_sub(1) == 1) {
// 如果共享引用计数变为0,说明当前是最后一个 MySharedPtr
// 此时销毁被管理对象
control_block_->destroy();
// 检查弱引用计数,如果弱引用计数也为0,则销毁控制块
if (control_block_->weak_count_.load() == 0) {
control_block_->delete_this();
control_block_ = nullptr; // 清空指针,防止悬空
}
}
// 如果 weak_count_ 仍有值,则 control_block_ 暂时不为 nullptr
// 因为 weak_ptr 可能还在观察它
// 只有当 shared_count_ 和 weak_count_ 都为0时,control_block_ 才会被 delete_this() 释放
// 但在这里,我们只负责处理 shared_count_ 的情况。
// 当最后一个 weak_ptr 析构时,它会检查 shared_count_ 是否为0,然后决定是否销毁 control_block_
// 在 MySharedPtr 的析构中,我们只负责在 shared_count_ 归零时销毁对象
// 并在 shared_count_ 和 weak_count_ 都归零时销毁控制块。
}
ptr_ = nullptr; // 清空原始指针
}
// 辅助函数:交换内容,用于 copy-and-swap 惯用法
void swap(MySharedPtr& other) noexcept {
std::swap(ptr_, other.ptr_);
std::swap(control_block_, other.control_block_);
}
public:
// 默认构造函数
MySharedPtr() : ptr_(nullptr), control_block_(nullptr) {}
// 构造函数:从原始指针创建 MySharedPtr (使用默认 delete 作为删除器)
explicit MySharedPtr(T* p) : ptr_(p), control_block_(nullptr) {
if (p != nullptr) {
// 使用 ControlBlock<T, std::default_delete<T>>
control_block_ = new ControlBlock<T, std::default_delete<T>>(p, std::default_delete<T>());
}
}
// 构造函数:从原始指针和自定义删除器创建 MySharedPtr
template <typename Deleter>
MySharedPtr(T* p, Deleter d) : ptr_(p), control_block_(nullptr) {
if (p != nullptr) {
// 使用用户提供的 Deleter
control_block_ = new ControlBlock<T, Deleter>(p, std::move(d));
}
}
// 拷贝构造函数
MySharedPtr(const MySharedPtr& other) noexcept : ptr_(other.ptr_), control_block_(other.control_block_) {
if (control_block_ != nullptr) {
control_block_->shared_count_.fetch_add(1); // 递增共享引用计数
}
}
// 移动构造函数
MySharedPtr(MySharedPtr&& other) noexcept : ptr_(other.ptr_), control_block_(other.control_block_) {
other.ptr_ = nullptr;
other.control_block_ = nullptr;
}
// 析构函数
~MySharedPtr() {
release(); // 释放资源
}
// 拷贝赋值运算符 (copy-and-swap 惯用法)
MySharedPtr& operator=(const MySharedPtr& other) noexcept {
MySharedPtr temp(other); // 利用拷贝构造函数创建临时对象
swap(temp); // 交换内容
return *this;
}
// 移动赋值运算符
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) { // 防止自我移动赋值
release(); // 释放当前资源
ptr_ = other.ptr_;
control_block_ = other.control_block_;
other.ptr_ = nullptr;
other.control_block_ = nullptr;
}
return *this;
}
// 重置 MySharedPtr,释放当前资源,并可选地管理新对象
void reset() {
release();
// ptr_ 和 control_block_ 在 release() 中已经被置为 nullptr
}
template <typename Deleter>
void reset(T* p, Deleter d) {
release(); // 释放当前资源
if (p != nullptr) {
ptr_ = p;
control_block_ = new ControlBlock<T, Deleter>(p, std::move(d));
}
}
void reset(T* p) {
release(); // 释放当前资源
if (p != nullptr) {
ptr_ = p;
control_block_ = new ControlBlock<T, std::default_delete<T>>(p, std::default_delete<T>());
}
}
// 获取原始指针
T* get() const noexcept {
return ptr_;
}
// 解引用操作符
T& operator*() const noexcept {
return *ptr_;
}
// 成员访问操作符
T* operator->() const noexcept {
return ptr_;
}
// 获取引用计数
long use_count() const noexcept {
return (control_block_ != nullptr) ? control_block_->shared_count_.load() : 0;
}
// 检查是否拥有对象
explicit operator bool() const noexcept {
return ptr_ != nullptr;
}
};
设计要点分析:
- 成员变量
T* ptr_和ControlBlockBase* control_block_: 这是MySharedPtr的核心,分别指向被管理对象和控制块。 release()辅助函数: 这是一个关键的私有方法,封装了MySharedPtr释放其所拥有资源的所有逻辑。它负责递减共享引用计数,并在计数归零时执行两阶段销毁(先销毁对象,再销毁控制块)。control_block_->shared_count_.fetch_sub(1):原子地递减共享引用计数,并返回递减前的值。如果递减前的值是1,说明当前是最后一个shared_ptr。control_block_->destroy():调用控制块的destroy方法,这将触发自定义删除器来销毁被管理对象。control_block_->weak_count_.load() == 0:在销毁被管理对象后,还需要检查弱引用计数。如果弱引用计数也为0,说明没有weak_ptr在观察了,此时可以安全地销毁控制块本身。
swap()辅助函数: 用于实现高效的拷贝赋值运算符(Copy-and-Swap idiom),这是一种实现强异常安全性的标准模式。- 构造函数:
- 默认构造函数: 创建一个空的
MySharedPtr。 - *从原始指针构造 (`explicit MySharedPtr(T p)
)**: 接收一个原始指针,并使用std::default_delete作为默认删除器。explicit`关键字防止隐式转换。 - *从原始指针和自定义删除器构造 (`template MySharedPtr(T p, Deleter d)
)**: 接收一个原始指针和任何可调用对象作为删除器。注意,ControlBlock模板实例化时会使用传入的Deleter`类型。
- 默认构造函数: 创建一个空的
- 拷贝构造函数 (
MySharedPtr(const MySharedPtr& other)): 复制ptr_和control_block_,并原子地递增control_block_的shared_count_。 - 移动构造函数 (
MySharedPtr(MySharedPtr&& other)): 从另一个MySharedPtr窃取资源,并将源MySharedPtr置为空状态,这是一个非常高效的操作。 - 析构函数 (
~MySharedPtr()): 调用release()来确保在MySharedPtr对象销毁时,其管理的资源被正确释放。 - 拷贝赋值运算符 (
operator=(const MySharedPtr& other)): 采用Copy-and-Swap惯用法,先通过拷贝构造函数创建一个临时对象,然后与自身交换内容,确保异常安全。 - 移动赋值运算符 (
operator=(MySharedPtr&& other)): 释放当前资源,然后从other窃取资源。需要检查this != &other以防止自我移动赋值。 reset()方法: 提供了重置MySharedPtr的功能,可以释放当前资源,并可选地管理新的对象。- *访问器 (
get(), `operator(),operator->()`)**: 提供对底层原始指针和被管理对象的访问。 use_count()方法: 返回当前的共享引用计数。operator bool(): 允许MySharedPtr在布尔上下文中被评估,如果它管理着一个非空对象,则为true。
第三讲:线程安全与引用计数
我们已经将std::atomic<long>用于shared_count_和weak_count_。现在,我们来更详细地讨论其重要性。
3.1 std::atomic 的作用
在多线程环境中,如果多个线程同时对一个非原子类型的变量进行读写操作,就会产生数据竞争。例如,假设shared_count_是一个普通的long变量:
- 线程A 读取
shared_count_(值为 N) - 线程B 读取
shared_count_(值为 N) - 线程A 递增 N,并将 N+1 写回
shared_count_ - 线程B 递增 N,并将 N+1 写回
shared_count_
最终,shared_count_的值只增加了1,而不是预期的2。这会导致引用计数不准确,从而引发内存泄漏(如果计数过高)或过早释放(如果计数过低)。
std::atomic类型提供了原子操作,这意味着这些操作是不可中断的,要么完全执行,要么不执行。对于我们的shared_ptr:
control_block_->shared_count_.fetch_add(1): 原子地执行“读取-修改-写入”序列,保证每次递增都正确无误。control_block_->shared_count_.fetch_sub(1): 同样,原子地递减,并返回递减前的值。这个返回值对于判断是否是最后一个所有者至关重要。control_block_->weak_count_.load(): 原子地读取弱引用计数。
3.2 引用计数的增减时机总结
为了更好地理解引用计数何时增减,我们通过一个表格来概括:
| 操作 | 共享引用计数 (shared_count_) |
弱引用计数 (weak_count_) |
备注 |
|---|---|---|---|
MySharedPtr(T* p) |
++ (从1开始) |
= (0) |
创建新控制块,成为第一个所有者 |
MySharedPtr(const MySharedPtr&) |
++ |
复制MySharedPtr,共享所有权 |
|
MySharedPtr(MySharedPtr&&) |
= (不变) |
= (不变) |
移动MySharedPtr,所有权转移,计数不变 |
MySharedPtr::operator=(const MySharedPtr&) |
++ (新),-- (旧) |
拷贝赋值,先递增新对象的计数,再递减旧对象的计数 | |
MySharedPtr::operator=(MySharedPtr&&) |
= (不变) |
= (不变) |
移动赋值,所有权转移,计数不变 |
~MySharedPtr() |
-- |
MySharedPtr销毁时递减计数 |
|
MySharedPtr::reset() |
-- |
显式释放所有权时递减计数 | |
| 对象销毁 | (== 0 时触发) |
shared_count_降为0时调用删除器,销毁被管理对象 |
|
| 控制块销毁 | (== 0) |
(== 0 时触发) |
shared_count_和weak_count_都降为0时,控制块被销毁(delete_this()) |
MyWeakPtr(const MySharedPtr&) |
++ |
MyWeakPtr诞生时递增弱引用计数 |
|
~MyWeakPtr() |
-- |
MyWeakPtr销毁时递减弱引用计数 |
重要提醒:
shared_count_的增减操作始终是原子性的。shared_count_降为1时,执行fetch_sub(1)返回1,表示它将变为0,此时触发对象销毁。- 控制块本身的销毁发生在
shared_count_和weak_count_都为0时。在MySharedPtr的release()中,我们检查了weak_count_。如果它也为0,就直接销毁控制块。如果weak_count_不为0,那么控制块将等待最后一个MyWeakPtr析构时再被销毁。
第四讲:自定义删除器的实践
自定义删除器是shared_ptr管理非new分配资源的关键。它允许我们传入任何可调用对象来执行清理工作。
4.1 常见的删除器类型
-
函数指针: 最简单的形式,适用于全局函数或静态成员函数。
void my_free_deleter(int* p) { std::cout << "Custom deleter: Calling free() for int* " << p << std::endl; free(p); // 对应 malloc 分配的内存 } void file_closer(FILE* f) { if (f) { std::cout << "Custom deleter: Closing file handle " << f << std::endl; fclose(f); } } -
Lambda 表达式: 现代C++中最常用且最灵活的方式,可以捕获上下文变量,非常简洁。
auto array_deleter = [](int* p) { std::cout << "Custom deleter: Deleting int array " << p << std::endl; delete[] p; // 对应 new int[...] 分配的数组 }; // 捕获一个日志对象,记录关闭信息 struct Logger { void log(const std::string& msg) { std::cout << msg << std::endl; } }; Logger my_logger; auto resource_cleanup = [&](MyResource* res) { my_logger.log("Custom deleter: Cleaning up MyResource."); res->cleanup(); delete res; }; -
仿函数(Functor): 实现
operator()的类对象,适用于需要维护状态的删除器。template <typename T> struct MyArrayDeleter { void operator()(T* p) const { std::cout << "Custom deleter (functor): Deleting array " << p << std::endl; delete[] p; } }; struct FileWriterDeleter { std::string filename_; FileWriterDeleter(const std::string& name) : filename_(name) {} void operator()(FILE* f) const { if (f) { std::cout << "Custom deleter (functor): Closing file " << filename_ << std::endl; fclose(f); } } };
4.2 将删除器传入 MySharedPtr
我们的MySharedPtr构造函数和reset方法都支持传入一个Deleter类型的参数,它通过模板参数Deleter被捕获到ControlBlock中。
// 构造函数:从原始指针和自定义删除器创建 MySharedPtr
template <typename Deleter>
MySharedPtr(T* p, Deleter d) : ptr_(p), control_block_(nullptr) {
if (p != nullptr) {
control_block_ = new ControlBlock<T, Deleter>(p, std::move(d));
}
}
这里,Deleter可以是void(*)(T*)(函数指针类型),也可以是lambda表达式的闭包类型(C++11开始,lambda表达式有唯一的匿名类型),或者是仿函数对象的类型。模板机制和ControlBlockBase的类型擦除功能使得MySharedPtr能够无缝地处理各种删除器。
第五讲:完整的代码示例与演示
现在,我们将把所有部分组合起来,提供一个完整的MySharedPtr实现,并编写一个main函数来演示其功能,包括默认行为、自定义删除器以及线程安全性。
#include <atomic> // For std::atomic
#include <utility> // For std::forward, std::move, std::swap
#include <functional> // For std::default_delete, std::function
#include <iostream> // For demonstration output
#include <thread> // For multi-threading demonstration
#include <vector> // For holding threads
// -----------------------------------------------------------------------------
// ControlBlockBase: 抽象基类
// -----------------------------------------------------------------------------
class ControlBlockBase {
public:
std::atomic<long> shared_count_;
std::atomic<long> weak_count_;
public:
ControlBlockBase() : shared_count_(1), weak_count_(0) {}
virtual ~ControlBlockBase() = default;
virtual void destroy() = 0;
virtual void delete_this() = 0;
};
// -----------------------------------------------------------------------------
// ControlBlock: 派生类
// -----------------------------------------------------------------------------
template <typename T, typename Deleter>
class ControlBlock : public ControlBlockBase {
private:
T* ptr_;
Deleter deleter_;
public:
ControlBlock(T* p, Deleter d) : ptr_(p), deleter_(std::move(d)) {}
~ControlBlock() override {
// No explicit action here, destroy() handles object deletion
// delete_this() handles control block deletion
}
void destroy() override {
if (ptr_ != nullptr) {
std::cout << " [ControlBlock] Calling custom deleter for object at " << ptr_ << std::endl;
deleter_(ptr_);
ptr_ = nullptr; // Prevent double deletion
}
}
void delete_this() override {
std::cout << " [ControlBlock] Deleting ControlBlock at " << this << std::endl;
delete this;
}
};
// -----------------------------------------------------------------------------
// MySharedPtr: 我们的自定义共享指针类
// -----------------------------------------------------------------------------
template <typename T>
class MySharedPtr {
private:
T* ptr_;
ControlBlockBase* control_block_;
void release() {
if (control_block_ != nullptr) {
if (control_block_->shared_count_.fetch_sub(1) == 1) {
// Last MySharedPtr, destroy the managed object
control_block_->destroy();
// If no weak_ptr also, destroy the control block
if (control_block_->weak_count_.load() == 0) {
control_block_->delete_this();
control_block_ = nullptr;
}
}
}
ptr_ = nullptr;
}
void swap(MySharedPtr& other) noexcept {
std::swap(ptr_, other.ptr_);
std::swap(control_block_, other.control_block_);
}
public:
// 默认构造函数
MySharedPtr() : ptr_(nullptr), control_block_(nullptr) {}
// 构造函数:从原始指针创建 MySharedPtr (使用默认 delete 作为删除器)
explicit MySharedPtr(T* p) : ptr_(p), control_block_(nullptr) {
if (p != nullptr) {
std::cout << " [MySharedPtr] Creating with default deleter for " << p << std::endl;
control_block_ = new ControlBlock<T, std::default_delete<T>>(p, std::default_delete<T>());
}
}
// 构造函数:从原始指针和自定义删除器创建 MySharedPtr
template <typename Deleter>
MySharedPtr(T* p, Deleter d) : ptr_(p), control_block_(nullptr) {
if (p != nullptr) {
std::cout << " [MySharedPtr] Creating with custom deleter for " << p << std::endl;
control_block_ = new ControlBlock<T, Deleter>(p, std::move(d));
}
}
// 拷贝构造函数
MySharedPtr(const MySharedPtr& other) noexcept : ptr_(other.ptr_), control_block_(other.control_block_) {
if (control_block_ != nullptr) {
control_block_->shared_count_.fetch_add(1);
std::cout << " [MySharedPtr] Copy constructing, new count: " << control_block_->shared_count_.load() << std::endl;
}
}
// 移动构造函数
MySharedPtr(MySharedPtr&& other) noexcept : ptr_(other.ptr_), control_block_(other.control_block_) {
std::cout << " [MySharedPtr] Move constructing." << std::endl;
other.ptr_ = nullptr;
other.control_block_ = nullptr;
}
// 析构函数
~MySharedPtr() {
std::cout << " [MySharedPtr] Destructing MySharedPtr for " << ptr_ << std::endl;
release();
}
// 拷贝赋值运算符 (copy-and-swap 惯用法)
MySharedPtr& operator=(const MySharedPtr& other) noexcept {
std::cout << " [MySharedPtr] Copy assigning." << std::endl;
MySharedPtr temp(other);
swap(temp);
return *this;
}
// 移动赋值运算符
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
std::cout << " [MySharedPtr] Move assigning." << std::endl;
if (this != &other) {
release();
ptr_ = other.ptr_;
control_block_ = other.control_block_;
other.ptr_ = nullptr;
other.control_block_ = nullptr;
}
return *this;
}
// 重置 MySharedPtr
void reset() {
std::cout << " [MySharedPtr] Calling reset()." << std::endl;
release();
}
template <typename Deleter>
void reset(T* p, Deleter d) {
std::cout << " [MySharedPtr] Calling reset(p, d)." << std::endl;
release();
if (p != nullptr) {
ptr_ = p;
control_block_ = new ControlBlock<T, Deleter>(p, std::move(d));
}
}
void reset(T* p) {
std::cout << " [MySharedPtr] Calling reset(p)." << std::endl;
release();
if (p != nullptr) {
ptr_ = p;
control_block_ = new ControlBlock<T, std::default_delete<T>>(p, std::default_delete<T>());
}
}
// 获取原始指针
T* get() const noexcept {
return ptr_;
}
// 解引用操作符
T& operator*() const noexcept {
return *ptr_;
}
// 成员访问操作符
T* operator->() const noexcept {
return ptr_;
}
// 获取引用计数
long use_count() const noexcept {
return (control_block_ != nullptr) ? control_block_->shared_count_.load() : 0;
}
// 检查是否拥有对象
explicit operator bool() const noexcept {
return ptr_ != nullptr;
}
// 允许 MySharedPtr 之间的隐式转换 (例如,MySharedPtr<Derived> 到 MySharedPtr<Base>)
// 但这里为了简化,我们暂时不实现复杂的转换。
// 如果需要,这里应该添加模板转换构造函数。
};
// -----------------------------------------------------------------------------
// 辅助类和函数,用于演示
// -----------------------------------------------------------------------------
struct MyResource {
int id;
MyResource(int _id) : id(_id) {
std::cout << "MyResource " << id << " constructed at " << this << std::endl;
}
~MyResource() {
std::cout << "MyResource " << id << " destructed from " << this << std::endl;
}
void do_work() {
std::cout << "MyResource " << id << " is doing work." << std::endl;
}
};
// 自定义删除器:用于 malloc 分配的内存
void my_free_deleter(int* p) {
std::cout << " [Custom Deleter] Using free() for int* " << p << std::endl;
free(p);
}
// 自定义删除器:用于文件句柄
void file_closer(FILE* f) {
if (f) {
std::cout << " [Custom Deleter] Closing file handle " << f << std::endl;
fclose(f);
}
}
// 自定义删除器:用于 MyResource 数组
struct MyResourceArrayDeleter {
void operator()(MyResource* p) const {
std::cout << " [Custom Deleter] Deleting MyResource array at " << p << std::endl;
delete[] p;
}
};
// -----------------------------------------------------------------------------
// 主函数:演示 MySharedPtr 的使用
// -----------------------------------------------------------------------------
int main() {
std::cout << "--- Demo 1: Basic MySharedPtr usage ---" << std::endl;
{
MySharedPtr<MyResource> sp1(new MyResource(1));
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // Expected: 1
MySharedPtr<MyResource> sp2 = sp1; // Copy construction
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // Expected: 2
std::cout << "sp2 use_count: " << sp2.use_count() << std::endl; // Expected: 2
MySharedPtr<MyResource> sp3;
sp3 = sp1; // Copy assignment
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // Expected: 3
std::cout << "sp3 use_count: " << sp3.use_count() << std::endl; // Expected: 3
sp1->do_work();
(*sp2).do_work();
sp3.reset(); // sp3 释放所有权
std::cout << "After sp3.reset():" << std::endl;
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // Expected: 2
std::cout << "sp3 use_count: " << sp3.use_count() << std::endl; // Expected: 0
MySharedPtr<MyResource> sp4(std::move(sp2)); // Move construction
std::cout << "After sp4(std::move(sp2)):" << std::endl;
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl; // Expected: 2
std::cout << "sp2 use_count: " << sp2.use_count() << std::endl; // Expected: 0
std::cout << "sp4 use_count: " << sp4.use_count() << std::endl; // Expected: 2
std::cout << "Exiting basic scope..." << std::endl;
} // sp1, sp4 离开作用域,MyResource(1) 被销毁
std::cout << "n--- Demo 2: Custom Deleters ---" << std::endl;
{
// 2.1 Custom deleter with function pointer (for malloc'd memory)
int* raw_int_ptr = (int*)malloc(sizeof(int));
*raw_int_ptr = 100;
MySharedPtr<int> sp_malloc(raw_int_ptr, my_free_deleter);
std::cout << "sp_malloc value: " << *sp_malloc << std::endl;
// 2.2 Custom deleter with lambda (for file handle)
FILE* file_handle = fopen("test.txt", "w");
if (file_handle) {
MySharedPtr<FILE> sp_file(file_handle, [](FILE* f){
std::cout << " [Custom Deleter Lambda] Closing file.txt" << std::endl;
fclose(f);
});
fprintf(sp_file.get(), "Hello from custom shared_ptr!n");
} else {
std::cerr << "Failed to open test.txt" << std::endl;
}
// 2.3 Custom deleter with functor (for array)
MySharedPtr<MyResource> sp_array(new MyResource[3]{{10}, {11}, {12}}, MyResourceArrayDeleter());
sp_array.get()[0].do_work();
sp_array.get()[1].do_work();
std::cout << "Exiting custom deleters scope..." << std::endl;
} // sp_malloc, sp_file, sp_array 离开作用域,各自的删除器被调用
std::cout << "n--- Demo 3: Thread Safety (simulated) ---" << std::endl;
{
MySharedPtr<MyResource> shared_res(new MyResource(200));
std::cout << "Initial shared_res use_count: " << shared_res.use_count() << std::endl;
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([shared_res]() { // Capture by value to create a new MySharedPtr
std::cout << " Thread " << std::this_thread::get_id()
<< " started. use_count: " << shared_res.use_count() << std::endl;
shared_res->do_work();
// Simulating some work
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << " Thread " << std::this_thread::get_id()
<< " finished. use_count: " << shared_res.use_count() << std::endl;
});
}
for (auto& t : threads) {
t.join();
}
std::cout << "All threads joined." << std::endl;
std::cout << "Final shared_res use_count: " << shared_res.use_count() << std::endl;
} // shared_res 离开作用域,MyResource(200) 被销毁
std::cout << "n--- Demo 4: Resetting with new resource ---" << std::endl;
{
MySharedPtr<MyResource> sp_reset(new MyResource(300));
std::cout << "sp_reset use_count: " << sp_reset.use_count() << std::endl; // Expected: 1
sp_reset.reset(new MyResource(301)); // Old MyResource(300) should be deleted
std::cout << "After reset with new resource:" << std::endl;
std::cout << "sp_reset use_count: " << sp_reset.use_count() << std::endl; // Expected: 1
MySharedPtr<MyResource> sp_reset_custom;
sp_reset_custom.reset(new MyResource[2]{{400}, {401}}, MyResourceArrayDeleter());
std::cout << "sp_reset_custom use_count: " << sp_reset_custom.use_count() << std::endl; // Expected: 1
std::cout << "Exiting reset demo scope..." << std::endl;
} // sp_reset, sp_reset_custom 离开作用域
std::cout << "n--- End of Demos ---" << std::endl;
return 0;
}
运行上述代码,你将观察到:
- 资源构造与析构的顺序:
MyResource的构造和析构信息会清晰地打印出来,证明了MySharedPtr正确地管理了对象的生命周期。 - 引用计数的准确性:
use_count()的输出会随着拷贝、赋值、reset和作用域结束而正确地增减。 - 自定义删除器的调用: 在“Demo 2”中,
my_free_deleter、lambda删除器和MyResourceArrayDeleter都会被正确调用,证明了自定义删除器机制的有效性。 - 线程安全: 在“Demo 3”中,多个线程同时访问同一个
MySharedPtr的副本,但引用计数始终保持正确,没有出现乱序或错误的计数,这归功于std::atomic的使用。每个线程中的shared_res实际上是原始shared_res的一个拷贝,因此每个线程开始时都会递增引用计数,结束时递减。主线程的shared_res会一直维持一个较高的计数,直到所有线程都结束。
第六讲:展望与进一步思考
我们已经成功手写了一个功能相对完善的MySharedPtr,它支持线程安全的引用计数和自定义删除器。但标准的std::shared_ptr还有更多高级特性和优化,值得我们去了解和思考。
6.1 std::make_shared 的优势
在我们的实现中,我们通过new ControlBlock(...)和new T(...)分两次分配内存。std::make_shared是一个工厂函数,它能在一个单独的内存块中同时分配被管理对象和控制块。这带来了以下好处:
- 性能提升: 减少了一次内存分配的开销。
- 内存局部性: 对象和控制块在内存中更接近,可能带来更好的缓存性能。
- 异常安全: 避免了在
new T成功后、new ControlBlock失败时导致的内存泄漏问题。如果new T和new ControlBlock之间发生异常,原始指针可能无法被正确删除。
实现make_shared需要更复杂的内存布局和模板技巧,超出了本次讲座的范围,但理解其价值非常重要。
6.2 std::weak_ptr 的角色
在我们的ControlBlockBase中,我们预留了weak_count_,但没有实现MyWeakPtr。weak_ptr是解决shared_ptr循环引用问题的关键。当两个或多个对象通过shared_ptr相互引用时,它们的引用计数永远不会降为零,导致内存泄漏。weak_ptr允许你观察一个对象而不拥有它,它不影响对象的生命周期。通过将循环引用中的一个shared_ptr替换为weak_ptr,可以打破循环,使得对象能在不再被shared_ptr拥有时被正确销毁。
6.3 性能考量与替代方案
尽管shared_ptr非常方便,但它并非没有开销:
- 引用计数开销: 每次拷贝、赋值或销毁都会涉及原子操作,这比普通的整数操作要慢。
- 控制块开销: 额外的内存分配和间接访问。
在某些对性能极其敏感的场景,或者当所有权模型非常简单(例如,单一所有权),std::unique_ptr通常是更好的选择,因为它没有引用计数开销。如果你的资源管理只需要在特定作用域内,或者所有权是独占的,优先考虑unique_ptr。
6.4 智能指针的转换
标准的std::shared_ptr支持不同类型之间的安全转换,例如static_pointer_cast、dynamic_pointer_cast和const_pointer_cast,这些都是在保持所有权语义的同时,实现多态性和类型安全的强大工具。
6.5 从实践到原理的升华
通过这次手写shared_ptr的实践,我们不仅学习了其内部实现细节,更重要的是,我们加深了对C++内存管理、RAII原则、类型擦除、模板编程以及并发编程中线程安全挑战的理解。这些知识对于成为一名优秀的C++开发者至关重要。希望大家能够将这些原理内化于心,在未来的编程实践中,能够更加自信、高效地编写健壮的代码。