本节书摘来自华章出版社《深入理解Android》一书中的第3章,第3.2节,作者孟德国 王耀龙 周金利 黎欢,更多章节内容可以访问云栖社区“华章计算机”公众号查看
3.2 智能指针
智能指针(smart ptr)泛指一类原生指针的封装,它的行为非常类似于原生指针。通过它,C++程序可以自动化地实现资源管理(比如对象的自动析构)以及很多包裹指针的衍生操作,如内容拷贝、引用计数、autoLocker、lazy evaluation等。
smart ptr使用广泛,有众多的原型及功能,几乎在所有的大型工程中都可以看到它们的身影,本章由于篇幅及目标定位所限,不能一一列举,有兴趣的读者可以参考STL、boost、android、chromium-base等开源代码。
smart ptr可以实现多种行为,其经典应用是实现动态分配对象的内存自动回收。下面介绍smart ptr的工作原理。
在C++语言中,smart ptr对象作为栈上分配的(stack-allocated)自动变量(对象)存在,在代码执行上下文退出其作用域时被自动析构(自动析构由编译器插桩的析构函数调用以及ABI中规定的退栈操作联合实现)
smart ptr的析构函数中一般包含被引用对象的delete操作,从而间接实现了被引用对象的自动析构。
smart ptr自动回收资源这一功能在异常发生的情况下尤其有用,异常发生处之后的代码可能不会执行。对于某些不合理的设计,如果误将资源回收操作(如delete)放置在此范围内,可能因delete未被执行而引起内存泄露。对于使用smart ptr的场景,异常栈展开的过程会析构栈上分配的变量,当然也就包括栈上分配的smart ptr对象,使smart ptr引用的资源一样可以被正确回收。WTF 库中smart ptr的行为正是实现被引用对象的引用计数及内存的自动回收。
下面展示smart ptr的一般实现结构:
【→smart pointer示例】
template <typename T>
class SmartPtr{
public:
typedef T ValueType;
typedef ValueType *PtrType;
//构造析构函数
SmartPtr() :m_ptr(NULL) {}
SmartPtr(PtrType ptr) :m_ptr(ptr) {}
~SmartPtr() {if(m_ptr) delete m_ptr;}
//拷贝构造函数
SmartPtr(const SmartPtr<T>& o);
template<typename U> SmartPtr(const SmartPtr<U>& o);
//拷贝赋值运算符
template<typename U> SmartPtr& operator=(const SmartPtr<U>& o);
//指针运算
ValueType& operator*() const { return *m_ptr; }
PtrType operator->() const { return m_ptr; }
//逻辑运算符重载
bool operator!() const { return !m_ptr; }
//转成raw ptr
operator PtrType() { return m_ptr;}
private:
PtrType m_ptr;
};
创建smart ptr对象的格式如下:
SmartPtr(new ValueType());
由上述代码可知:
入口参数一般是堆上分配的对象,当然这本质上取决于析构函数的行为。
拷贝构造函数和拷贝赋值运算符的实现,涉及被引用对象的所有权传递行为,其实现可以是将被引用对象的所有权直接传递给新对象或者右值对象,也可以是增加被引用对象的引用计数。
定义取值(operator *)和指针(operator ->)运算符的目的是使smart ptr在行为上类似于原生指针。
定义operator!()运算符是因为C/C++代码存在对指针是否为空的简洁判断语句if(!ptr)。定义operator!()后,smart ptr可以应用在if(!SmatrPtr) 和if(!!smartPtr)这样的场景中。
operator PtrType()是裸指针转换操作,使得smart ptr在C++编译系统提供的一次类型转换的帮助下能够用在需要裸指针的地方,同时也可以使用裸指针的值来参与逻辑运算,比如if(SmartPtr)。
smart ptr的实现和使用时需要注意的细节,将会在3.2.1节和3.2.2节中进行描述。
依据复制和赋值时行为的不同,WTF提供了OwnPtr和RefPtr这两类smart ptr,本章将以Android 4.2的代码为蓝本来介绍它们的实现。
3.2.1 OwnPtr的实现及使用
先来看一下OwnPtr的主要定义。
【→OwnPtr.h】
template<typename T> class OwnPtr {
public:
//关键点(1)
typedef typename RemovePointer<T>::Type ValueType;
typedef ValueType* PtrType;
//关键点(2)
template<typename U> OwnPtr(const PassOwnPtr<U>& o);
//关键点(3)
OwnPtr(const OwnPtr<ValueType>&);
//关键点(4)
~OwnPtr() { deleteOwnedPtr(m_ptr); }
void clear();
PassOwnPtr<T> release();
PtrType leakPtr();
//关键点(5)
ValueType& operator*() const { return *m_ptr; }
//关键点(6)
PtrType operator->() const { return m_ptr; }
// This conversion operator allows implicit conversion to bool but not to other integer types.
typedef PtrType OwnPtr::*UnspecifiedBoolType;
//关键点(7)
OwnPtr& operator=(const PassOwnPtr<T>&);
//关键点(8)
template<typename U> OwnPtr& operator=(const PassOwnPtr<U>&);
//关键点(9)
operator UnspecifiedBoolType() const { return m_ptr ? &OwnPtr::m_ptr : 0; }
#ifdef LOOSE_OWN_PTR
//关键点(10)
explicit OwnPtr(PtrType ptr) : m_ptr(ptr) { }
#endif
private:
PtrType m_ptr;
};
上面摘取的是OwnPtr的主要实现部分,下面来分析其实现和使用的要点。
关键点(1):与前面给出的SmartPtr主要的不同在于引入了RemovePointer类,本质上RemovePointer就是一种type_traits,用于萃取Type属性。这种type traits行为在STL中迭代器(iterator)的实现中经常使用,读者应该不会陌生。
关键点(2)(3):统称拷贝构造函数,但关键点(3)是严格意义上的定义,关键点(2)算作类型转换构造函数,应用关键点(2)一般要求U和T可以相互转换。在分析PassOwnPtr之前,可以暂时认为PassOwnPtr和OwnPtr完全等同(WTF设计PassOwnPtr主要作为OwnPtr的传递形态,也就是辅助完成复制及赋值)。对于关键点(3),细心的读者会发现OwnPtr的实现中只提供了拷贝构造函数的定义,并未提供拷贝构造函数的实现,所以拷贝构造函数永远不应该被调用,否则必然出现链接错误,也就是说OwnPtr是不允许拷贝构造的。
【→OwnPtr.h】
template<typename T>
template<typename U>
inline OwnPtr<T>::OwnPtr(const PassOwnPtr<U>& o) : m_ptr(o.leakPtr() {}
再看一下leakPtr()的实现:
【→PassOwnPtr.h】
template<typename T>
inline typename PassOwnPtr<T>::PtrType PassOwnPtr<T>::leakPtr() const
{
PtrType ptr = m_ptr;
m_ptr = 0;
return ptr;
}
从上述leakPtr的实现可以看出,通过这个函数PassOwnPtr会放弃对象的所有权,并将被引用对象的指针返回。
关键点(7)(8):在具体分析关键点(7)(8)前,我们先分析一下OwnPtr的拷贝赋值运算符,其实OwnPtr类没有提供拷贝赋值运算符的声明和实现,所以它采用默认的拷贝赋值运算,故其行为就是bitwise copy,这种浅拷贝是非常危险的,使用不当极易引起被引用对象的重复删除,从而触发异常。其实在最新的WTF代码中,OwnPtr的拷贝赋值运算符在编译器不支持右值引用的情况下,被声明为private,即不允许使用:
Private:
//最新webkit代码中OwnPtr拷贝赋值运算符的声明形式
#if !COMPILER_SUPPORTS(CXX_RVALUE_REFERENCES)
OwnPtr& operator=(const OwnPtr<T>& o);
#endif
Android 4.2中OwnPtr的实现还是比较危险的,希望读者在使用时注意。
【→OwnPtr.h】
template<typename T> inline OwnPtr<T>& OwnPtr<T>::operator=(const PassOwnPtr<T>& o)
{
PtrType ptr = m_ptr;
m_ptr = o.leakPtr();
deleteOwnedPtr(ptr);
return *this;
}
在这里,我们看到了OwnPtr的一个非常重要的特征—对象所有权的传递,源PassOwnPtr的m_ptr被赋值0,新OwnPtr的m_ptr指向了源对象。OwnPtr借助PassownPtr实现对象所有权的传递。OwnPtr的这一特征使得将OwnPtr作为函数参数和返回值时,一般要传递引用,有意要将对象所有权转交到函数中或者函数外的情况除外。从上述代码中我们也可以看到拷贝赋值运算符的返回值是OwnPtr&。
关键点(9):一个比较考究的设计。在前面的【→smart pointer示例】中笔者展示了返回raw ptr的类型转换操作,返回raw ptr的设计可以应用于更多的场合,但是若使用者对被引用对象的生存期把握不精准,可能引起空指针问题。相比而言,关键点(9)返回指向类内部成员的指针,因内部增加了对m_ptr的判断而更加安全。
WTF为了可以在函数的参数和返回值中使用OwnPtr,而设计了PassOwnPtr。PassOwnPtr的实现和行为本质上跟OwnPtr一致,可以理解为OwnPtr的传递形态,读者可自行分析。
3.2.2 RefPtr的实现及使用
RefPtr和OwnPtr的不同之处在于RefPtr要操作对象的引用计数,这就限制了RefPtr可以引用的对象的范围—包含ref()和deref()方法的类的对象才可以由RefPtr引用。
那么每一个将被RefPtr引用的类都要手工的重复的去添加ref()和deref()函数吗?答案是没有必要,因为WTF提供了RefCounted类模板。以下代码列出了RefCounted类的定义:
【→RefCounted.h】
class RefCountedBase {
public:
void ref() { ++m_refCount; }
protected:
RefCountedBase() : m_refCount(1) { }
~RefCountedBase() { }
bool derefBase()
{
if (m_refCount == 1) {
return true;
}
--m_refCount;
return false;
}
private:
int m_refCount;
};
template<typename T>
class RefCounted : public RefCountedBase {
public:
void deref()
{
if (derefBase())
//关键点(1)
delete static_cast<T*>(this);
}
protected:
RefCounted() { }
~RefCounted() { }
};
RefCounted类的定义分成两部分:RefCountedBase类和RefCounted类,其目的是使模板类RefCounted中的代码尽可能少,因为RefCounted类模板会被大量继承和使用,RefCounted类尽可能少的代码可以避免模板类大量实例化而引起代码过分膨胀。有了RefCounted模板,那么任何一个类CLASS想要被RefPtr引用,只需要继承自RefCounted。关键点(1)处代码显式将this指针转成其子类对象指针,目的是:
删除子类对象,因为RefCounted模板的析构函数没有声明为virtual函数,因此显式转化为子类指针是必需的。
有了前面对SmartPtr和OwnPtr的分析,对于RefPtr只需分析一下拷贝赋值运算符。
【→RefPtr.h】
template<typename T> inline RefPtr<T>& RefPtr<T>::operator=(T* optr)
{
refIfNotNull(optr);
T* ptr = m_ptr;
m_ptr = optr;
derefIfNotNull(ptr);
return *this;
}
该段代码的核心在refIfNotNull和derefIfNotNull,也就是增加和减少引用计数,其实现比较简单,读者可自行分析。
3.2.3 线程安全性
本节讨论一下被smart ptr索引的对象的线程安全性问题。
OwnPtr索引的对象显然是线程不安全的,而RefPtr索引的对象若继承自RefCounted,则引用计数连同整个被索引的对象都不是线程安全的。对于继承自ThreadSafeRefConunted (ThreadSafeRefCounted.h)的类的对象,它只保证引用计数的线程安全性,并不保证对RefPtr所引用的整个对象读写的线程安全性。RefPtr和OwnPtr一样是线程不安全的,涉及多线程写访问它们索引对象的情况要注意加锁。
细心的读者可能还注意到CrossThreadRefConted(CrossThreadRefConted.h)类,该类在Android 4.2版的WebKit中使用非常少,虽然它也有ref和deref函数,但它却不能也不应该被当作基类来继承,因为其构造和析构函数是private的,并且其deref函数首先调用了delete this,然后其析构函数又调用了delete m_data,这种语义只能用来直接索引某个对象,也就是—template RefPtr >。