C++ template —— 智能指针(十二)
在管理动态分配的内存时,一个最棘手的问题就是决定何时释放这些内存,而智能指针就是用来简化内存管理的编程方式。智能指针一般有独占和共享两种所有权模型。
------------------------------------------------------------------------------------------------------------
20.1 holder和trule
本节将介绍两种智能指针类型:holder类型独占一个对象;而trule可以使对象的拥有者从一个holder传递给另一个holder。
20.1.1 安全处理异常
正常情况下,程序只有一个入口一个出口,异常的出现使程序多了其他出口,导致程序可能会提前终止,对异常的不当使用会导致许多问题,特别是内存泄漏问题。即便我们可以通过异常处理机制来解决这种问题,但我们会发现异常执行路径会影响程序正常的执行路径了,并且对象的释放操作不得不在两个不同的地方执行:一个在正常执行路径,一个在异常执行路径。
对于动态分配的内存,只要遵循“谁申请谁释放”的原则,一般都不会导致内存泄漏,但异常的出现令这种内存管理变得更加复杂,智能指针旨在解决这个问题。
同时,通常都应该避免使用会抛出异常的析构函数,因为当一个异常被抛出的时候,析构函数都是被自动调用的;而此时如果再抛出另一个异常,那么将会导致程序立即中止。
智能指针的优点在于:我们可以很方便的管理动态分配的内存(不再需要在析构函数中释放对象),同时,也避免了抛出异常而导致的资源泄漏。
20.1.2 holder
智能指针会在下面两种情况下释放所指向的对象:本身被释放,或者把另一个指针赋值给它。下面我们模拟实现一个智能指针:
// pointers/holder.hpp template<typename T> class Holder { private: T* ptr; // 引用它所持有的对象(前提是该对象存在) public: // 缺省构造函数:让该holder引用一个空对象 Holder() : ptr(0) { } // 针对指针的构造函数:让该holder引用该指针所指向的对象 // 这里使用explicit,禁止隐式转型(也即禁止了使用赋值语法来初始化Holder对象,如“holderObj = originObj”形式的赋值语法) // 但依然可以通过对象构造的形式来给对象初始化,如"Holder holderObj(originObj)",这里是显式转型。 explicit Holder (T* p) : ptr(p) {} // 析构函数:释放所引用的对象(前提是该对象存在) ~Holder() { delete ptr; } // 针对新指针的赋值运算符 Holder<T>& operator= (T* p){ delete ptr; ptr = p; return *this; } // 指针运算符 T& operator* () const { return *ptr; } T* operator-> () const { return ptr; } // 获取所引用的对象(前提是该对象存在) T* get() const { return ptr; } // 释放对所引用对象的所有权 void release() { ptr = 0; } // 与另一个holder交换所有权 void exchange_with(Holder<T>& h) { swap(ptr, h.ptr); } // 与其他的指针交换所有权 void exchange_twith(T*& p) { // 参数是什么语法?传入指针p的引用? swap(ptr, p); } private: // 不想外提供拷贝构造函数和拷贝赋值运算符 // 不允许一个Holder对象A赋值给另一个Holder对象B. Holder(Holder<T> const&); Holder<T>& operator= (Holder<T> const&); };
从语义上讲,该holder独占ptr所引用对象的所有权。而且,这个对象一定要用new操作来创建,因为在销毁holder所拥有对象的时候,需要用到delete。接下来,release()成员函数释放holder对其持有对象的所有权。另外,上面的普通赋值运算符也设计得比较巧妙,它会销毁和释放任何被拥有的对象,因为另一个对象会替代原先的对象被holder所拥有,而且赋值运算符也不会返回原先对象的一个holder或指针(而是返回新对象的一个holder)。最后,我们添加了两个exchange_with()成员函数,从而可以在不销毁原有对象的前提下,方便地替换该holder所拥有的对象。
所以,我们可以如下使用上面的Holder创建两个对象:
void do_two_things() { Holder<Something> first(new Something); firsh->perform(); Holder<Something> second(new Something); second->perform(); }
20.1.3 作为成员的holder
我们也可以在类中使用holder来避免资源泄漏。要注意的是,只有那些完成构造之后的对象,它的析构函数才会被调用。因此,如果在构造函数内部产生异常,那么只有那些构造函数已正常执行完毕的成员对象,它的析构函数才会被调用。
// pointers/refmem2.hpp #include "holder.hpp" class RefMembers { private: Holder<MemType> ptr1; // 所引用的成员 Holder<MemType> ptr2; public: // 缺省构造函数 // - 不可能出现资源泄漏 RefMembers() : ptr1(new MemType), ptr2(new MemType) { } // 拷贝构造函数 // - 不可能出现资源泄漏 RefMembers (RefMembers const& x) : ptr1(new MemType(*x.ptr1)), ptr2(new MemType(*x.ptr2)) { } // 赋值运算符 const RefMembers& operator= (RefMembers const& x){ *ptr1 = *x.ptr1; *ptr2 = *x.ptr2; return *this; } // 不需要析构函数 // (缺省的析构函数将会让ptr1和ptr2删除它们所引用的对象) ... };
要注意的是,我们在这里可以省略用户定义的析构函数,但一定要编写拷贝构造函数和赋值运算符
20.1.4 资源获取于初始化
Holder所用到的基本思想是一种称为“资源获取去初始化”或RAII的模式(RAII在博文xxxxx有相关讲解,可供参考)。
20.1.5 hodler局限
包括 20.1.6 和 20.1.7两小节,介绍了holder在参数传递,返回返回值处理时的不足之处,以及复制holder、跨函数调用来复制holder所会产生的问题(这部分内容在博文xxxx中有相关讲解,可供参考)。并引出下一节trule的内容。
20.1.8 trule
为了解决上一小节留下的问题,我们引进了一个专门用于传递holder的辅助类模板,并把它称为trule。在语言中,它是一个术语,来自于transfer capsule的缩写。下面是其定义:
// pointers/trule.hpp #ifndef TRULE_HPP #define TRULE_HPP template <typename T> class Holder; template <typename T> class Trule { private: T* ptr; // trule所引用的对象(如果有的话) public: // 构造函数,确保trule只能作为返回类型,用于将holder从被调用函数传递给调用函数 // 显式构造函数(会自动屏蔽默认无参构造函数),只能通过Holder构造Trule对象 Trule (Holder<T>& h){ ptr = h.get(); h.release(); } // 拷贝构造函数 // 这里,trule通常是作为那些想传递holders的函数的返回类型,也就是
说trule对象总是作为临时对象(rvalues,右值)出现;因此它们的类型也就只能是
常引用(reference-to-const)类型。 Trule (Trule<T> const& t){ ptr = t.ptr; // 由于Trule不能作为一份拷贝,也不能含有一份拷贝,如果我们希望实现类似于拷贝操作,
就必须移除原trule的所有权。我们是通过将被封装指针置为空来实现这种移除操作的。而最后
这个置空操作显然只能针对non-const对象,所以才有了这种把const强制转型为non-const的做法。 // 另外,由于原来的对象实际上并没有被定义为常类型,所以即使这样做有些别扭,但在这种情况下这种转型却能合法地实现。 // 因此,对于最后需要把一个holder转换为trule,并且将其返回的函数,如果要声明这类函数的
返回类型,我们就必须把它声明为trule<T>类型,而绝对不能声明为trule<T> const,
这点需特别注意。如下面例子中的函数load_something() const_cast<Trule<T>&>(t).ptr = 0; // 置空操作 } // 析构函数 ~Trule() { delete ptr; } private: // 对于trule的用法,除了作为传递holder对象的返回类型,我们要防止把它用于其他地方。
于是,一个接收non-const引用对象的拷贝构造函数和一个类似的拷贝赋值运算符,都被声
明为私用函数,防止外界直接调用。通过禁止将trule作为左值的方法,因为左值允许取
址和赋值操作,这种特性容易导致其用于其他地方而没有报错。 Trule(Trule<T>&); Trule<T>& operator= (Trule<T>&); // 禁止拷贝赋值 friend class Holder<T>; }; #endif // TRULE_HPP
还有一点需要注意的是,上面的代码并不完全是把一个holder完全转换为一个trule:如果是这样的话,holder就必须是一个可修改的左值。这也是我们为什么要使用一个单独的类型来实现trule,而不是将它的功能合并到holder类模板中的原因。
最后,对于上面实现的trule,只有被holder模板所辨识并且使用之后,才能算是完整的。如下:
// pointers/holder2.hpp template <typename T> class Holder { // 前面已经定义的成员 ... public: Holder(Trule<T> const& t){ ptr = t.ptr; const_cast<Trule<T>&>(t).ptr = 0; } Holder<T>& operator= (Trule<T> const& t) { delete ptr; ptr = t.ptr; const_cast<Trule<T>&>(t).ptr = 0; return *this; } };
为了充分演示对holder/trule作了哪些改善,我们可以重写load_something()例子,如下:
// pointers/truletest.cpp #include "holder2.hpp" #include "trule.hpp" class Something { }; void read_something(Something* x) { } // 返回类型为Trule<Something>,通过将Holder<Something>转换成返回类型(也即,通过trule传递返回值) Trule<Something> load_something() { Holder<Something> result(new Something); read_something(result.get()); return result; } int main() { // 接收load_something函数返回的Trule<Something>类型的值,并通过Holder内部接收Trule对象的构造函数初始化Holder对象ptr Holder<Something> ptr(load_something()); .... }
20.2 引用计数
设计一个引用计数的智能指针,基本思想是:对于每个被指向的对象,都保存一个计数,用于代表指向该对象的指针的个数,当计数值减少到0时,就删除此对象。
我们首先面对的问题是:计算器在什么地方?这里可以有两种方式,一种是把计算器放在对象中,但如果对象早期已经设计好,则无法再把计算器放入对象;另一种也是通常会使用的就是使用专用的(内存)分配器。
我们面对的第二个问题是:对象的析构和释放。我们有可能会需要使用非标准方式(比如C的free(),或者delete[]运算符释放对象数组)来释放对象,故而,我们还需要指定一种单独的对象(释放)policy。
对于大多数用CountingPtr计数的对象,我们可以使用下面这个简单的对象policy:
// pointers/stdobjpolicy.hpp class StandardObjectPolicy { public: template<typename T> void dispose(T* object){ delete object; } }; // pointers/stdarraypolicy.hpp class StandardArrayPolicy { public: template<typename T> void dispose(T* array){ delete[] array; } };
在考虑了上面两个问题之后,我们现在开始定义我们的CountingPtr模板:
// pointers/countingptr.hpp template <typename T, typename CounterPolicy = SimpleReferenceCount, // 计算器的policy typename ObjectPolicy = StandardObjectPolicy> // 对象(释放)policy class CountingPrt : private CounterPolicy, private ObjectPolicy { private: // typedef 两个简单的别名 typedef CountPolicy CP; typedef ObjectPolicy OP; T* object_pointer_to; // 所引用的对象 // 如果没有引用任何对象,则为NULL public: // 缺省构造函数(没有显式初始化,即没有加上explicit关键字) CountingPtr(){ this->object_pointed_to = NULL; } // 一个针对转型的构造函数(转型自一个内建的指针) explicit CountingPtr(T* p) { this->init(p); // 使用普通指针初始化 } // 拷贝构造函数 CountingPtr(CountingPtr<T, CP, OP> const& cp) : CP((CP const&)cp), // 拷贝policy OP((OP const&)cp){ this->attach(cp); // 拷贝指针,并增加计数值 } // 析构函数 ~CountingPtr(){ this->detach(); // 减少计数值,如果计数值为0,则释放该计数器 } // 针对内建指针的赋值运算符 CountingPtr<T, CP, OP>& operator= (T* p){ // 计数指针不能指向*p assert(p != this->object_pointed_to); this->detach(); // 减少计数值,如果计数值为0,则释放该计数器 this->init(p); // 用一个普通指针进行初始化 return *this; } // 拷贝赋值运算符(要考虑自己给自己赋值) CountingPtr<T, CP, OP>& operator= (CountingPtr<T, CP, OP> const& cp){ if(this->object_pointed_to != cp.object_pointed_to){ this->detach(); // 减少计数值,如果计数值为0,则释放该计数器 CP::operator=((CP const&)cp); // 对policy进行赋值 OP::operator=((OP const&)op); this->attach(cp); // 拷贝指针并增加计数值 } return *this; } // 使之成为智能指针的运算符 T* operator->() const { return this->object_pointed_to; } T& operator* () const { return *this->object_pointed_to; } // 以后在这里将可能会增加一些其他的接口 .... private: // 辅助函数 // - 用普通指针进行初始化(前提是普通指针存在) void init(T* p){ if (p != NULL) { CounterPolicy::init(p); } this->object_pointed_to = p; } // - 拷贝指针并且增加计数值(前提是指针存在) void attach(CountingPtr<T, CP, OP> const& cp){ this->object_pointed_to = cp.object_pointed_to; if (cp.object_pointed_to != NULL) { CounterPolicy::increment(cp.object_pointed_to); } } // - 减少计数值(如果计数值为0, 则释放计数器) void detach(){ if (this->object_pointed_to != NULL) { CounterPolicy::decrement(this->object_pointed_to); if (CounterPolicy::is_zero(this->object_pointed_to)) { // 如果有必要的话,释放计数器 CounterPolicy::dispose(this->object_pointed_to); // 使用object policy来释放所指向的对象 ObjectPolicy::dispose(this->object_pointed_to); } } } };
上面代码需要注意:
(1)在拷贝赋值操作中,要判断是否为自赋值;
(2)由于空指针并没有一个可关联的计数器,所以在减少计数值之前,必须先显式地检查空指针的情况;
(3)在前面的代码中,我们使用继承来包含两种policy。这样做确保了在policy类为空的情况下,并不需要占用存储空间(前提是我们的编译器实现了空基类优化);
20.2.5 一个简单的非侵入式计数器
从总体看来,我们已经完成了CountingPtr的设计,下面我们需要为计数policy编写代码。
于是,我们先来看一个针对计数器的policy,它并不把计数器存储于所指向对象的内部,也就是说,它是一种非侵入式的计数器policy(或者称为非插入式的计数器policy)。对于计数器而言,最主要的问题是如何分配存储空间。事实上,同一个计数器需要被多个CountingPtr所共享;因此,它的生命周期必须持续到最后一个智能指针被释放之后。通常而言,我们会使用一种特殊的分配器来完成这种任务,这种分配器专门用于分配大小固定的小对象。
// pointers/simplerefcount.hpp #include <stddef.h> // 用于size_t的定义 #include "allocator.hpp" class SimpleReferenceCount { private: size_t* counter; // 已经分配的计数器 public: SimpleReferenceCount(){ counter = NULL; } // 缺省的拷贝构造函数和拷贝赋值运算符都是允许的 // 因为它们只是拷贝这个共享的计数器 public: // 分配计数器,并把它的值初始为1 template <typename T> void init(T*) { Counter = alloc_counter(); *counter = 1; } // 释放该计数器 template <typename T> void dispose(T*) { dealloc_counter(counter); } // 计数值加1 template<typename T> void increment(T*){ ++*counter; } // 计数值减1 template<typename T> void decrement(T*){ --*counter; } // 检查计数值是否为0 template<typename T> bool is_zero(T*){ return *counter == 0; } };
20.2.6 一个简单的侵入式计数器模板
侵入式(或插入式)计数器policy就是将计数器放到被管理对象本身的类型中(或者可能存放到由被管理对象所控制的存储空间中)。显然,这种policy通常需要在设计对象类型的时候就加以考虑;因此这种方案很可能会专用于被管理对象的类型。
// pointers/memberrefcount.hpp template<typename ObjectT, // 包含计数器的类型 typename CountT, // 计数器的类型 CountT Object::*CountP> // 计数器的位置,需要在设计ObjectT对象的时候就考虑到计数器 class MemberReferenceCount { public: // 缺省构造函数和析构函数都是允许的 // 让计数器的值初始化为1 void init(ObjectT* object){ object->*CountP = 1; } // 对于计数器的释放,并不需要显式执行任何操作 void dispose(ObjectT*){ } // 计数器加1 void increment(ObjectT* object){ ++object->*CountP; } // 计数器减1 void increment(ObjectT* object){ --object->*CountP; } // 检查计数值是否为0 template<typename T> bool is_zero(ObjectT* object){ return object->*CounP == 0; } };
如果使用这种policy的话,那么在类的实现中,就可以很快地写出类的引用计数指针类型。其中类的设计框架大概如下:
class ManagedType { private: size_t ref_count; public: typedef CountingPtr<ManagedType, MemberReferenceCount <ManagedType, // 包含计数器的对象类型 size_t, // 计数器类型 &ManagedType::ref_count> > Ptr; .... };
有了上面这个定义之后,我们就可以使用ManageeType::Ptr,方面地引用“那些用于访问ManagedType对象的”引用计数指针类型(在此为智能指针类型CountingPtr)。
书中还介绍了关于智能指针的其他一些功能实现,包括常数性相关内容、隐式转型,以及比较等等,有兴趣自行查阅学习,这里不介绍。