C++ 中如何实现自定义的智能指针

在现代 C++ 编程中,智能指针是管理动态资源的关键工具。虽然标准库已经提供了 std::unique_ptrstd::shared_ptrstd::weak_ptr,但在某些特殊场景下我们可能需要一个定制化的智能指针来满足特殊需求,例如对资源进行统一的计数、延迟释放或自定义回调。本文将从设计原则出发,演示如何实现一个最小可用的自定义智能指针,并讨论其在实际项目中的应用场景。

一、设计目标

  1. 自动资源管理:在智能指针离开作用域时自动释放资源。
  2. 引用计数:支持多对象共享同一资源,并在最后一个指针销毁时释放资源。
  3. 线程安全:在多线程环境下能够正确地增减引用计数。
  4. 易于使用:接口与标准智能指针保持一致,方便替换。
  5. 可扩展性:可以方便地添加自定义回调或日志功能。

二、核心实现思路

1. 控制块(Control Block)

控制块是所有共享对象共用的一块内存,用于存放引用计数和原始指针。典型结构如下:

template <typename T>
struct ControlBlock {
    std::atomic <size_t> ref_count{1};
    T* ptr;
    std::function<void(T*)> deleter; // 自定义删除器

    ControlBlock(T* p, std::function<void(T*)> d)
        : ptr(p), deleter(d) {}
};
  • ref_countstd::atomic 实现线程安全。
  • deleter 允许用户传入自定义删除逻辑,例如文件句柄、网络连接等。

2. 自定义智能指针类

template <typename T>
class MySharedPtr {
public:
    // 构造
    explicit MySharedPtr(T* p = nullptr,
                         std::function<void(T*)> d = [](T* p){ delete p; })
        : control_(p ? new ControlBlock <T>(p, d) : nullptr) {}

    // 拷贝构造
    MySharedPtr(const MySharedPtr& other) noexcept
        : control_(other.control_) {
        if (control_) control_->ref_count.fetch_add(1, std::memory_order_relaxed);
    }

    // 移动构造
    MySharedPtr(MySharedPtr&& other) noexcept
        : control_(other.control_) {
        other.control_ = nullptr;
    }

    // 赋值
    MySharedPtr& operator=(const MySharedPtr& other) noexcept {
        if (this != &other) {
            release();
            control_ = other.control_;
            if (control_) control_->ref_count.fetch_add(1, std::memory_order_relaxed);
        }
        return *this;
    }

    // 移动赋值
    MySharedPtr& operator=(MySharedPtr&& other) noexcept {
        if (this != &other) {
            release();
            control_ = other.control_;
            other.control_ = nullptr;
        }
        return *this;
    }

    // 析构
    ~MySharedPtr() { release(); }

    // 访问
    T& operator*() const noexcept { return *(control_->ptr); }
    T* operator->() const noexcept { return control_->ptr; }
    T* get() const noexcept { return control_ ? control_->ptr : nullptr; }

    size_t use_count() const noexcept { return control_ ? control_->ref_count.load(std::memory_order_relaxed) : 0; }

private:
    ControlBlock <T>* control_ = nullptr;

    void release() {
        if (control_ && control_->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            control_->deleter(control_->ptr);
            delete control_;
        }
    }
};

3. 使用示例

struct Resource {
    void do_something() { std::cout << "资源被使用\n"; }
};

void custom_deleter(Resource* r) {
    std::cout << "自定义删除器被调用\n";
    delete r;
}

int main() {
    MySharedPtr <Resource> p1(new Resource());
    {
        MySharedPtr <Resource> p2 = p1; // 共享引用
        p2->do_something();
        std::cout << "引用计数:" << p2.use_count() << "\n";
    } // p2 离开作用域

    std::cout << "引用计数:" << p1.use_count() << "\n";

    MySharedPtr <Resource> p3(new Resource(), custom_deleter);
    p3->do_something();
} // p1, p3 离开作用域,分别调用删除器

输出示例:

资源被使用
引用计数:2
引用计数:1
资源被使用
自定义删除器被调用
自定义删除器被调用

三、实际应用场景

  1. 跨平台资源管理
    在需要在多平台(Windows/Linux/Android)之间共享同一资源对象时,自定义删除器可以针对不同平台实现不同的释放逻辑,保持统一的接口。

  2. 性能优化
    标准 std::shared_ptr 在每个实例中都会携带一个计数器,如果项目对内存占用极端敏感,可以采用更轻量的控制块或自定义计数实现。

  3. 延迟释放
    对于某些需要在特定事件发生后才真正释放的资源(如数据库连接池),可以将删除器实现为延迟回调。

  4. 日志与监控
    自定义删除器可以集成日志记录,方便追踪资源生命周期,定位内存泄漏。

四、常见坑与最佳实践

  • 异常安全:在构造函数中若出现异常,确保已分配的控制块不会泄漏。这里使用了 RAII 包装器 std::unique_ptr<ControlBlock<T>> 或者在 try/catch 中手动 delete
  • 循环引用:自定义智能指针与标准 std::weak_ptr 一样,若两个对象相互持有 MySharedPtr,会导致循环引用。此时应使用 MyWeakPtr 或手动拆解引用。
  • 多线程同步:虽然 std::atomic 提供原子操作,但如果需要更高层次的锁定,例如在删除资源时进行复杂的同步,建议在删除器内部加锁。
  • 模板特化:针对特定类型可进行模板特化,提供更高效的删除逻辑。

五、结语

通过对控制块和智能指针类的分离实现,我们可以在不牺牲安全性的前提下,获得更大的灵活性。自定义智能指针并不一定需要覆写所有标准特性,只要满足项目需求即可。希望本文能帮助你在 C++ 项目中更好地管理资源,写出更可靠、更易维护的代码。

发表评论