在现代 C++ 开发中,智能指针是管理资源生命周期的核心工具。虽然标准库提供了 std::shared_ptr、std::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. 关键细节解释
-
计数器存储方式
` 让递增/递减操作原子化。
计数器和指针放在同一个SharedPtr对象中是可行的,但如果想要多线程安全且共享计数器,最好把计数器单独存放,并通过原子操作来更新。这里使用 `std::atomic -
移动语义中的“所有权转移”
在移动构造和移动赋值时,源对象的指针和计数器被置为nullptr,从而避免再次释放资源。目标对象继续持有计数器,确保引用计数的正确性。 -
释放资源
release()函数在析构或赋值前被调用。它先递减计数器,如果计数器变为0,则删除对象和计数器本身。注意在多线程环境下,计数器递减后需要检查是否为0,且该检查必须是原子完成。 -
异常安全
由于使用了std::atomic,拷贝构造/赋值操作本身不会抛出异常。若想在构造函数中分配计数器时抛异常(如内存不足),需要在构造器内进行异常处理;此处为了简洁未做额外处理。 -
自定义 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 更为稳妥;但自定义实现可以为特殊需求(如自定义分配器、跟踪日志等)提供更灵活的控制。