它本质上是一个对象,但是被设计成使用方法和外观都和内建指针相似。
智能指针是从模板中生成的,模板的参数确定指向的对象类型。大部分智能指针的模板看起来可能会是这样:
template<class T> class SmartPtr { public: SmartPtr(T* realPtr = 0); // 建立一个智能指针 // 指向dumb pointer所指的对象。未初始化的指针,缺省值为0(null) SmartPtr(const SmartPtr& rhs); // 拷贝一个智能指针 ~SmartPtr(); // 释放智能指针 // 赋值操作 SmartPtr& operator=(const SmartPtr& rhs); T* operator->() const; // 解引用(通过->)一个智能指针 // 以访问所指对象的成员 T& operator*() const; // 解引用(通过&)智能指针 private: T *pointer; // 智能指针所指的对象 };
1.智能指针的构造,赋值和析构
构造函数很简单,把一个智能指针指向的对象赋值给内部的私有成员pointer
,否则把内部指针设为0.
2.对于拷贝构造函数,赋值操作符和析构函数,由于存在对所指向的对象所有权问题所以实现起来比较复杂。对于一个智能指针,当它被释放时必须负责删除它所指向的对象。假设智能指针指向的对象是动态分配的,这种假设是比较常见的。
看一下标准C++库中的auto_ptr模板,它是一个指向堆对象的智能指针。在auto_ptr释放时,它的析构函数负责删除它所指向的对象。
template<class T> class auto_ptr { public: auto_ptr(T *ptr = 0): pointee(ptr) {} ~auto_ptr() { delete pointee; } ... private: T *pointee; };
假如auto_ptr拥有对象时,它可以正常运行。但是当auto_ptr被拷贝或被赋值时,会发生什么情况呢?
auto_ptr<TreeNode> ptn1(new TreeNode);
auto_ptr<TreeNode> ptn2 = ptn1; // 调用拷贝构造函数
//会发生什么情况?
auto_ptr<TreeNode> ptn3;
ptn3 = ptn2; // 调用 operator=;
// 会发生什么情况?
如果只简单拷贝内部指针的话,会导致两个auto_ptr指向一个相同的对象。当释放auto_ptr时会重复删除两次这个对象。这将导致灾难性的错误。
另一种方法是通过调用new,建立一个所指对象的新拷贝。这确保了不会有许多指向同一个对象的auto_ptr,但是建立(以后还得释放)新对象会造成不可接受的性能损耗。并且我们不知道要建立什么类型的对象,因为auto_ptr<T>对象不用必须指向类型为T的对象,它也可以指向T的派生类型对象。虚拟构造函数可能帮助我们解决这个问题,但是好象不能把它们用在auto_ptr这样的通用类中。
如果quto_ptr禁止拷贝和赋值,就可以消除这个问题,但是采用“当auto_ptr被拷贝和赋值时,对象所有权随之被传递”的方法,是一个更具灵活性的解决方案:
template<class T> class auto_ptr { public: ... auto_ptr(auto_ptr<T>& rhs); // 拷贝构造函数 auto_ptr<T>& // 赋值 operator=(auto_ptr<T>& rhs); // 操作符 ... }; template<class T> auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs) { pointee = rhs.pointee; // 把*pointee的所有权 // 传递到 *this rhs.pointee = 0; // rhs不再拥有 } // 任何东西 template<class T> auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) { if (this == &rhs) // 如果这个对象自我赋值 return *this; // 什么也不要做 delete pointee; // 删除现在拥有的对象 pointee = rhs.pointee; // 把*pointee的所有权 rhs.pointee = 0; // 从 rhs 传递到 *this return *this; }
赋值操作符在接受新对象的所有权之前必须删除原来拥有的对象。不然,它原来拥有的对象将永远不会删除,这将导致内存泄露。除了auto_ptr以外,没人可以拥有它指向的对象。
因为调用auto_ptr的拷贝构造函数后,它的所有权被传递出去,它本身就不可用了。那么在函数调用中,如果通过传值的方式传递一个auto_ptr对象,这将产生严重的后果。因为在函数调用结束后,这个对象将无法继续使用。
//将auto_ptr以传值的方式传递 void printTreeNode(ostream& s, auto_ptr<TreeNode> p) { s << *p; } int main() { auto_ptr<TreeNode> ptn(new TreeNode); ... printTreeNode(cout, ptn); //通过传值方式传递auto_ptr ... }
当形参p被初始化时,调用ptn的拷贝构造函数,它所指向对象的所有权传递给p。在函数调用结束后,p离开了作用域,它的析构函数删除它所指向的对象。但是这时ptn已经不再指向任何对象。所以在函数调用后对它的操作都将是未知的。可能会引发灾难性的后果。程序崩溃。
通过const引用传递参数可以避免这一问题
void printTreeNode(ostream& s, const auto_ptr<TreeNode>& p) { s << *p; } 智能指针的析构函数通常是这样的: template<class T> SmartPtr<T>::~SmartPtr() { if (*this owns *pointee) { delete pointee; } }
有时删除前不需要进行测试,例如在一个auto_ptr总是拥有它指向的对象时。而在另一些时候,测试会更为复杂:一个使用了引用计数智能指针必须在判断是否有权删除所指对象前调整引用计数值。当然还有一些智能指针象一般指针一样,当它们被删除时,对所指对象没有任何影响。
3.实现解引用操作符
让我们把注意力转向智能指针的核心部分,operator*和operator-> 函数。前者返回所指的对象。理论上,这很简单:
template<class T> T& SmartPtr<T>::operator*() const { perform "smart pointer" processing; return *pointee; }
这里返回类型必须是一个引用。如果返回对象,会导致灾难性后果,这个问题前面已经讨论过了。还有一个方面是,返回对象,你的智能指针将丧失对虚函数的支持。想一下,如果智能指针指向的类型不是T,而是T的派生对象(很明显这是可以的),那么当返回对象时,由于返回类型是T,会发生对象切割问题。派生类的部分会丢失。当你调用对象的虚函数时,将不会触发与原先所指向对象的动态类型相符的函数。
实现operator->:
template<class T> T* SmartPtr<T>::operator->() const { perform "smart pointer" processing; return pointee;//返回auto_ptr指向的原始指针 }
4.测试智能指针是否为NULL
到目前为止,我们还无法测试一个智能指针是否为空(实际上是判断它指向的对象是否为空)
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ... // error!
if (ptn) ... // error!
if (!ptn) ... // error!
虽然给智能指针加一个成员函数isNull来判断是否为空,是一件极其容易的事,但是,这将破坏它与一般指针使用上的相似性。因为我们无法想测试一般指针那样测试智能指针。必须通过调用isNUll。另一种做法是提供隐式转换操作符。一般应用于这种目的的类型转换时void*:
template<class T> class SmartPtr { public: ... operator void*(); // 如果智能指针为null, ... // 返回0, 否则返回 }; // 非0。 SmartPtr<TreeNode> ptn; ... if (ptn == 0) ... // 现在正确 if (ptn) ... // 也正确 if (!ptn) ... // 正确
象所有的类型转换函数一样,它有一个缺点:在一些情况下虽然大多数程序员希望它调用失败,但是函数确实能够成功地被调用(参见条款M5)。特别是它允许智能指针与完全不同的类型之间进行比较:
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if (pa == po) ... // 这能够被成功编译!
即使在SmartPtr<Apple> 和 SmartPtr<Orange>之间没有operator= 函数,也能够编译,因为智能指针被隐式地转换为void*指针,而对于内建指针类型有内建的比较函数。这种进行隐式类型转换的行为特性很危险。
在void*类型转换方面,也有一些变化。有些设计者采用到const void*的类型转换,还有一些采取转换到bool的方法。这些变化都没有消除混合类型比较的问题。
有一种两全之策可以提供合理的测试null值的语法形式,同时把不同类型的智能指针之间进行比较的可能性降到最低。这就是在智能指针类中重载operator!,当且仅当智能指针是一个空指针时,operator!返回true:
这仅仅是智能指针的原理和细节的一小部分
下面是一个具体的auto_ptr的实现实例
template<class T> class auto_ptr { public: explicit auto_ptr(T *p = 0); //explicit防止构造函数支持隐式转换 template<class U> // 拷贝构造函数成员模板 auto_ptr(auto_ptr<U>& rhs);//用另一个类型兼容的auto_ptr对象初始化一个新的auto_ptr对象 ~auto_ptr(); template<class U> // 赋值操作成员模板 auto_ptr<T>& operator=(auto_ptr<U>& rhs); // 用另一个类型兼容的auto_ptr对象给它赋值 T& operator*() const; T* operator->() const; T* get() const; // 返回包容指针的当前值 T* release(); // 放弃包容指针的所有权,并返回其当前值 void reset(T *p = 0); // 删除包容指针,获得指针p的所有权 private: T *pointee; template<class U> // 让所有的auto_ptr类 成为友元 friend class auto_ptr<U>; }; template<class T> inline auto_ptr<T>::auto_ptr(T *p) : pointee(p) {} template<class T> inline auto_ptr<T>::auto_ptr(auto_ptr<U>& rhs) : pointee(rhs.release()) {} template<class T> inline auto_ptr<T>::~auto_ptr() { delete pointee; } template<class T> template<class U> inline auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<U>& rhs) { if (this != &rhs) reset(rhs.release()); return *this; } template<class T> inline T& auto_ptr<T>::operator*() const { return *pointee; } template<class T> inline T* auto_ptr<T>::operator->() const { return pointee; } template<class T> inline T* auto_ptr<T>::get() const { return pointee; } template<class T> inline T* auto_ptr<T>::release() { T *oldPointee = pointee; pointee = 0; return oldPointee; } template<class T> inline void auto_ptr<T>::reset(T *p) { if (pointee != p) { delete pointee; pointee = p; } }