在 C++20 之前,实现自定义智能指针往往需要手动管理引用计数、线程安全以及异常安全。随着标准的演进,std::shared_ptr 和 std::unique_ptr 已经提供了非常完整的功能,通常不再需要自己实现。但在某些极端场景下,例如需要与旧库交互、对生命周期做特殊约束,或实现特殊资源管理策略,手写智能指针仍然有意义。下面给出一个基于 std::atomic 的线程安全引用计数实现,并说明关键点。
1. 基本结构
template <typename T>
class SharedPtr {
public:
explicit SharedPtr(T* ptr = nullptr);
SharedPtr(const SharedPtr& other);
SharedPtr(SharedPtr&& other) noexcept;
~SharedPtr();
SharedPtr& operator=(const SharedPtr& other);
SharedPtr& operator=(SharedPtr&& other) noexcept;
T& operator*() const noexcept;
T* operator->() const noexcept;
T* get() const noexcept { return ptr_; }
std::size_t use_count() const noexcept { return count_->load(); }
private:
void release();
T* ptr_;
std::atomic<std::size_t>* count_;
};
ptr_存放实际指针。count_指向一个原子计数器。计数器的生命周期与SharedPtr对象共享。
2. 构造与析构
template <typename T>
SharedPtr <T>::SharedPtr(T* ptr) : ptr_(ptr) {
if (ptr) {
count_ = new std::atomic<std::size_t>(1);
} else {
count_ = nullptr;
}
}
template <typename T>
SharedPtr <T>::~SharedPtr() {
release();
}
- 直接裸指针构造时,计数器初始化为 1。
nullptr时,计数器为nullptr,表示空指针。
3. 拷贝构造
template <typename T>
SharedPtr <T>::SharedPtr(const SharedPtr& other)
: ptr_(other.ptr_), count_(other.count_) {
if (count_) {
count_->fetch_add(1, std::memory_order_relaxed);
}
}
- 使用
fetch_add递增计数。memory_order_relaxed适用于计数器,因为引用计数本身不需要同步其他内存操作。
4. 移动构造
template <typename T>
SharedPtr <T>::SharedPtr(SharedPtr&& other) noexcept
: ptr_(other.ptr_), count_(other.count_) {
other.ptr_ = nullptr;
other.count_ = nullptr;
}
- 只转移指针和计数器,后者置空。
5. 赋值运算符
template <typename T>
SharedPtr <T>& SharedPtr<T>::operator=(const SharedPtr& other) {
if (this != &other) {
release(); // 先释放自身
ptr_ = other.ptr_;
count_ = other.count_;
if (count_) count_->fetch_add(1, std::memory_order_relaxed);
}
return *this;
}
template <typename T>
SharedPtr <T>& SharedPtr<T>::operator=(SharedPtr&& other) noexcept {
if (this != &other) {
release();
ptr_ = other.ptr_;
count_ = other.count_;
other.ptr_ = nullptr;
other.count_ = nullptr;
}
return *this;
}
- 赋值前先释放旧资源,防止泄露。
6. 资源释放
template <typename T>
void SharedPtr <T>::release() {
if (count_ && count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr_;
delete count_;
}
}
fetch_sub返回递减前的值。若递减后为 0,则当前实例是最后一个持有者,需销毁对象和计数器。- 使用
memory_order_acq_rel以确保析构顺序的可见性。
7. 访问运算符
template <typename T>
T& SharedPtr <T>::operator*() const noexcept {
return *ptr_;
}
template <typename T>
T* SharedPtr <T>::operator->() const noexcept {
return ptr_;
}
- 简单地转发给内部指针。
8. 线程安全与异常安全
- 引用计数本身是原子操作,线程安全。
- 构造时若分配计数器失败(
new抛异常),对象已处于空状态,析构时不做任何操作,保证不泄露。 - 赋值时先释放后获取,避免异常导致资源泄露。
9. 使用示例
int main() {
SharedPtr <int> p1(new int(42));
SharedPtr <int> p2 = p1; // 计数 2
{
SharedPtr <int> p3(std::move(p1)); // 计数 2, p1 为空
std::cout << *p3 << " 计数: " << p3.use_count() << '\n';
} // p3 销毁,计数 1
std::cout << *p2 << " 计数: " << p2.use_count() << '\n';
return 0;
}
输出:
42 计数: 2
42 计数: 1
10. 进一步改进
- 自定义删除器:在构造函数中加入模板参数
Deleter,类似std::unique_ptr的实现。 - 弱引用:实现 `WeakPtr ` 与 `SharedPtr` 配合使用,解决循环引用。
- 内存池:对计数器做对象池化,减少
new/delete频繁开销。
结语
通过上述实现,已完成一个最小化、线程安全、异常安全的自定义 SharedPtr。虽然标准库已提供了成熟的 std::shared_ptr,但在需要自定义生命周期管理或与旧 API 集成时,手写智能指针仍然是可行且有用的方案。希望本文能帮助你在特定场景中快速搭建自己的智能指针。