所谓的智能指针可以这么理解,即将在C/C++中的built-in points封装成一个全新的对象(其实质就是个对象),但是经过类的封装之后能让client看起来,用起来就感觉像是build-in的指针一样!但是又包含了其他各式各样的用途,包括资源管理以及自动的重复写码工作。与智能指针相对,内建的指针也就是dumb(蠢的) pointers了。为了达到以上目标,智能指针允许你对它的各种行为的定义有很大的控制权。
- 构造和析构函数:以一个dumb pointer 作为参数实现构造函数,当没有实际指向物时默认为0;当需要销毁一个智能指针时,且是指向对象的最后一个smart pointer时,智能指针有义务去销毁其所指对象,调用其析构函数!
- 复制和赋值:可以选择是deep copy或是shallow copy,有的smart pointer根本不允许复制和赋值!这些权利在你手中。
- 解引用:和内建的指针一样支持解(引)用的功能,实现'*'和'&'操作符重载。
一般的智能指针如下(详见More Effective C++)
template<class T>
SmartPtr{
public:
SmartPtr(T* realptr = 0);
~SmartPtr();
SmartPtr& operator=(const SmartPtr& ptr);
T* operator*()const;
T& operator->()const;
private:
T* pointer;
};
智能指针的一个运用就是能够封装远程数据,就如隧道类似,屏蔽远程机器上数据的操作方式,让对远程数据的操作就像是在本地操作一样。
Smart Pointers 的构造、赋值和析构
智能指针实际上就是利用类内的一个dumb class指针指向其heap中的对象。通过Smart Pointer的行为操作和类的封装使其看起来如同dumb pointer一样,但是功能用处却大于dumb pointer。C++标准程序中的auto_ptr template 其实例化后的对象就是一个Smart Pointer。其模板如下:
template<class T>
class auto_ptr{
public:
auto_ptr(T* ptr = 0):pointee(ptr){}
~auto_ptr(){delete pointee;}
private:
T* pointee;
};
现在的一个问题是当Smart Pointers之间相互复制和赋值时会发生什么事情:当一个智能指针复制给另外一个智能指针时,若发生shallow copy则两个智能指针都拥有同样一个dumb pointer对象,当一个智能指针销毁时势必会调用其所指之物的析构函数将其销毁,这将意味着所指对象会被销毁两次,这种双杀的行为会造成难以预料的后果,而且灾难严重!另外一种就是deep copy,重新产生一个所指对象的副本。但是这样一来便会给程序的性能带来不小的冲击。更要命的是,当所指对象时带有继承结构的基类对象时,由于基类指针可以指向派生类的对象,我们并不知道到底应该复制哪个类对象。虽然这个问题可以运用之前的virtual constructor来解决(在基类中产生一个clone的虚函数,在派生类中实现各自的clone函数,在函数中真正调用该派生类的拷贝函数,基类只需调用虚函数clone返回复制完派生类对象的指针即可)。其实如果你禁止这一套的复制和赋值操作就可解决麻烦,但是还有没有更加具有弹性的方法呢?答案是:转移所指对象的拥有权!auto_ptr是这样干的:
template<class T> auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs){ pointee = rhs.pointee; rhs.pointee = 0; }
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs){
if(this == &rhs)
return *this;
delete pointee;
pointee = rhs.pointee;
rhs.pointee = 0;
return *this;
}
从上面可发现几个有意思的现象,第一,我们平常在复制和赋值member functions 中的参数总是 const -T 的reference,而上述的两个函数并没有const属性,只是因为拥有权的转移需要改变原来的智能指针所以不能为const!这种拥有权的转移其实类似于计算机网络中的令牌,拥有权从一个对象转换到另一个对象中。但是这样的拥有权的转移又导致了另外一个问题:如果涉及到函数中在传参还是返回一个对象时,都不可再使用pass-by-value,除了你必须付出不必要的constructor和deconstructor代价外,在复制中,对象的拥有权也转移了,实参中对所指对象的拥有权转移到了形参中,当函数返回时形参的销毁同时也会销毁所指对象,如果在函数外再调用实参的析构函数,则又将raw对象才销毁一次,结果又是难以预料的。所以还是pass-by-reference较好。
实现Dereferencing Operators(解引操作符)
很简单,具体参考dumb指针
template<class T>
T& auto_ptr<T>::operator*()const{
return *pointee;}
唯一的问题就是注意返回是T的reference,原因同样是不知道T的真正类型,可能导致派生类复制给基类中的切割问题!当Smart Pointer 别无所指是 解引用就是错误的一种行为,对于错误的处理方法,选择权在你。爱咋干就咋干。
template<class T>
T* auto_ptr<T>::operator->()const{
return pointee;}
一般而言返回的不是smart pointer 或者是 dumb pointer,,对于返回dumb pointer来说这样做并不是一个好的主意。对于许多用途而言,以上关于Smart Pointers的内容已经足够了,如果你想还想更深入的了解,请给位看官往下看。
测试Smart Pointers是否为NULL
想想对于dumb pointers我们通常是怎么判断为空的呢?基本可如下:
SmartPtr<Node> ptn; if(ptn)... if(ptn == NULL)... if(!ptn)...
我们可以一一来瞧瞧上面的各行:首先是第二行,似乎看起来是正确的,因为我们追求的目标正是让Smart Pointers看起来就像是dumb pointers一样,但是现在的情况还没有那么的明朗,你要知道我们的智能指针实质上还是一个类对象,并不是真正的一个"指针",这句话的用法无异于你将你任意定义的一个类对象放进if判断语句内,你觉得这样能判断是否为空吗?!然后我们来瞧瞧第三行,同上简单的一个对象能和NULL作比较吗?至于第四行也是如此,在没有相关的操作符或是隐式转换的情况下,上述测试都是不成立的。于是问题也接着来了,那到底解决呢?通过上述分析,解决之道似乎已经变得清晰:隐式转换和操作符重载。可以预见的隐式转换可如下:
SmartPtr<T>::operator void*();//直接返回其地址
但同时导致了另一个问题:
SmartPtr<Apple> pa; SmartPtr<Orange> pb; if(pa == pb)... //???
唉~~~真的一波未平一波又起,这个问题又使得我们尴尬起来:这种操作本身就不应该允许,连编译都不应该通过的,但是有了上述的隐式转化之后,这个bug就出现了!太烦人了。其实换个角度想一想,我们完全可以不返回void*类型的,返回一个bool类型直接在操作符函数内判断智能指针是否有所指是不是更加和明了呢?!所以我们不妨试试下面的方法:
bool SmartPtr<T>::operator !()const;//返回bool值
if(!ptn)...//这样写就没有问题了 但是其他两种方式还是不行的
对于比较两个不同类型的智能指针唯一的风险就是
if(!pa == !pb)... //但这种情况很少出现
iostream程序库不但允许隐式转换为void*,还提供一个operator!
将smart Pointers转换为Dumb Pointers
这个问题在之前的'->'就有涉及到,智能指针返回的无非是其本身或是dumb pointers,若返回其本身,那么对于接受dumb pointer参数的函数就应该是这样:
display(&(*ptn));//不得不说真的狠丑陋啊!
但是若直接返回dunb pointer 那么我们的智能指针就会变得完全没有必要性了,失去她的封装性,将dunb pointer直接曝露在了外边,着实是件危险的事情,违背了原先的设计意愿。如换个想法通过隐式转换将Smart Pointer转换为dumb pointer则 delete ptn;这样的操作又是不正确的,编译器会努力的将ptn转化为dumb pointer 这样做是件非常危险的事情,简单的原则就是:除非不得已,否则不要提供对dumb pointer的隐式转换操作符。
Smart Pointers 和"与继承有关的"类型转换
若有以下类“:
class MusicProduct{
public:
MusicProduct(const string& title);
virtual void play()const=0;
virtual void displaytitle()cosnt=0;
...
};
class Cassette :public MusicProduct{
public:
Cassette(const string& title);
virtual void play()const;
virtual void dispalytitle()const;
...
}
class CD :public MusicProduct{
public:
CD (const string& title);
virtual void play()const;
virtual void dispalytitle()const;
...
}
void diaplaANDplay(const MusicProduct* omo,int numtimes ){
for(int i =1;i<=num;i++){
omo->displaytitle();
omo->play();
}
}
似乎我们可以这样:
Cassette * f = new Cassette("Apapalooza");
CD * c = new CD("disco");
dispalyAndplay(f,10);
dispalyANDplay(c,10);
接着我们将dumb pointers以smart pointer取代:
void diaplaANDplay(const SmartPtr<MusicProduct>& omo,int numtimes ); SmartPtr<Cassette> f (new Cassette("Apapalooza")); SmartPtr<CD> c(new CD("disco")); dispalyAndplay(f,10);//*式 dispalyANDplay(c,10);
class SmartPtr<Cassette>{
public:
operator SmartPtr<MusicProduct>(){
return SmartPtr<MusicProduct>(pointee);
}
...
private:
Casstte* pointee;
};
class SmartPtr<CD>{
public:
operator SmartPtr<MusicProduct>(){
return SmartPtr<MusicProduct>(pointee);
}
...
CD* pointee;
};
利用MusicProduct Casstte和CD之间的继承关系就可以解决这一问题,可是若要是还有其他的类派生于MusicProduct,岂不是还要在写同一个以上的类转换,这违背了template的设计原则!C++语言的一个扩充性解决了这个问题:
template<class T>
class SmartPtr{
public:
SmartPtr(T* realptr = 0);
T* operator->();
T& operator*();
template<class newType>
operator SmartPtr<newType>(){
return SmartPtr<newType>(pointee);
}
...
};
是不是很神奇,这是我第一眼的感觉,它的前提是newType和T之间需要某种转换的关系。这样的话我们的问题也就解决了。让我们来观察一下上述*式是怎么调用的:1.f 的类型是SmartPtr<Cassette>,而diaplayANDplay()需要的是SmartPtr<MusicProduct>,于是编译器就会需找某种方法,企图将f转化为SmartPtr<MusicProduct>,编译器首先在SmartPtr<MusicProduct>内部企图寻找一个单一自变量的constructor,,且其自变量为SmartPtr<Cassette>,但是很遗憾没有找到,2 于是再接再厉在SmartPtr<Cassette>的内部寻找一个隐式的类型转换操作符,希望可以产生出一个smartPtr<MusicProduct>对象,但也失败了。3 接下来编译器再试图寻找一个"可实例化以导出合宜的转换函数"的member function template.这一次在SmartPtr<Cassette>的内部找到了这样的一个东西,它将被实例化并令newType绑定至MusicProduct时,产生了所需函数。