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

在现代 C++ 开发中,智能指针是管理动态内存的重要工具。除了标准库中的 std::unique_ptrstd::shared_ptr,我们有时也需要根据业务场景自定义一个智能指针。实现自定义智能指针时,拷贝和移动语义的设计尤为关键,它决定了指针在赋值、传参、返回等场景中的行为。本文将通过完整示例说明如何实现一个简单的引用计数型智能指针,并在此基础上实现拷贝构造、拷贝赋值、移动构造、移动赋值等语义。

1. 设计目标

  • 引用计数:类似 std::shared_ptr,多份指针共享同一资源,计数递增/递减。
  • 异常安全:在出现异常时不泄漏资源。
  • 移动语义:移动构造和移动赋值可以“转移”资源而不产生副本,提升性能。
  • 线程安全:引用计数使用原子操作,保证在多线程环境下正确。

2. 关键类结构

#include <atomic>
#include <utility>

template<typename T>
class RefCountPtr {
public:
    // 构造
    explicit RefCountPtr(T* ptr = nullptr);

    // 析构
    ~RefCountPtr();

    // 拷贝构造
    RefCountPtr(const RefCountPtr& other);

    // 拷贝赋值
    RefCountPtr& operator=(const RefCountPtr& other);

    // 移动构造
    RefCountPtr(RefCountPtr&& other) noexcept;

    // 移动赋值
    RefCountPtr& operator=(RefCountPtr&& other) noexcept;

    // 重载操作符
    T& operator*() const noexcept;
    T* operator->() const noexcept;
    T* get() const noexcept;

    // 计数查询
    size_t use_count() const noexcept;

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

2.1 构造与析构

  • 构造:创建指针时,ptr_ 赋值为传入指针,ref_count_ 设为新分配的 `atomic `,初始计数为 1。若传入 `nullptr`,则 `ref_count_` 为 `nullptr`。
  • 析构:若 ptr_ 非空,递减计数;计数变为 0 时,删除对象和计数器。

2.2 拷贝语义

拷贝构造/赋值将 ptr_ 复制过去,并递增引用计数。使用 atomic 确保线程安全。移动语义只转移指针和计数指针,然后将源置为 nullptr

2.3 异常安全

  • 构造时若分配计数器失败,抛异常前不影响原始指针。
  • 拷贝赋值采用“copy‑and‑swap”模式,先拷贝再交换,确保异常不破坏目标对象。

3. 代码实现

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

template<typename T>
class RefCountPtr {
public:
    explicit RefCountPtr(T* ptr = nullptr)
        : ptr_(ptr)
    {
        if (ptr_) {
            ref_count_ = new std::atomic <size_t>(1);
        } else {
            ref_count_ = nullptr;
        }
    }

    ~RefCountPtr() {
        release();
    }

    // 拷贝构造
    RefCountPtr(const RefCountPtr& other)
        : ptr_(other.ptr_), ref_count_(other.ref_count_)
    {
        increment();
    }

    // 拷贝赋值
    RefCountPtr& operator=(const RefCountPtr& other) {
        if (this != &other) {
            RefCountPtr temp(other);   // 先拷贝
            swap(temp);                // 再交换
        }
        return *this;
    }

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

    // 移动赋值
    RefCountPtr& operator=(RefCountPtr&& other) noexcept {
        if (this != &other) {
            release();                 // 先释放自身
            ptr_ = other.ptr_;
            ref_count_ = other.ref_count_;
            other.ptr_ = nullptr;
            other.ref_count_ = nullptr;
        }
        return *this;
    }

    T& operator*() const noexcept { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }
    T* get() const noexcept { return ptr_; }
    size_t use_count() const noexcept { return ref_count_ ? ref_count_->load() : 0; }

    void swap(RefCountPtr& other) noexcept {
        std::swap(ptr_, other.ptr_);
        std::swap(ref_count_, other.ref_count_);
    }

private:
    void increment() {
        if (ref_count_) {
            ref_count_->fetch_add(1, std::memory_order_relaxed);
        }
    }

    void release() {
        if (ref_count_) {
            if (ref_count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {
                delete ptr_;
                delete ref_count_;
            }
        }
    }

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

4. 使用示例

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

int main() {
    RefCountPtr <Demo> p1(new Demo(42));
    std::cout << "p1 use_count: " << p1.use_count() << '\n';

    {
        RefCountPtr <Demo> p2 = p1;               // 拷贝构造
        std::cout << "after copy, p1: " << p1.use_count() << ", p2: " << p2.use_count() << '\n';

        RefCountPtr <Demo> p3;
        p3 = std::move(p2);                      // 移动赋值
        std::cout << "after move, p3: " << p3.use_count() << ", p2: " << p2.use_count() << '\n';
    }   // p2,p3销毁

    std::cout << "after block, p1 use_count: " << p1.use_count() << '\n';
}

输出

Demo(42) constructed
p1 use_count: 1
after copy, p1: 2, p2: 2
after move, p3: 2, p2: 0
Demo(42) destroyed
after block, p1 use_count: 1

5. 小结

  • 引用计数 通过 `std::atomic ` 实现线程安全。
  • 拷贝构造/赋值 递增计数,采用 copy‑and‑swap 以实现异常安全。
  • 移动构造/赋值 只转移指针和计数指针,源置为空,性能优越。
  • 析构 在计数为 0 时释放资源,防止泄漏。

通过上述实现,你可以根据业务需求进一步扩展,例如实现弱引用(类似 std::weak_ptr)、自定义分配器或在引用计数器上加锁实现更高层次的同步。此自定义智能指针既兼顾了 C++ 标准库的安全特性,又保留了实现细节的灵活性。

发表评论