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

在 C++ 现代化进程中,智能指针(如 std::shared_ptr、std::unique_ptr)已经成为管理资源的重要工具。若想在自己的项目中使用自定义智能指针,核心任务之一就是正确实现拷贝和移动语义。下面以一个简易的 RefCountPtr 为例,演示完整的实现思路和关键细节。

1. 设计目标

  • 自动管理引用计数:多个 RefCountPtr 对同一对象时,共享同一计数。
  • 支持拷贝构造/赋值:拷贝后引用计数递增。
  • 支持移动构造/赋值:移动后原对象置空,计数不变。
  • 防止循环引用:通过弱引用 WeakRefCountPtr 解决。

2. 基础实现

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

template<typename T>
class RefCountPtr {
public:
    explicit RefCountPtr(T* ptr = nullptr)
        : ptr_(ptr), count_(ptr ? new std::atomic <size_t>(1) : nullptr) {}

    // 拷贝构造
    RefCountPtr(const RefCountPtr& other) noexcept
        : ptr_(other.ptr_), count_(other.count_) {
        inc();
    }

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

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

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

    ~RefCountPtr() {
        release();
    }

    T* operator->() const noexcept { return ptr_; }
    T& operator*() const noexcept { return *ptr_; }
    explicit operator bool() const noexcept { return ptr_; }
    size_t use_count() const noexcept { return count_ ? *count_ : 0; }

private:
    void inc() noexcept {
        if (count_) ++(*count_);
    }

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

    T* ptr_;
    std::atomic <size_t>* count_;
};

关键点说明

  • 原子计数:使用 `std::atomic ` 以保证多线程安全。
  • release():递减计数,若为零则销毁对象与计数器。
  • 移动操作:移动后将源对象的指针和计数器置空,避免重复释放。

3. 弱引用实现

循环引用时,RefCountPtr 无法自行断开。通过弱引用来解决:

template<typename T>
class WeakRefCountPtr {
public:
    explicit WeakRefCountPtr(const RefCountPtr <T>& src)
        : ptr_(src.ptr_), count_(src.count_) {}

    WeakRefCountPtr(const WeakRefCountPtr& other)
        : ptr_(other.ptr_), count_(other.count_) {}

    WeakRefCountPtr& operator=(const WeakRefCountPtr& other) {
        ptr_ = other.ptr_;
        count_ = other.count_;
        return *this;
    }

    // 尝试升级为强引用
    RefCountPtr <T> lock() const {
        if (count_ && *count_ > 0) {
            return RefCountPtr <T>(ptr_, count_);
        }
        return RefCountPtr <T>(); // 空指针
    }

private:
    T* ptr_;
    std::atomic <size_t>* count_;
};

4. 测试案例

struct Node {
    int value;
    Node(int v) : value(v) { std::cout << "Node(" << value << ") constructed\n"; }
    ~Node() { std::cout << "Node(" << value << ") destroyed\n"; }
};

int main() {
    RefCountPtr <Node> p1(new Node(10));
    {
        RefCountPtr <Node> p2 = p1; // 拷贝
        std::cout << "use_count: " << p1.use_count() << "\n";
    } // p2 结束,计数减一
    std::cout << "use_count after block: " << p1.use_count() << "\n";

    RefCountPtr <Node> p3 = std::move(p1); // 移动
    std::cout << "p1 is " << (p1 ? "valid" : "null") << "\n";
    std::cout << "p3.use_count: " << p3.use_count() << "\n";
}

运行结果示例:

Node(10) constructed
use_count: 2
use_count after block: 1
p1 is null
p3.use_count: 1
Node(10) destroyed

5. 常见坑与优化

  • 裸指针泄漏:在构造函数中一定要检查 ptr 是否为 nullptr
  • 多线程竞争:虽然使用 std::atomic,但仍需注意对 ptr_ 的读写是否存在数据竞争。
  • 性能开销:每次拷贝都需要原子递增,若对象频繁拷贝可考虑使用 std::shared_ptr
  • 循环引用:如上所述,弱引用是常见的解决方案;在设计类图时尽量避免。

6. 结语

自定义智能指针可以更好地符合项目需求,但实现时必须细心处理拷贝、移动和计数等细节。上述 RefCountPtr 是一个完整、线程安全且可直接使用的参考实现,能够帮助你在 C++ 项目中快速搭建资源管理层。祝编码愉快!

发表评论