C++中的智能指针:实现自己的unique_ptr

智能指针是 C++11 标准引入的关键特性,用来简化资源管理并避免内存泄漏。最常见的智能指针有 std::unique_ptrstd::shared_ptrstd::weak_ptr,其中 std::unique_ptr 以独占所有权的方式管理资源,类似于单例对象。本文将手动实现一个简易的 unique_ptr,从中学习其内部机制与常见错误。

1. 设计目标

  • 独占所有权:同一时刻只能有一个指针拥有资源。
  • 移动语义:支持移动构造和移动赋值,不能拷贝。
  • 自定义删除器:支持传入自定义析构函数。
  • 异常安全:构造失败后不泄漏资源。

2. 基本结构

template<typename T, typename Deleter = std::default_delete<T>>
class SimpleUniquePtr {
public:
    // 构造函数
    explicit SimpleUniquePtr(T* ptr = nullptr, Deleter del = Deleter())
        : ptr_(ptr), deleter_(del) {}

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

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

    // 移动赋值
    SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept {
        if (this != &other) {
            reset();
            ptr_ = other.ptr_;
            deleter_ = std::move(other.deleter_);
            other.ptr_ = nullptr;
        }
        return *this;
    }

    // 析构
    ~SimpleUniquePtr() { reset(); }

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

    // 重置资源
    void reset(T* ptr = nullptr) noexcept {
        if (ptr_ != ptr) {
            if (ptr_) deleter_(ptr_);
            ptr_ = ptr;
        }
    }

    // 移除所有权
    T* release() noexcept {
        T* old = ptr_;
        ptr_ = nullptr;
        return old;
    }

    // 判断是否为空
    explicit operator bool() const noexcept { return ptr_ != nullptr; }

private:
    T* ptr_;
    Deleter deleter_;
};

3. 关键实现细节

  1. 移动语义

    • SimpleUniquePtr 的移动构造函数将源对象的 ptr_deleter_ 迁移到目标对象,并把源对象的 ptr_ 置为 nullptr,从而确保资源只有一个所有者。
    • noexcept 标记保证移动操作不会抛异常,从而支持标准库容器对 unique_ptr 的内部使用。
  2. 自定义删除器

    • Deleter 是模板参数,默认为 `std::default_delete `。
    • 通过 std::move 将删除器也移动,以保持删除器的独占性。
  3. 异常安全

    • 在构造函数中直接将传入的指针赋值给成员,若构造失败(如 new 失败)会抛异常,成员 ptr_ 仍为 nullptr,不泄漏资源。
    • reset 与析构函数均使用 noexcept,避免异常进一步传播。

4. 示例使用

int main() {
    SimpleUniquePtr <int> p1(new int(10));
    std::cout << *p1 << std::endl;           // 输出 10

    SimpleUniquePtr <int> p2 = std::move(p1); // 资源转移
    if (!p1) std::cout << "p1 is empty\n";

    p2.reset(new int(20));                   // 替换资源
    std::cout << *p2 << std::endl;           // 输出 20

    // 自定义删除器
    SimpleUniquePtr <int> p3(new int(30),
        [](int* ptr){ std::cout << "delete: " << *ptr << '\n'; delete ptr; });

    return 0;
}

运行结果(假设输入输出正常):

10
p1 is empty
20
delete: 30

5. 常见错误与调试技巧

错误 说明 解决方案
双重删除 误将 ptr_ 复制给另一个指针,而未移动所有权。 确保所有 unique_ptr 操作都是移动,禁止拷贝。
悬空指针 通过 release() 释放所有权后忘记销毁。 release() 返回后自行管理资源或再包装成 unique_ptr
异常泄漏 reset 或构造中抛异常,导致未释放资源。 通过 noexcept 和 RAII 确保析构时释放。
自定义删除器漏删 删除器未正确实现或未被移动。 确保删除器是可调用对象且在 reset 时调用。

6. 进一步扩展

  • 数组支持:实现 `SimpleArrayUniquePtr `,类似 `std::unique_ptr`。
  • 延迟销毁:结合 std::weak_ptrstd::shared_ptr,实现多级所有权。
  • 多线程安全:在多线程环境下使用互斥锁保护 ptr_,但这会破坏 unique_ptr 的无锁特性,需谨慎使用。

7. 结语

实现自己的 unique_ptr 可以帮助你深入理解 C++ 的所有权语义、移动语义以及异常安全。通过手写代码,你可以更好地把握资源管理的细节,进而在实际项目中更自信地使用标准库提供的智能指针。祝你编码愉快!

发表评论