在管理动态分配的内存时,一个最棘手的问题就是决定何时释放这些内存,而智能指针就是用来简化内存管理的编程方式。智能指针一般有独占和共享两种所有权模型。
------------------------------------------------------------------------------------------------------------
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() { } // 针对指针的构造函数:让该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 = ;
} // 与另一个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 = ; // 置空操作
} // 析构函数
~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 = ;
} Holder<T>& operator= (Trule<T> const& t) {
delete ptr;
ptr = t.ptr;
const_cast<Trule<T>&>(t).ptr = ;
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 = ;
} // 释放该计数器
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 == ;
}
};
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 = ;
} // 对于计数器的释放,并不需要显式执行任何操作
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 == ;
}
};
如果使用这种policy的话,那么在类的实现中,就可以很快地写出类的引用计数指针类型。其中类的设计框架大概如下:
class ManagedType
{
private:
size_t ref_count;
public:
typedef CountingPtr<ManagedType,
MemberReferenceCount
<ManagedType, // 包含计数器的对象类型
size_t, // 计数器类型
&ManagedType::ref_count> >
Ptr;
....
};
有了上面这个定义之后,我们就可以使用ManageeType::Ptr,方面地引用“那些用于访问ManagedType对象的”引用计数指针类型(在此为智能指针类型CountingPtr)。
书中还介绍了关于智能指针的其他一些功能实现,包括常数性相关内容、隐式转型,以及比较等等,有兴趣自行查阅学习,这里不介绍。