在现代 C++ 开发中,智能指针(如 std::shared_ptr、std::unique_ptr)已经成为管理资源的核心工具。然而,在一些特殊场景下,标准库提供的智能指针可能不满足需求,或者需要更细粒度的控制。这时,自定义智能指针显得尤为重要。本文将从实现思路、关键技术点以及典型应用场景三个维度,详细剖析如何在 C++ 中实现一个自定义智能指针。
1. 为什么要自定义智能指针?
-
自定义引用计数策略
标准的std::shared_ptr使用原子引用计数,线程安全,但在单线程或轻量级场景下会带来不必要的开销。可以实现一个非原子计数器,或使用读写锁优化。 -
延迟销毁或回收
某些对象需要延迟销毁,例如需要在特定时间点或条件下回收。自定义智能指针可以包装一个回调或生命周期管理器,满足此需求。 -
多重资源管理
例如一个对象同时拥有文件句柄、网络连接和内存,想要一次性管理。可以在智能指针中统一处理所有资源的释放。 -
安全性与可测性
标准智能指针不允许自定义拷贝构造时的行为。自定义指针可以提供更细粒度的控制,例如在拷贝时执行特定日志或监控。
2. 关键技术实现
下面给出一个简化版的 MySharedPtr,实现基本的引用计数和自定义销毁策略。
#include <atomic>
#include <memory>
#include <utility>
#include <iostream>
template <typename T>
class MySharedPtr {
public:
// 默认构造
MySharedPtr() noexcept : ptr_(nullptr), ref_count_(nullptr) {}
// 从裸指针构造
explicit MySharedPtr(T* ptr) : ptr_(ptr) {
ref_count_ = new std::atomic <size_t>(1);
}
// 拷贝构造
MySharedPtr(const MySharedPtr& other) noexcept
: ptr_(other.ptr_), ref_count_(other.ref_count_) {
if (ref_count_) {
ref_count_->fetch_add(1, std::memory_order_relaxed);
}
}
// 移动构造
MySharedPtr(MySharedPtr&& other) noexcept
: ptr_(other.ptr_), ref_count_(other.ref_count_) {
other.ptr_ = nullptr;
other.ref_count_ = nullptr;
}
// 拷贝赋值
MySharedPtr& operator=(const MySharedPtr& other) noexcept {
if (this != &other) {
release();
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
if (ref_count_) {
ref_count_->fetch_add(1, std::memory_order_relaxed);
}
}
return *this;
}
// 移动赋值
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) {
release();
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
other.ptr_ = nullptr;
other.ref_count_ = nullptr;
}
return *this;
}
// 析构
~MySharedPtr() {
release();
}
// 访问对象
T* get() const noexcept { return ptr_; }
T& operator*() const noexcept { return *ptr_; }
T* operator->() const noexcept { return ptr_; }
// 用自定义销毁器
template<typename Deleter>
void set_deleter(Deleter deleter) noexcept {
// 这里实现自定义 deleter 逻辑
// 例如存储 deleter 并在 release 时调用
// 省略实现细节
}
private:
void release() noexcept {
if (ref_count_ && ref_count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr_;
delete ref_count_;
}
}
T* ptr_;
std::atomic <size_t>* ref_count_;
};
2.1 线程安全性考虑
- 原子计数:使用 `std::atomic `,保证在多线程下拷贝、析构的安全。
- 内存序:
fetch_add使用memory_order_relaxed,因为这里只需要计数的原子性。fetch_sub使用memory_order_acq_rel,保证在最后一次释放时同步。
2.2 自定义销毁器
如果想让 MySharedPtr 支持自定义销毁器,可以将 ref_count_ 换成一个结构体,包含计数和 deleter。实现方式类似于 std::shared_ptr 的控制块。
struct ControlBlock {
std::atomic <size_t> ref_count;
std::function<void(T*)> deleter;
ControlBlock(T* p, std::function<void(T*)> d)
: ref_count(1), deleter(std::move(d)) {}
};
然后在 release 时调用 deleter(ptr_)。
2.3 与 RAII 的结合
自定义智能指针天然符合 RAII 原则,资源在离开作用域时自动释放。可以结合 std::optional、std::variant 等高级类型进一步扩展。
3. 典型使用场景
3.1 资源池化
在高性能游戏或服务器中,频繁的 new/delete 会导致碎片。可以用自定义智能指针包装对象池,例如:
class ObjectPool {
public:
T* acquire();
void release(T* ptr);
};
template<typename T>
class PoolPtr {
T* ptr_;
ObjectPool* pool_;
public:
PoolPtr(T* p, ObjectPool* pool) : ptr_(p), pool_(pool) {}
~PoolPtr() { if (ptr_) pool_->release(ptr_); }
// ... 访问语法
};
3.2 线程局部缓存
有时需要在每个线程中存放一个共享资源的副本。可以在 thread_local 变量中持有自定义智能指针,保证线程安全。
3.3 延迟释放的场景
例如图形渲染管线,某些纹理资源需要在帧结束后统一释放。可以用回调函数或事件队列在 release 时推迟到帧结束。
4. 性能比较
| 指标 | std::shared_ptr | MySharedPtr |
|---|---|---|
| 引用计数存储 | 原子变量 | 原子变量 |
| 内存占用 | 控制块 + 指针 | 控制块 + 指针 |
| 构造/析构 | 2 次内存分配 | 1 次内存分配(如果自定义) |
| 线程安全 | 原子操作 | 原子操作 |
| 可定制化 | 限制 | 高度可定制 |
在大多数业务场景下,使用标准库已足够;但当需要定制化资源管理或性能优化时,自定义智能指针能提供更好的灵活性。
5. 结语
自定义智能指针是一把双刃剑:它可以提供更细粒度的资源控制和性能优化,但也会增加代码复杂度与维护成本。建议在以下两种情况慎重使用:
- 标准库已满足需求时,优先使用
std::unique_ptr/std::shared_ptr。 - 当业务场景确实需要自定义销毁策略、资源池化或特殊线程安全策略时,再考虑自定义实现。
通过对引用计数、线程安全、销毁器等核心细节的深入理解,你可以根据项目需求灵活选择合适的智能指针实现方式,从而让 C++ 代码既安全又高效。