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

在 C++17 之后,智能指针已经非常成熟且易于使用,std::unique_ptr 和 std::shared_ptr 等已覆盖大多数需求。然而,学习如何从零实现自己的智能指针,有助于深入理解内存管理、所有权语义和 RAII 机制。下面我们用最简洁的方式实现一个类似 std::unique_ptr 的 MyUniquePtr,并展示它的使用场景。

1. 设计目标

  • 单一所有权:只能有一个指针实例管理同一块内存。
  • 自动释放:析构时自动 delete 对象。
  • 移动语义:支持移动构造和移动赋值,防止无意中复制。
  • 防止拷贝:拷贝构造和拷贝赋值被禁用。
  • 提供原始指针访问:通过 operator*, operator->, get() 等。

2. 代码实现

#include <utility>  // std::swap
#include <cassert>  // assert

template <typename T>
class MyUniquePtr {
public:
    // 默认构造,指针为空
    MyUniquePtr() noexcept : ptr_(nullptr) {}

    // 从裸指针构造
    explicit MyUniquePtr(T* ptr) noexcept : ptr_(ptr) {}

    // 析构:释放资源
    ~MyUniquePtr() { reset(); }

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

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

    // 移动赋值
    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            reset();                 // 先释放自己的资源
            ptr_ = other.ptr_;       // 拿走资源
            other.ptr_ = nullptr;    // 让 source 成为空指针
        }
        return *this;
    }

    // 访问被管理对象
    T& operator*() const noexcept { assert(ptr_); return *ptr_; }
    T* operator->() const noexcept { assert(ptr_); return ptr_; }
    T* get() const noexcept { return ptr_; }

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

    // 释放并置空
    void reset(T* new_ptr = nullptr) noexcept {
        if (ptr_) delete ptr_;
        ptr_ = new_ptr;
    }

    // 取回裸指针,转移所有权
    T* release() noexcept {
        T* temp = ptr_;
        ptr_ = nullptr;
        return temp;
    }

    // 用新指针替换旧指针
    void swap(MyUniquePtr& other) noexcept {
        std::swap(ptr_, other.ptr_);
    }

private:
    T* ptr_;
};

3. 使用示例

#include <iostream>
#include <string>

int main() {
    // 1. 简单使用
    MyUniquePtr <int> p1(new int(42));
    std::cout << *p1 << std::endl;          // 输出 42

    // 2. 移动构造
    MyUniquePtr <int> p2(std::move(p1));
    if (!p1) std::cout << "p1 is empty\n";

    // 3. 移动赋值
    MyUniquePtr<std::string> p3(new std::string("Hello"));
    MyUniquePtr<std::string> p4;
    p4 = std::move(p3);
    std::cout << *p4 << std::endl;           // 输出 Hello

    // 4. reset 与 release
    p4.reset();                               // 释放 string
    p4 = MyUniquePtr<std::string>(new std::string("C++"));
    std::cout << *p4 << std::endl;           // 输出 C++

    int* raw = p4.release();                  // 取回裸指针
    delete raw;                                // 手动 delete

    return 0;
}

4. 关键点回顾

  • RAII:构造时绑定资源,析构时释放,保证异常安全。
  • 所有权移动:移动构造/赋值通过 std::move 将资源转移,防止资源泄漏。
  • 禁用拷贝:复制会导致多重 delete,因而被删除。
  • swap:提供原子交换,方便实现容器等高级功能。
  • release:在特殊场景下需要把所有权交给外部管理时使用。

5. 延伸阅读

  • shared_ptr:多所有者、引用计数实现。可以在 MyUniquePtr 的基础上增加计数器实现。
  • make_unique:返回 `MyUniquePtr `,避免裸指针泄漏。可自行实现。
  • 自定义 deleter:支持对数组、文件句柄等非普通 delete 的资源进行正确释放。

通过上述实现,你可以在不依赖标准库的情况下,掌握智能指针的核心机制,进一步理解 C++ 内存管理的哲学。

发表评论