RAII (Resource Acquisition Is Initialization, 资源获取即初始化) 是 C++ 特有的资源管理方式,依托栈和析构函数对所有资源(包括堆)进行管理。实际上就是利用一个类来实现一个来管理资源,将资源和类对象的生命周期进行绑定,这样就可以不用再手动释放资源。
先看下上一节给出的类:
enum class shape_type { circle, triangle, rectangle }; class shape{}; class circle : public shape{}; class triangle : public shape{}; class rectangle : public shape{}; shape *create_shape(shape_type type) { switch (type) { case shape_type::circle: return new circle(); case shape_type::rectangle: return new rectangle(); case shape_type::triangle: return new triangle(); default: return nullptr; } } /** * 可以完成智能指针的最基本的功能:利用 RAII 机制,对超出作用域的对象自动进行释放。但缺了: * 1. 只适用于 shape 类 * 2. 行为不像指针 * 3. 拷贝该类对象会引发程序异常 */ 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 auto_ptr{ public: // 构造函数 explicit auto_ptr(T* ptr = nullptr):ptr_(ptr){} // 析构函数 ~auto_ptr() { delete ptr_; } T* get() const { return ptr_; } // 重载运算符 * T& operator*() const { return *ptr_; } // 重载运算符 -> T* operator->() const { return ptr_; } // 重载 bool operator bool() const { return ptr_; } private: T* ptr_; };
对于拷贝构造函数和赋值,关键问题是如何定义其行为。假设有下面代码:
// C++11的语法,对象初始化可以统一用大括号 auto_ptr<shape> ptr1{ create_shape(shape_type::circle) }; auto_ptr<shape> ptr2{ptr1};
对于第二行,当前没有定义拷贝构造函数,会在编译时报错。
最简单的情况就是禁止拷贝和赋值,即
auto_ptr( const auto_ptr& ) = delete; auto_ptr& operator=(const auto_ptr&) = delete;
实际上这会解决一类错误:由于拷贝构造函数浅拷贝两个对象的指针指向的是同一内存,在析构时会对这片内存释放两次,从而导致错误。
我们可以考虑在拷贝时把对象拷贝一份,也就是深拷贝,但智能指针的目的就是要减少对象的拷贝。
可以尝试在拷贝时转移指针的所有权,大致如下:
template <typename T> class auto_ptr{ public: // 构造函数 explicit auto_ptr(T* ptr = nullptr):ptr_(ptr){} // 析构函数 ~auto_ptr() { delete ptr_; } T* get() const { return ptr_; } // 重载运算符 * T& operator*() const { return *ptr_; } // 重载运算符 -> T* operator->() const { return ptr_; } // 重载 bool operator bool() const { return ptr_; } // 拷贝构造函数 // 通过 other 对象调用 release 方法来释放 other 对象对指针的所有权,同时将所有权赋给新构造的对象。这类指针的设计就是只允许一个智能指针拥有资源 auto_ptr(auto_ptr& other){ _ptr = other.release(); } // 赋值分为拷贝构造和交换两步,一场只可能发生在第一步。如果第一步拷贝发生了异常,this 没有参与运算不会受到影响,无论拷贝构造成功与否,结果只有赋值成功和没有效果两种状态,不会发生因为赋值破坏当前对象 auto_ptr& operator=(auto_ptr &rhs) { // 先把 rhs 维护的指针交给 auto_ptr 临时对象 auto_ptr(rhs),然后将临时对象与 this 对象交换,临时对象拿到 this 之前维护的指针,它会随着临时对象的销毁而被 delete,而 rhs 指针在拷贝的过程中也失去了原本资源(release)。 auto_ptr(rhs).swap(*this); return *this; } T* release() { T* ptr = _ptr; _ptr = nullptr; return ptr; } void swap(auto_ptr& rhs) { std::swap(_ptr, rhs._ptr); } private: T* _ptr; };
赋值函数中还有一个类似于 if ( this!= &rhs )
的判断,这种判断异常安全性不够好,如果赋值过程中发生异常,this 对象的内容可能已经被破坏了。
上述代码本质上是 C++98 的 auto_ptr 的定义,auto_ptr 在 C++17 时已经被正式从 C++ 标准里删除了。
上面实现最大的问题是,它的行为会让程序员很容易犯错,一不小心把资源指针传递给了另外一个智能指针,原本指针就不再拥有这个对象了。
此外,上述代码是不支持容器操作的。
在 C++03 标准下,有如下一个 demo:
int main() { std::auto_ptr<int> iptr(new int(1)); std::vector<std::auto_ptr<int>> integer_vec; integer_vec.push_back(iptr); return 0; }
由于在C++03标准中还没有引入移动语义,只能以push_back函数向vector中添加元素。
实际上上述代码无法编译通过,会报如下错误信息:
/c++/9.0/ext/new_allocator.h:146:9: error: no matching function for call to ‘std::auto_ptr
::auto_ptr(const std::auto_ptr &)’
{ ::new((void *)__p) _Tp(__val); }
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
先给出错误原因:由于类std::auto_ptr 没有提供const std::auto_ptr
我们查看之前自己写的 auto_ptr 发现拷贝构造函数参数不是 const 修饰的,而实际上 std::auto_ptr 的设计者想使 auto_ptr 的复制构造函数具备移动构造函数的属性(如果不懂右值、移动等内容,可以看看右值引用的正确用法),这就是使得 auto_ptr 复制构造函数参数不能由 const 修饰,否则参数指向的资源无法移动到新创建的对象中。
std::auto_ptr 的核心代码如下:
template <typename _Tp> class auto_ptr { private: _Tp *_M_ptr; public: typedef _Tp element_type; explicit auto_ptr(element_type *__p = 0) throw() : _M_ptr(__p) { } auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) {} template <typename _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) throw() : _M_ptr(__a.release()) {} auto_ptr &operator=(auto_ptr& __a) throw() { reset(__a.release()); return *this; } ~auto_ptr() { delete _M_ptr; } element_type* release() throw() { element_type *__tmp = _M_ptr; _M_ptr = 0; return __tmp; } void reset(element_type *__p = 0) throw() { if (__p != _M_ptr) { delete _M_ptr; _M_ptr = __p; } } //... };
而在 push_back 函数中,输入参数 __x 是const std::auto_ptr&类型,能接受iptr:
template<typename _Tp, typename _Alloc> void std::vector<_Tp, _Alloc>::push_back(const value_type& __x);
在 push_back 函数内部会调用 _Alloc_traits::construct 函数来构造一个新的 std::auto_ptr 对象 obj,然后将这个 obj 放到 integer_vec 中
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
- 因为要构造obj,那么必要会调用std::auto_ptr的复制构造函数,且输入参数是__x;
- 但由于 __x 是 const std::auto_ptr& 类型,而 std::auto_ptr 的复制构造函数输入类型是 std::auto_ptr&,接受不了 __x 作为输入,因此会导致 construct 函数执行失败。出现上述的错误。
C++11 引入移动语义,提出了 std::unique_ptr,才真正地完成了 std::auto_ptr 的设计意图,而原本的 std::auto_ptr 也被标记为deprecated。
由于 std::unique_ptr 对象管理的资源,不可共享,只能在 std::unique_ptr 对象之间转移,因此类 std::unique_ptr 就禁止了复制构造函数、赋值表达式,仅实现了移动构造函数等。
对于上节自己实现的 auto_ptr,仅需要做点小修改就能实现 unique_ptr 基本功能:
template <typename T> class unique_ptr{ … template <typename U> unique_ptr(unique_ptr<U>&& other) { ptr_ = other.release(); } unique_ptr& operator=(unique_ptr rhs) { rhs.swap(*this); return *this; } … };
- 把拷贝构造函数中的参数类型 unique_ptr& 改成了 unique_ptr&&,现在它成了移动构造函数。
- 把赋值函数中的参数类型 unique_ptr& 改成了 unique_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。
- 现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。
根据 C++ 的规则,如果提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用(但建议手动禁用), 于是可以得到以下结果:
unique_ptr<shape> ptr1{create_shape(shape_type::circle)}; unique_ptr<shape> ptr2{ptr1}; // 编译出错 unique_ptr<shape> ptr3; ptr3 = ptr1; // 编译出错 ptr3 = std::move(ptr1); // OK unique_ptr<shape> ptr4{std::move(ptr3)}; // OK
同时多态中,一个 circle* 是可以隐式转换成 shape* 的,为了使 unique_ptr
需要注意的是,上面增加模板的构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 。更通用的方式仍然是同时定义标准的 拷贝/移动构造函数和所需的模板构造函数。
关于 unique_ptr 更进一步的介绍,可以看这篇文章 从auto_ptr到unique_ptr,是C++的成长
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(); // 减少计数,返回值供调用者判断是否是最后一个指向共享计数的 shared_ptr long get_count() const; };
真正多线程安全的版本需要用到其他知识,目前先实现一个简单化的版本:
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 shared_ptr{ public: explicit shared_ptr(T* ptr = nullptr) : ptr_(ptr) { if (ptr) shared_count_ = new shared_count(); } ~shared_ptr() { if (ptr_ && !shared_count_->reduce_count()) { delete ptr_; delete shared_count_; } } shared_ptr(const shared_ptr& other) { ptr_ = other.ptr_; if (ptr_) { other.shared_count_->add_count(); shared_count_ = other.shared_count_; } } template <typename U> shared_ptr(const shared_ptr<U>& other) { ptr_ = other.ptr_; if (ptr_) { other.shared_count_->add_count(); shared_count_ =other.shared_count_; } } template <typename U> shared_ptr(shared_ptr<U>&& other) { ptr_ = other.ptr_; if (ptr_) { shared_count_ =other.shared_count_; other.ptr_ = nullptr; } } void swap(shared_ptr& rhs) { std::swap(ptr_, rhs.ptr_); std::swap(shared_count_, rhs.shared_count_); } private: T* ptr_; shared_count* shared_count_; };
- 构造函数中会构造一个 shared_count 用于计数。
- 析构函数在看到 ptr_ 非空时,需要对引用数减一,并在引用数降到零时彻底删除对象和共享计数。
- 对于拷贝构造函数,需要将引用计数加一,对于移动构造函数,不需要调整引用计数,但要将 other.ptr_ 置空。
上述代码会报如下错误:
fatal error: ‘ptr_’ is a private member of ‘shared_ptr’
错误原因是模板的各个实例间并不天然就有 friend 关系,因此不能互访私有成员 ptr_ 和 shared_count_。需要显式声明友元模板:
template <typename T> friend class shared_ptr;
此外对于 unique_ptr/auto_ptr 中用 release 函数来手工释放所有权,在 shared_ptr 中不适用了,应当添加一个返回引用计数的函数:
long use_count() { 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() { shared_ptr<circle> ptr1(new circle()); printf("use count of ptr1 is %ld\n", ptr1.use_count()); shared_ptr<shape> 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"); } }
这段代码的运行结果是:
use count of ptr1 is 1
use count of ptr2 was 0
use count of ptr2 is now 2
ptr1 is not empty~circle()
对应于 C++ 里的不同的类型强制转换:
智能指针需要实现类似的函数模板。实现本身并不复杂,但为了实现这些转换,需要添加构造函数,使在对智能指针内部的指针对象赋值时,使用一个现有的智能指针的共享计数。如下所示:
template <typename U> smart_ptr(const smart_ptr<U>& 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_ptr<T> dynamic_pointer_cast(const smart_ptr<U>& other) { T* ptr = dynamic_cast<T*>(other.get()); return smart_ptr<T>(other, ptr); }
完整的 shared_ptr 代码:
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 shared_ptr { public: template <typename U> friend class shared_ptr; explicit shared_ptr(T* ptr = nullptr) : ptr_(ptr) { if (ptr) { shared_count_ = new shared_count(); } } ~shared_ptr() { if (ptr_ && !shared_count_->reduce_count()) { delete ptr_; delete shared_count_; } } shared_ptr(const shared_ptr& other) { ptr_ = other.ptr_; if (ptr_) { other.shared_count_->add_count(); shared_count_ = other.shared_count_; } } template <typename U> shared_ptr(const shared_ptr<U>& other) noexcept { ptr_ = other.ptr_; if (ptr_) { other.shared_count_->add_count(); shared_count_ = other.shared_count_; } } template <typename U> shared_ptr(shared_ptr<U>&& other) noexcept { ptr_ = other.ptr_; if (ptr_) { shared_count_ = other.shared_count_; other.ptr_ = nullptr; } } template <typename U> shared_ptr(const shared_ptr<U>& other, T* ptr) noexcept { ptr_ = ptr; if (ptr_) { other.shared_count_->add_count(); shared_count_ = other.shared_count_; } } shared_ptr& operator=(shared_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(shared_ptr& rhs) noexcept { std::swap(ptr_, rhs.ptr_); std::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(shared_ptr<T>& lhs, shared_ptr<T>& rhs) noexcept { lhs.swap(rhs); } template <typename T, typename U> shared_ptr<T> static_pointer_cast( const shared_ptr<U>& other) noexcept { T* ptr = static_cast<T*>(other.get()); return shared_ptr<T>(other, ptr); } template <typename T, typename U> shared_ptr<T> reinterpret_pointer_cast( const shared_ptr<U>& other) noexcept { T* ptr = reinterpret_cast<T*>(other.get()); return shared_ptr<T>(other, ptr); } template <typename T, typename U> shared_ptr<T> const_pointer_cast( const shared_ptr<U>& other) noexcept { T* ptr = const_cast<T*>(other.get()); return shared_ptr<T>(other, ptr); } template <typename T, typename U> shared_ptr<T> dynamic_pointer_cast( const shared_ptr<U>& other) noexcept { T* ptr = dynamic_cast<T*>(other.get()); return shared_ptr<T>(other, ptr); }