如何在C++中实现一个自定义的智能指针?

在 C++11 之后,标准库已经提供了 std::unique_ptrstd::shared_ptrstd::weak_ptr 等智能指针,它们大大简化了资源管理的难度。然而在某些特殊场景下,我们可能需要一个既能像 std::unique_ptr 那样保证唯一所有权,又能在内部实现自定义行为(例如日志、引用计数的定制、资源池的回收等)的智能指针。下面我们以实现一个名为 SimpleUniquePtr 的自定义智能指针为例,展示其设计思路、核心实现细节以及使用示例。

1. 需求分析

功能 说明
1. 唯一所有权 只能有一个 SimpleUniquePtr 拥有该资源,复制构造/赋值禁止
2. 自动析构 当指针离开作用域时自动释放资源
3. 自定义析构器 可以在构造时传入自定义的析构函数
4. 日志跟踪 每次资源获取、释放时打印日志
5. 可自定义内存分配 允许使用自定义的 new/delete 组合

2. 设计要点

  • 内部结构SimpleUniquePtr 只保存一个原始指针 T* ptr_ 和一个析构函数 std::function<void(T*)> deleter_。构造时若未传入 deleter_,则默认使用 delete
  • 移动语义:实现移动构造和移动赋值操作,保证资源所有权转移时原指针置为空。
  • 禁止拷贝:拷贝构造和拷贝赋值都被删除,防止出现多重所有权。
  • 日志:在构造、析构、移动等关键时刻打印日志,便于调试和性能分析。
  • 自定义分配器:通过 allocate 静态成员模板提供对资源的分配,并在 SimpleUniquePtr 中持有 Allocator*(如果需要)。

3. 核心代码

#include <iostream>
#include <functional>
#include <memory>
#include <utility>
#include <chrono>
#include <iomanip>

template <typename T>
class SimpleUniquePtr {
public:
    // 默认析构器
    using Deleter = std::function<void(T*)>;

    // 构造:接受裸指针和可选析构器
    explicit SimpleUniquePtr(T* ptr = nullptr, Deleter deleter = nullptr)
        : ptr_(ptr), deleter_(deleter ? std::move(deleter) : DefaultDeleter()) {
        log("Constructed", ptr_);
    }

    // 移动构造
    SimpleUniquePtr(SimpleUniquePtr&& other) noexcept
        : ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
        other.ptr_ = nullptr;
        log("Move Constructed", ptr_);
    }

    // 移动赋值
    SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept {
        if (this != &other) {
            reset();               // 先释放自身资源
            ptr_ = other.ptr_;
            deleter_ = std::move(other.deleter_);
            other.ptr_ = nullptr;
            log("Move Assigned", ptr_);
        }
        return *this;
    }

    // 禁止拷贝
    SimpleUniquePtr(const SimpleUniquePtr&) = delete;
    SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete;

    // 析构
    ~SimpleUniquePtr() {
        reset();
        log("Destructed", ptr_);
    }

    // 访问
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }

    // 手动释放
    void reset(T* ptr = nullptr, Deleter deleter = nullptr) {
        if (ptr_) {
            deleter_(ptr_);
            log("Reset", ptr_);
        }
        ptr_ = ptr;
        deleter_ = deleter ? std::move(deleter) : DefaultDeleter();
    }

    // 取出裸指针(不释放)
    T* release() {
        T* old = ptr_;
        ptr_ = nullptr;
        log("Released", old);
        return old;
    }

private:
    static Deleter DefaultDeleter() {
        return [](T* p) { delete p; };
    }

    void log(const std::string& msg, T* ptr) const {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        std::tm tm;
        localtime_r(&time, &tm);
        std::cout << "[" << std::put_time(&tm, "%F %T") << "] " << msg << " (ptr=" << static_cast<void*>(ptr) << ")\n";
    }

    T* ptr_ = nullptr;
    Deleter deleter_;
};

4. 使用示例

// 自定义资源
struct Resource {
    int id;
    Resource(int v) : id(v) { std::cout << "Resource(" << id << ") constructed.\n"; }
    ~Resource() { std::cout << "Resource(" << id << ") destroyed.\n"; }
};

int main() {
    // 1. 默认析构器
    SimpleUniquePtr <Resource> ptr1(new Resource(1));

    // 2. 自定义析构器(比如回收到对象池)
    auto poolDeleter = [](Resource* r) {
        std::cout << "Returning Resource(" << r->id << ") to pool.\n";
        delete r;   // 这里仅做示例,实际可实现对象池
    };
    SimpleUniquePtr <Resource> ptr2(new Resource(2), poolDeleter);

    // 3. 移动语义
    SimpleUniquePtr <Resource> ptr3 = std::move(ptr1);
    if (!ptr1.get()) std::cout << "ptr1 is empty after move.\n";

    // 4. 手动释放
    Resource* raw = ptr2.release();   // ptr2 失去所有权
    std::cout << "Raw pointer id: " << raw->id << "\n";
    delete raw; // 需要手动删除

    return 0;
}

5. 进一步扩展

  • 引用计数:可以在内部加入一个 `std::atomic ` 计数器,支持 `SimpleSharedPtr` 的实现。
  • 多线程安全:使用 std::mutexstd::atomic 保护内部状态,确保在多线程环境下移动、reset 等操作安全。
  • 自定义分配器:引入 `std::allocator ` 或自定义 `Allocator` 类,让 `SimpleUniquePtr` 在 `new`/`delete` 之外使用预分配内存池。

6. 小结

  • SimpleUniquePtr 通过组合原始指针和 std::function 实现自定义析构器。
  • 移动语义保证了唯一所有权,拷贝被删除以避免错误。
  • 日志功能帮助跟踪资源生命周期,适合调试和性能分析。
  • 可进一步扩展为多功能智能指针或与内存池配合使用。

自定义智能指针虽然在标准库已完备的情况下不常见,但在需要细粒度控制资源释放或在老旧项目中使用时,仍然是一个非常实用且灵活的工具。

发表评论