如果只用一句话概括我做完这个练习的感受:unique_ptr 是所有 C++ 智能指针里最值得手写一遍的类。 平时用标准库时,它看起来只是"析构自动 delete"的指针,但当你自己实现一个简化版,才会真正理解独占所有权、移动语义、noexcept 这些概念到底是怎么拧在一起的。这是我 Week 1 Day 2 的练习任务:不依赖标准库,手写一个最小可用的 UniquePtr,然后再把它放进真实的场景里跑一遍。这篇文章就把当天的设计过程、每一行代码背后的原因、以及实测的现象完整记录下来,适合正在学 C++ 移动语义或智能指针的同学参考。
1. 为什么要手写 unique_ptr:独占所有权的底层逻辑
1.1 独占所有权到底在防什么
在 C++11 之前,new 和 delete 的配对完全是程序员自己的责任。漏写一个 delete 就是内存泄漏,多写一个就是 double free。智能指针的核心思路是把"释放资源"这个动作绑定到析构函数上,利用栈上对象生命周期结束时必定析构的特性,让资源管理变成一件"不需要特意记得"的事情。这个思想就是 RAII。
在众多智能指针里,unique_ptr 最特别的地方在于"独占"两个字。独占所有权意味着:同一时刻,只能有一个 unique_ptr 对象拥有它底层的裸指针资源。当这个对象生命周期结束,资源被自动释放;当所有权发生转移,旧对象立刻失去对资源的控制权。
这里最核心的风险是复制。如果允许复制,两个智能指针就会同时指向同一块堆内存,第一个析构时把它 delete 掉,第二个析构时再 delete 一次。这个问题非常隐蔽,因为崩溃往往不出现在资源被释放的那一刻,而是在之后某个看似毫不相关的地方。所以 unique_ptr 从设计上就禁止复制,而且禁止的方式不是运行时检测,而是直接让编译器拦截——这是 C++ "零成本抽象"在资源管理上的经典体现:能编译期解决的问题,绝不拖到运行时。
1.2 为什么自己写一遍比看源码更有效
标准库里的 std::unique_ptr 完整实现有几百行,因为它要处理自定义删除器、数组特化、类型转换、noexcept 规范、空基类优化等大量工程细节。直接读源码,很容易被这些枝节淹没,反而看不清核心机制。
手写一个简化版,就是刻意绕过这些细节,只保留一个裸指针成员和若干关键操作。这样做的好处是:你会被迫回答一系列"为什么"的问题——为什么拷贝要被删除?为什么移动之后源对象必须置空?为什么移动构造函数要加 noexcept?这些问题在阅读源码时容易被忽略,但亲手实现一遍之后,它们会变成肌肉记忆。理解了独占所有权的实现机制,后面再看标准库的完整实现或工程中的各种用法,都会顺畅很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一版最小骨架:构造、析构与禁用拷贝
2.1 成员变量、构造函数与析构函数
第一版只需要一个模板参数和一个裸指针成员,代码如下:
cpp复制template <typename T>
class UniquePtr {
private:
T* ptr_;
public:
explicit UniquePtr(T* p = nullptr) : ptr_(p) {}
~UniquePtr() {
delete ptr_;
}
};
这里有两个值得停下来想清楚的地方。
第一,构造函数为什么加 explicit?因为默认构造函数给了默认参数 nullptr,不做修饰的话,编译器允许从裸指针隐式转换到 UniquePtr。比如 UniquePtr<int> p = new int(42); 这种写法会通过编译,但语义上有点危险:裸指针和智能指针同时存在,外部可能还握着那个原始指针,一旦双方都认为自己对资源拥有控制权,后面必然出问题。加上 explicit,就强制必须以 UniquePtr<int> p(new int(42)); 的形式创建对象,让所有权的归属从一开始就非常清晰。
第二,析构函数为什么可以直接 delete ptr_?因为 this 指针本身是线程安全的,而资源释放只发生一次。当 ptr_ 是 nullptr 时,delete nullptr 是安全操作,不会产生任何效果。这种"空指针安全"是后面很多操作的根基,比如移动后的源对象、默认构造的对象,都可以放心析构。
2.2 用 = delete 直接堵死拷贝
如果不对拷贝做任何处理,编译器会为这个类自动生成默认的拷贝构造函数和拷贝赋值运算符。默认拷贝行为就是把 ptr_ 的值复制一份。结果就是两个 UniquePtr 对象指向同一块内存,析构时 double free。
防止办法很简单直接:
cpp复制 UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
= delete 是 C++11 提供的关键能力,它明确告诉编译器:这个操作不存在,谁调用谁编译报错。注意,这里的拦截发生在编译期,不产生任何运行时开销。可以把这个设计理解成一个"所有权的红绿灯"——红灯亮着,任何想复制资源的意图都会在编译阶段被拦下来
