如何在C++中实现自定义智能指针的复制与移动语义

在现代 C++ 开发中,智能指针是管理资源生命周期的核心工具。虽然标准库提供了 std::shared_ptrstd::unique_ptr 等常用实现,但在某些特定场景下,我们需要自行编写一个符合业务需求的智能指针。例如,想要在复制时记录引用计数、在移动时保持指针状态等。下面以一个简化版的 SharedPtr 为例,展示如何实现复制和移动语义,并给出常见的使用细节与注意点。

1. 设计思路

  • 引用计数:使用一个外部共享的计数器(如 std::shared_ptr 内部实现那样)来记录对象的引用次数。
  • 复制语义:拷贝构造和拷贝赋值时,计数器递增,指针指向同一对象。
  • 移动语义:移动构造和移动赋值时,将指针所有权转移到目标对象,源对象置为空,计数器保持不变。
  • 线程安全:计数器使用 `std::atomic `,确保多线程环境下安全。

2. 代码实现

#include <atomic>
#include <iostream>
#include <utility>

template <typename T>
class SharedPtr {
private:
    T* ptr_;
    std::atomic <size_t>* count_;

    void release() {
        if (count_ && --(*count_) == 0) {
            delete ptr_;
            delete count_;
        }
    }

public:
    // 默认构造
    SharedPtr() noexcept : ptr_(nullptr), count_(nullptr) {}

    // 从裸指针构造
    explicit SharedPtr(T* ptr) : ptr_(ptr), count_(new std::atomic <size_t>(1)) {}

    // 拷贝构造
    SharedPtr(const SharedPtr& other) noexcept
        : ptr_(other.ptr_), count_(other.count_) {
        if (count_) ++(*count_);
    }

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

    // 拷贝赋值
    SharedPtr& operator=(const SharedPtr& other) noexcept {
        if (this != &other) {
            release();
            ptr_ = other.ptr_;
            count_ = other.count_;
            if (count_) ++(*count_);
        }
        return *this;
    }

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

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

    // 获取引用计数
    size_t use_count() const noexcept { return count_ ? *count_ : 0; }

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

    // 取裸指针
    T* get() const noexcept { return ptr_; }

    // 重置
    void reset(T* ptr = nullptr) noexcept {
        release();
        if (ptr) {
            ptr_ = ptr;
            count_ = new std::atomic <size_t>(1);
        } else {
            ptr_ = nullptr;
            count_ = nullptr;
        }
    }

    ~SharedPtr() noexcept { release(); }
};

3. 关键细节解释

  1. 计数器存储方式
    计数器和指针放在同一个 SharedPtr 对象中是可行的,但如果想要多线程安全且共享计数器,最好把计数器单独存放,并通过原子操作来更新。这里使用 `std::atomic

    ` 让递增/递减操作原子化。
  2. 移动语义中的“所有权转移”
    在移动构造和移动赋值时,源对象的指针和计数器被置为 nullptr,从而避免再次释放资源。目标对象继续持有计数器,确保引用计数的正确性。

  3. 释放资源
    release() 函数在析构或赋值前被调用。它先递减计数器,如果计数器变为0,则删除对象和计数器本身。注意在多线程环境下,计数器递减后需要检查是否为0,且该检查必须是原子完成。

  4. 异常安全
    由于使用了 std::atomic,拷贝构造/赋值操作本身不会抛出异常。若想在构造函数中分配计数器时抛异常(如内存不足),需要在构造器内进行异常处理;此处为了简洁未做额外处理。

  5. 自定义 deleter
    若需要自定义删除策略(如使用自定义内存池或文件句柄等),可以在 SharedPtr 中加入一个 std::function<void(T*)> deleter 成员,并在 release() 时调用。

4. 使用示例

int main() {
    SharedPtr <int> p1(new int(42));
    std::cout << "p1 use_count: " << p1.use_count() << "\n"; // 1

    SharedPtr <int> p2 = p1;   // 拷贝
    std::cout << "after copy, p1: " << p1.use_count() << ", p2: " << p2.use_count() << "\n"; // 2, 2

    SharedPtr <int> p3 = std::move(p2); // 移动
    std::cout << "after move, p2: " << (p2 ? "valid" : "null") << ", p3: " << p3.use_count() << "\n"; // null, 2

    p1.reset(new int(100));
    std::cout << "p1 new value: " << *p1 << ", use_count: " << p1.use_count() << "\n"; // 1

    return 0;
}

5. 小结

  • 复制时共享计数器,计数递增;
  • 移动时转移指针与计数器,源对象置空;
  • 通过 std::atomic 保证线程安全;
  • release() 负责资源释放与计数递减。

在实际项目中,若业务需求不复杂,直接使用 std::shared_ptr 更为稳妥;但自定义实现可以为特殊需求(如自定义分配器、跟踪日志等)提供更灵活的控制。

发表评论