在 C++ 中,智能指针是管理资源的重要工具。虽然 std::unique_ptr、std::shared_ptr 等已经为我们提供了常用的实现,但在某些特殊场景下,我们可能需要一个定制化的智能指针。本文将从设计思路、关键实现细节以及常见误区三个方面,系统阐述如何实现一个既安全又高效的自定义智能指针。
一、设计目标
- 所有权语义:支持独占(类似
unique_ptr)与共享(类似shared_ptr)两种所有权模式,且可以在编译期选择。 - 资源回收:使用 RAII 原则,确保对象生命周期结束时自动释放资源。
- 线程安全:在共享模式下,计数器的增减必须原子化。
- 可扩展性:支持自定义 deleter、可与 STL 容器配合使用。
二、核心实现
2.1 基础模板
template <typename T, bool Shared = false>
class SmartPtr;
Shared控制所有权模式。若为false,实现独占指针;若为true,实现共享指针。
2.2 独占指针实现
template <typename T>
class SmartPtr<T, false> {
private:
T* ptr_;
public:
explicit SmartPtr(T* p = nullptr) noexcept : ptr_(p) {}
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
SmartPtr(SmartPtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
SmartPtr& operator=(SmartPtr&& other) noexcept {
reset();
ptr_ = other.ptr_;
other.ptr_ = nullptr;
return *this;
}
~SmartPtr() { reset(); }
T& operator*() const noexcept { return *ptr_; }
T* operator->() const noexcept { return ptr_; }
T* get() const noexcept { return ptr_; }
explicit operator bool() const noexcept { return ptr_ != nullptr; }
void reset(T* p = nullptr) noexcept {
if (ptr_) delete ptr_;
ptr_ = p;
}
};
2.3 共享指针实现
共享指针需要一个控制块来维护引用计数与 deleter。实现简化示例:
template <typename T>
class SmartPtr<T, true> {
private:
struct ControlBlock {
std::atomic <size_t> count{1};
T* ptr;
void (*deleter)(T*) = [](T* p){ delete p; };
explicit ControlBlock(T* p, void(*d)(T*) = nullptr)
: ptr(p), deleter(d ? d : [](T* p){ delete p; }) {}
};
ControlBlock* cb_;
void release() noexcept {
if (!cb_) return;
if (cb_->count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
cb_->deleter(cb_->ptr);
delete cb_;
}
cb_ = nullptr;
}
public:
explicit SmartPtr(T* p = nullptr, void(*d)(T*) = nullptr) noexcept
: cb_(p ? new ControlBlock(p, d) : nullptr) {}
SmartPtr(const SmartPtr& other) noexcept
: cb_(other.cb_) {
if (cb_) cb_->count.fetch_add(1, std::memory_order_acq_rel);
}
SmartPtr& operator=(const SmartPtr& other) noexcept {
if (this != &other) {
release();
cb_ = other.cb_;
if (cb_) cb_->count.fetch_add(1, std::memory_order_acq_rel);
}
return *this;
}
SmartPtr(SmartPtr&& other) noexcept : cb_(other.cb_) {
other.cb_ = nullptr;
}
SmartPtr& operator=(SmartPtr&& other) noexcept {
if (this != &other) {
release();
cb_ = other.cb_;
other.cb_ = nullptr;
}
return *this;
}
~SmartPtr() { release(); }
T& operator*() const noexcept { return *(cb_->ptr); }
T* operator->() const noexcept { return cb_->ptr; }
T* get() const noexcept { return cb_ ? cb_->ptr : nullptr; }
explicit operator bool() const noexcept { return cb_ && cb_->ptr; }
size_t use_count() const noexcept { return cb_ ? cb_->count.load(std::memory_order_relaxed) : 0; }
};
2.4 自定义 deleter 支持
通过在 SmartPtr 构造函数中接收 deleter,用户可以实现非 delete 的资源释放方式,例如:
SmartPtr<Foo, true> p(new Foo, [](Foo* f){ f->cleanup(); delete f; });
三、常见误区
-
拷贝构造时忘记计数
共享指针的拷贝构造必须显式递增引用计数。若未递增,多个指针指向同一对象会导致重复删除。 -
多线程下计数器不原子
共享指针的计数器若不是原子操作,在并发环境中会产生数据竞争,导致程序崩溃。请使用std::atomic。 -
自定义 deleter 与控制块不匹配
如果 deleter 需要额外信息(如引用计数),应在控制块中存储相关成员,或使用std::shared_ptr的自定义 deleter/allocator 机制。 -
忽略异常安全
在构造函数中分配控制块或执行 deleter 时若抛异常,需保证已分配的资源能被正确释放。使用try-catch或 RAII 包装器是常见做法。
四、使用示例
struct Resource {
void release() { std::cout << "Resource released\n"; }
};
int main() {
// 独占指针
SmartPtr<Resource, false> up(new Resource);
// 共享指针
SmartPtr<Resource, true> sp1(new Resource);
SmartPtr<Resource, true> sp2 = sp1;
std::cout << "Use count: " << sp2.use_count() << '\n';
// 自定义 deleter
SmartPtr<Resource, true> sp3(new Resource,
[](Resource* r){ r->release(); delete r; });
return 0;
}
五、总结
自定义智能指针可以在满足特殊需求时提供更大的灵活性。关键在于:
- 明确所有权语义并保持一致;
- 正确实现计数器与资源释放逻辑;
- 在多线程环境下确保原子操作;
- 提供易用的接口以降低使用成本。
在实际项目中,如果需求不超过标准库所能覆盖的范围,建议优先使用 std::unique_ptr / std::shared_ptr。但在需要特殊 deleter、与旧接口兼容或学习目的时,自定义实现会是一个不错的练手项目。