现代C++学习: 自己动手,实现C++的智能指针

📅 2026/7/8 11:52:35
现代C++学习: 自己动手,实现C++的智能指针
引言上一讲我们描述了一个某种程度上可以当成智能指针用的类 shape_wrapper。使用那个智能指针可以简化资源的管理从根本上消除资源包括内存泄漏的可能性。这一讲我们就来进一步讲解如何将 shape_wrapper 改造成一个完整的智能指针。你会看到智能指针本质上并不神秘其实就是 RAII 资源管理功能的自然展现而已。在学完这一讲之后你应该会对 C 的 unique_ptr 和 shared_ptr 的功能非常熟悉了。同时如果你今后要创建类似的资源管理类也不会是一件难事。回顾我们上一讲给出了下面这个类class shape_wrapper { public: explicit shape_wrapper( shape* ptr nullptr) : ptr_(ptr) {} ~shape_wrapper() { delete ptr_; } shape* get() const { return ptr_; } private: shape* ptr_; };这个类可以完成智能指针的最基本的功能对超出作用域的对象进行释放。但它缺了点东西这个类只适用于 shape 类该类对象的行为不够像指针拷贝该类对象会引发程序行为异常下面我们来逐一看一下怎么弥补这些问题。模板化和易用性要让这个类能够包装任意类型的指针我们需要把它变成一个类模板。这实际上相当容易template typename T class smart_ptr { public: explicit smart_ptr(T* ptr nullptr) : ptr_(ptr) {} ~smart_ptr() { delete ptr_; } T* get() const { return ptr_; } private: T* ptr_; };和 shape_wrapper 比较一下我们就是在开头增加模板声明 template 然后把代码中的 shape 替换成模板参数 T 而已。这些修改非常简单自然吧模板本质上并不是一个很复杂的概念。这个模板使用也很简单把原来的 shape_wrapper 改成 smart_ptr 就行。目前这个 smart_ptr 的行为还是和指针有点差异的它不能用 * 运算符解引用它不能用 - 运算符指向对象成员它不能像指针一样用在布尔表达式里不过这些问题也相当容易解决加几个成员函数就可以template typename T class smart_ptr { public: … T operator*() const { return *ptr_; } T* operator-() const { return ptr_; } operator bool() const { return ptr_; } }拷贝构造和赋值拷贝构造和赋值我们暂且简称为拷贝这是个比较复杂的问题了。关键还不是实现问题而是我们该如何定义其行为。假设有下面的代码smart_ptrshape ptr1{create_shape(shape_type::circle)}; smart_ptrshape ptr2{ptr1};对于第二行究竟应当让编译时发生错误还是可以有一个更合理的行为我们来逐一检查一下各种可能性。最简单的情况显然是禁止拷贝。我们可以使用下面的代码template typename T class smart_ptr { … smart_ptr(const smart_ptr) delete; smart_ptr operator(const smart_ptr) delete; … };禁用这两个函数非常简单但却解决了一种可能出错的情况。否则smart_ptr ptr2{ptr1}; 在编译时不会出错但在运行时却会有未定义行为——由于会对同一内存释放两次通常情况下会导致程序崩溃。我们是不是可以考虑在拷贝智能指针时把对象拷贝一份不行通常人们不会这么用因为使用智能指针的目的就是要减少对象的拷贝啊。何况虽然我们的指针类型是 shape但实际指向的却应该是 circle 或 triangle 之类的对象。在 C 里没有像 Java 的 clone 方法这样的约定一般而言并没有通用的方法可以通过基类的指针来构造出一个子类的对象来。我们要么试试在拷贝时转移指针的所有权大致实现如下template typename T class smart_ptr { … smart_ptr(smart_ptr other) { ptr_ other.release(); } smart_ptr operator(smart_ptr rhs) { smart_ptr(rhs).swap(*this); return *this; } … T* release() { T* ptr ptr_; ptr_ nullptr; return ptr; } void swap(smart_ptr rhs) { using std::swap; swap(ptr_, rhs.ptr_); } … };在拷贝构造函数中通过调用 other 的 release 方法来释放它对指针的所有权。在赋值函数中则通过拷贝构造产生一个临时对象并调用 swap 来交换对指针的所有权。实现上是不复杂的。如果你学到的赋值函数还有一个类似于 if (this ! rhs) 的判断的话那种用法更啰嗦而且异常安全性不够好——如果在赋值过程中发生异常的话this 对象的内容可能已经被部分破坏了对象不再处于一个完整的状态。上面代码里的这种惯用法见参考资料 [1]则保证了强异常安全性赋值分为拷贝构造和交换两步异常只可能在第一步发生而第一步如果发生异常的话this 对象完全不受任何影响。无论拷贝构造成功与否结果只有赋值成功和赋值没有效果两种状态而不会发生因为赋值破坏了当前对象这种场景。如果你觉得这个实现还不错的话那恭喜你你达到了 C 委员会在 1998 年时的水平上面给出的语义本质上就是 C98 的 auto_ptr 的定义。如果你觉得这个实现很别扭的话也恭喜你因为 C 委员会也是这么觉得的auto_ptr 在 C17 时已经被正式从 C 标准里删除了。上面实现的最大问题是它的行为会让程序员非常容易犯错。一不小心把它传递给另外一个 smart_ptr你就不再拥有这个对象了……“移动”指针在下一讲我们将完整介绍一下移动语义。这一讲我们先简单看一下 smart_ptr 可以如何使用“移动”来改善其行为。我们需要对代码做两处小修改template typename T class smart_ptr { … smart_ptr(smart_ptr other) { ptr_ other.release(); } smart_ptr operator(smart_ptr rhs) { rhs.swap(*this); return *this; } … };看到修改的地方了吗我改了两个地方把拷贝构造函数中的参数类型 smart_ptr 改成了 smart_ptr现在它成了移动构造函数。把赋值函数中的参数类型 smart_ptr 改成了 smart_ptr在构造参数时直接生成新的智能指针从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝完全依赖于构造参数时走的是移动构造还是拷贝构造。根据 C 的规则如果我提供了移动构造函数而没有手动提供拷贝构造函数那后者自动被禁用记住C 里那些复杂的规则也是为方便编程而设立的。于是我们自然地得到了以下结果smart_ptrshape ptr1{create_shape(shape_type::circle)}; smart_ptrshape ptr2{ptr1}; // 编译出错 smart_ptrshape ptr3; ptr3 ptr1; // 编译出错 ptr3 std::move(ptr1); // OK可以 smart_ptrshape ptr4{std::move(ptr3)}; // OK可以这个就自然多了。这也是 C11 的 unique_ptr 的基本行为。子类指针向基类指针的转换哦我撒了一个小谎。不知道你注意到没有一个 circle* 是可以隐式转换成 shape* 的但上面的 smart_ptr 却无法自动转换成 smart_ptr。这个行为显然还是不够“自然”。不过只需要额外加一点模板代码就能实现这一行为。在我们目前给出的实现里只需要增加一个构造函数即可——这也算是我们让赋值函数利用构造函数的好处了。template typename U smart_ptr(smart_ptrU other) { ptr_ other.release(); }这样我们自然而然利用了指针的转换特性现在 smart_ptr 可以移动给 smart_ptr但不能移动给 smart_ptr。不正确的转换会在代码编译时直接报错。需要注意上面这个构造函数不被编译器看作移动构造函数因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话就需要把拷贝构造函数标记成 delete 了见“拷贝构造和赋值”一节。不过更通用的方式仍然是同时定义标准的拷贝 / 移动构造函数和所需的模板构造函数。下面的引用计数智能指针里我们就需要这么做。至于非隐式的转换因为本来就是要写特殊的转换函数的我们留到这一讲的最后再讨论。引用计数unique_ptr 算是一种较为安全的智能指针了。但是一个对象只能被单个 unique_ptr 所拥有这显然不能满足所有使用场合的需求。一种常见的情况是多个智能指针同时拥有一个对象当它们全部都失效时这个对象也同时会被删除。这也就是 shared_ptr 了。unique_ptr 和 shared_ptr 的主要区别如下图所示多个不同的 shared_ptr 不仅可以共享一个对象在共享同一对象时也需要同时共享同一个计数。当最后一个指向对象和共享计数的 shared_ptr 析构时它需要删除对象和共享计数。我们下面就来实现一下。我们先来写出共享计数的接口class shared_count { public: shared_count(); void add_count(); long reduce_count(); long get_count() const; };这个 shared_count 类除构造函数之外有三个方法一个增加计数一个减少计数一个获取计数。注意上面的接口增加计数不需要返回计数值但减少计数时需要返回计数值以供调用者判断是否它已经是最后一个指向共享计数的 shared_ptr 了。由于真正多线程安全的版本需要用到我们目前还没学到的知识我们目前先实现一个简单化的版本class shared_count { public: shared_count() : count_(1) {} void add_count() { count_; } long reduce_count() { return --count_; } long get_count() const { return count_; } private: long count_; };现在我们可以实现我们的引用计数智能指针了。首先是构造函数、析构函数和私有成员变量template typename T class smart_ptr { public: explicit smart_ptr(T* ptr nullptr) : ptr_(ptr) { if (ptr) { shared_count_ new shared_count(); } } ~smart_ptr() { if (ptr_ !shared_count_ -reduce_count()) { delete ptr_; delete shared_count_; } } private: T* ptr_; shared_count* shared_count_; };构造函数跟之前的主要不同点是会构造一个 shared_count 出来。析构函数在看到 ptr_ 非空时此时根据代码逻辑shared_count 也必然非空需要对引用数减一并在引用数降到零时彻底删除对象和共享计数。原理就是这样不复杂。当然我们还有些细节要处理。为了方便实现赋值及其他一些惯用法我们需要一个新的 swap 成员函数void swap(smart_ptr rhs) { using std::swap; swap(ptr_, rhs.ptr_); swap(shared_count_, rhs.shared_count_); }赋值函数可以跟前面一样保持不变但拷贝构造和移动构造函数是需要更新一下的smart_ptr(const smart_ptr other) { ptr_ other.ptr_; if (ptr_) { other.shared_count_ -add_count(); shared_count_ other.shared_count_; } } template typename U smart_ptr(const smart_ptrU other) { ptr_ other.ptr_; if (ptr_) { other.shared_count_ -add_count(); shared_count_ other.shared_count_; } } template typename U smart_ptr(smart_ptrU other) { ptr_ other.ptr_; if (ptr_) { shared_count_ other.shared_count_; other.ptr_ nullptr; } }除复制指针之外对于拷贝构造的情况我们需要在指针非空时把引用数加一并复制共享计数的指针。对于移动构造的情况我们不需要调整引用数直接把 other.ptr_ 置为空认为 other 不再指向该共享对象即可。不过上面的代码有个问题它不能正确编译。编译器会报错像fatal error: ‘ptr_’ is a private member of ‘smart_ptr’错误原因是模板的各个实例间并不天然就有 friend 关系因而不能互访私有成员 ptr_ 和 shared_count_。我们需要在 smart_ptr 的定义中显式声明template typename U friend class smart_ptr;此外我们之前的实现类似于单一所有权的 unique_ptr 中用 release 来手工释放所有权。在目前的引用计数实现中它就不太合适了应当删除。但我们要加一个对调试非常有用的函数返回引用计数值。定义如下long use_count() const { if (ptr_) { return shared_count_ -get_count(); } else { return 0; } }这就差不多是一个比较完整的引用计数智能指针的实现了。我们可以用下面的代码来验证一下它的功能正常class shape { public: virtual ~shape() {} }; class circle : public shape { public: ~circle() { puts(~circle()); } }; int main() { smart_ptrcircle ptr1(new circle()); printf(use count of ptr1 is %ld\n, ptr1.use_count()); smart_ptrshape ptr2; printf(use count of ptr2 was %ld\n, ptr2.use_count()); ptr2 ptr1; printf(use count of ptr2 is now %ld\n, ptr2.use_count()); if (ptr1) { puts(ptr1 is not empty); } }这段代码的运行结果是上面我们可以看到引用计数的变化以及最后对象被成功删除。指针类型转换对应于 C 里的不同的类型强制转换static_castreinterpret_castconst_castdynamic_cast智能指针需要实现类似的函数模板。实现本身并不复杂但为了实现这些转换我们需要添加构造函数允许在对智能指针内部的指针对象赋值时使用一个现有的智能指针的共享计数。如下所示template typename U smart_ptr(const smart_ptrU other, T* ptr) { ptr_ ptr; if (ptr_) { other.shared_count_ -add_count(); shared_count_ other.shared_count_; } }这样我们就可以实现转换所需的函数模板了。下面实现一个 dynamic_pointer_cast 来示例一下template typename T, typename U smart_ptrT dynamic_pointer_cast( const smart_ptrU other) { T* ptr dynamic_castT*(other.get()); return smart_ptrT(other, ptr); }在前面的验证代码后面我们可以加上smart_ptrcircle ptr3 dynamic_pointer_castcircle(ptr2); printf(use count of ptr3 is %ld\n, ptr3.use_count());编译会正常通过同时能在输出里看到下面的结果最后对象仍然能够被正确删除。这说明我们的实现是正确的。代码列表为了方便你参考下面我给出了一个完整的 smart_ptr 代码列表#include utility // std::swap class shared_count { public: shared_count() noexcept : count_(1) {} void add_count() noexcept { count_; } long reduce_count() noexcept { return --count_; } long get_count() const noexcept { return count_; } private: long count_; }; template typename T class smart_ptr { public: template typename U friend class smart_ptr; explicit smart_ptr(T* ptr nullptr) : ptr_(ptr) { if (ptr) { shared_count_ new shared_count(); } } ~smart_ptr() { if (ptr_ !shared_count_ -reduce_count()) { delete ptr_; delete shared_count_; } } smart_ptr(const smart_ptr other) { ptr_ other.ptr_; if (ptr_) { other.shared_count_ -add_count(); shared_count_ other.shared_count_; } } template typename U smart_ptr(const smart_ptrU other) noexcept { ptr_ other.ptr_; if (ptr_) { other.shared_count_-add_count(); shared_count_ other.shared_count_; } } template typename U smart_ptr(smart_ptrU other) noexcept { ptr_ other.ptr_; if (ptr_) { shared_count_ other.shared_count_; other.ptr_ nullptr; } } template typename U smart_ptr(const smart_ptrU other, T* ptr) noexcept { ptr_ ptr; if (ptr_) { other.shared_count_ -add_count(); shared_count_ other.shared_count_; } } smart_ptr operator(smart_ptr rhs) noexcept { rhs.swap(*this); return *this; } T* get() const noexcept { return ptr_; } long use_count() const noexcept { if (ptr_) { return shared_count_ -get_count(); } else { return 0; } } void swap(smart_ptr rhs) noexcept { using std::swap; swap(ptr_, rhs.ptr_); swap(shared_count_, rhs.shared_count_); } T operator*() const noexcept { return *ptr_; } T* operator-() const noexcept { return ptr_; } operator bool() const noexcept { return ptr_; } private: T* ptr_; shared_count* shared_count_; }; template typename T void swap(smart_ptrT lhs, smart_ptrT rhs) noexcept { lhs.swap(rhs); } template typename T, typename U smart_ptrT static_pointer_cast( const smart_ptrU other) noexcept { T* ptr static_castT*(other.get()); return smart_ptrT(other, ptr); } template typename T, typename U smart_ptrT reinterpret_pointer_cast( const smart_ptrU other) noexcept { T* ptr reinterpret_castT*(other.get()); return smart_ptrT(other, ptr); } template typename T, typename U smart_ptrT const_pointer_cast( const smart_ptrU other) noexcept { T* ptr const_castT*(other.get()); return smart_ptrT(other, ptr); } template typename T, typename U smart_ptrT dynamic_pointer_cast( const smart_ptrU other) noexcept { T* ptr dynamic_castT*(other.get()); return smart_ptrT(other, ptr); }如果你足够细心的话你会发现我在代码里加了不少 noexcept。这对这个智能指针在它的目标场景能正确使用是十分必要的。我们会在下面的几讲里回到这个话题。内容小结这一讲我们从 shape_wrapper 出发实现了一个基本完整的带引用计数的智能指针。现在你应当已经对智能指针有一个较为深入的理解了。