如何使用灵巧指针?
灵巧(smart)指针
灵巧指针是一种外观和行为都被设计成与内建指针相类似的对象,不过它能提供更多的功能。它们有许多应用的领域,包括资源管理(参见条款9、10、25和31)和重复代码任务的自动化(参见条款17和29)
当你使用灵巧指针替代C++的内建指针(也就是dumb pointer),你就能控制下面这些方面的指针的行为:
构造和析构。你可以决定建立灵巧指针时应该怎么做。通常赋给灵巧指针缺省值0,避免出现令人头疼的未初始化的指针。当指向某一对象的最后一个灵巧指针被释放时,一些灵巧指针负责删除它们指向的对象。这样做对防止资源泄漏很有帮助。
拷贝和赋值。你能对拷贝灵巧指针或设计灵巧指针的赋值操作进行控制。对于一些类型的灵巧指针来说,期望的行为是自动拷贝它们所指向的对象或用对这些对象进行赋值操作,也就是进行deep copy(深层拷贝)。对于其它的一些灵巧指针来说,仅仅拷贝指针本身或对指针进行赋值操作。还有一部分类型的灵巧指针根本就不允许这些操作。无论你认为应该如何去做,灵巧指针始终受你的控制。
Dereferencing(取出指针所指东西的内容)。当客户端引用被灵巧指针所指的对象,会发生什么事情呢?你可以自行决定。例如你可以用灵巧指针实现条款17提到的lazy fetching 方法。
灵巧指针从模板中生成,因为要与内建指针类似,必须是strongly typed(强类型)的;模板参数确定指向对象的类型。大多数灵巧指针模板看起来都象这样:
classSmartPtr{
public:
SmartPtr(T*realPtr=0);//建立一个灵巧指针
//指向dumbpointer所指的
//对象。未初始化的指针
//缺省值为0(null)
SmartPtr(constSmartPtr&rhs);//拷贝一个灵巧指针
~SmartPtr();//释放灵巧指针
//makeanassignmenttoasmartptr
SmartPtr&operator=(constSmartPtr&rhs);
T*operator->()const;//dereference一个灵巧指针
//以访问所指对象的成员
T&operator*()const;//dereference灵巧指针
private:
T*pointee;//灵巧指针所指的对象
};
拷贝构造函数和赋值操作符都被展现在这里。对于灵巧指针类来说,不能允许进行拷贝和赋值操作,它们应该被声明为private(参见Effective C++条款27)。两个dereference操作符被声明为const,是因为dereference一个指针时不能对指针进行修改(尽管可以修改指针所指的对象)。最后,每个指向T对象的灵巧指针包含一个指向T的dumb pointer。这个dumb pointer指向的对象才是灵巧指针指向的真正对象。
进入灵巧指针实作的细节之前,应该研究一下客户端如何使用灵巧指针。考虑一下,存在一个分布式系统(即其上的对象一些在本地,一些在远程)。相对于访问远程对象,访问本地对象通常总是又简单而且速度又快,因为远程访问需要远程过程调用(RPC),或其它一些联系远距离计算机的方法。
对于编写程序代码的客户端来说,采用不同的方法分别处理本地对象与远程对象是一件很烦人的事情。让所有的对象都位于一个地方会更方便。灵巧指针可以让程序库实现这样的梦想。
classDBPtr{//中对象的灵巧指针模板
public:
DBPtr(T*realPtr=0);//建立灵巧指针,指向
//由一个本地dumbpointer
//给出的DB对象
DBPtr(DataBaseIDid);//建立灵巧指针,
//指向一个DB对象,
//具有惟一的DB识别符
...//其它灵巧指针函数
};//同上
classTuple{//数据库元组类
public:
...
voiddisplayEditDialog();//显示一个图形对话框,
//允许用户编辑元组。
//usertoeditthetuple
boolisValid()const;//返回*this是否通过了
};//合法性验证
//这个类模板用于在修改T对象时进行日志登记。
//有关细节参见下面的叙述:
template<classT>
classLogEntry{
public:
LogEntry(constT&objectToBeModified);
~LogEntry();
};
voideditTuple(DBPtr<Tuple>&pt)
{
LogEntry<Tuple>entry(*pt);//为这个编辑操作登记日志
//有关细节参见下面的叙述
//重复显示编辑对话框,直到提供了合法的数值。
do{
pt->displayEditDialog();
}while(pt->isValid()==false);
}
在editTuple中被编辑的元组物理上可以位于本地也可以位于远程,但是编写editTuple的程序员不用关心这些事情。灵巧指针类隐藏了系统的这些方面。程序员只需关心通过对象进行访问的元组,而不用关心如何声明它们,其行为就像一个内建指针。
注意在editTuple中LogEntry对象的用法。一种更传统的设计是在调用displayEditDialog前开始日志记录,调用后结束日志记录。在这里使用的方法是让LogEntry的构造函数启动日志记录,析构函数结束日志记录。正如条款9所解释的,当面对异常时,让对象自己开始和结束日志记录比显示地调用函数可以使的程序更健壮。而且建立一个LogEntry对象比每次都调用开始记录和结束记录函数更容易。
正如你所看到的,使用灵巧指针与使用dumppointer没有很大的差别。这表明了封装是非常有效的。灵巧指针的客户端可以象使用dumbpointer一样使用灵巧指针。正如我们将看到的,有时这种替代会更透明化。
灵巧指针的构造、赋值和析构
灵巧指针的的析构通常很简单:找到指向的对象(一般由灵巧指针构造函数的参数给出),让灵巧指针的内部成员dumbpointer指向它。如果没有找到对象,把内部指针设为0或发出一个错误信号(可以是抛出一个异常)。
灵巧指针拷贝构造函数、赋值操作符函数和析构函数的实作由于所有权的问题所以有些复杂。如果一个灵巧指针拥有它指向的对象,当它被释放时必须负责删除这个对象。这里假设灵巧指针指向的的对象是动态分配的。这种假设在灵巧指针中是常见的(有关确定这种假设是真实的方法,参见条款27)。
看一下标准C++类库中auto_ptr模板。这如条款9所解释的,一个auto_ptr对象是一个指向堆对象的灵巧指针,直到auto_ptr被释放。auto_ptr的析构函数删除其指向的对象时,会发生什么事情呢?auto_ptr模板的实作如下:
template<classT>
classauto_ptr{
public:
auto_ptr(T*ptr=0):pointee(ptr){}
~auto_ptr(){deletepointee;}
...
private:
T*pointee;
};
假如auto_ptr拥有对象时,它可以正常运行。但是当auto_ptr被拷贝或被赋值时,会发生什么情况呢?
auto_ptr<TreeNode>ptn1(newTreeNode);
auto_ptr<TreeNode>ptn2=ptn1;//调用拷贝构造函数
//会发生什么情况?
auto_ptr<TreeNode>ptn3;
ptn3=ptn2;//调用operator=;
//会发生什么情况?
如果我们只拷贝内部的dumb pointer,会导致两个auto_ptr指向一个相同的对象。这是一个灾难,因为当释放quto_ptr时每个auto_ptr都会删除它们所指的对象。这意味着一个对象会被我们删除两次。这种两次删除的结果将是不可预测的(通常是灾难性的)。
另一种方法是通过调用new,建立一个所指对象的新拷贝。这确保了不会有许多指向同一个对象的auto_ptr,但是建立(以后还得释放)新对象会造成不可接受的性能损耗。并且我们不知道要建立什么类型的对象,因为auto_ptr<T>对象不用必须指向类型为T的对象,它也可以指向T的派生类型对象。虚拟构造函数(参见条款25)可能帮助我们解决这个问题,但是好象不能把它们用在auto_ptr这样的通用类中。
如果quto_ptr禁止拷贝和赋值,就可以消除这个问题,但是采用“当auto_ptr被拷贝和赋值时,对象所有权随之被传递”的方法,是一个更具灵活性的解决方案:
classauto_ptr{
public:
...
auto_ptr(auto_ptr<T>&rhs);//拷贝构造函数
auto_ptr<T>&//赋值
operator=(auto_ptr<T>&rhs);//操作符
...
};
template<classT>
auto_ptr<T>::auto_ptr(auto_ptr<T>&rhs)
{
pointee=rhs.pointee;//把*pointee的所有权
//传递到*this
rhs.pointee=0;//rhs不再拥有
}//任何东西
template<classT>
auto_ptr<T>&auto_ptr<T>::operator=(auto_ptr<T>&rhs)
{
if(this==&rhs)//如果这个对象自我赋值
return*this;//什么也不要做
deletepointee;//删除现在拥有的对象
pointee=rhs.pointee;//把*pointee的所有权
rhs.pointee=0;//从rhs传递到*this
return*this;
}
注意赋值操作符在接受新对象的所有权以前必须删除原来拥有的对象。如果不这样做,原来拥有的对象将永远不会被删除。记住,除了auto_ptr对象,没有人拥有auto_ptr指向的对象。
因为当调用auto_ptr的拷贝构造函数时,对象的所有权被传递出去,所以通过传值方式传递auto_ptr对象是一个很糟糕的方法。因为:
voidprintTreeNode(ostream&s,auto_ptr<TreeNode>p)
{s<<*p;}
intmain()
{
auto_ptr<TreeNode>ptn(newTreeNode);
...
printTreeNode(cout,ptn);//通过传值方式传递auto_ptr
...
}
当printTreeNode的参数p被初始化时(调用auto_ptr的拷贝构造函数),ptn指向对象的所有权被传递到给了p。当printTreeNode结束执行后,p离开了作用域,它的析构函数删除它指向的对象(就是原来ptr指向的对象)。然而ptr不再指向任何对象(它的dumb pointer是null),所以调用printTreeNode以后任何试图使用它的操作都将产生不可定义的行为。只有在你确实想把对象的所有权传递给一个临时的函数参数时,才能通过传值方式传递auto_ptr。这种情况很少见。
这不是说你不能把auto_ptr做为参数传递,这只意味着不能使用传值的方法。通过const引用传递(Pass-by-reference-to-const)的方法是这样的:
voidprintTreeNode(ostream&s,
constauto_ptr<TreeNode>&p)
{s<<*p;}
在函数里,p是一个引用,而不是一个对象,所以不会调用拷贝构造函数初始化p。当ptn被传递到上面这个printTreeNode时,它还保留着所指对象的所有权,调用printTreeNode以后还可以安全地使用ptn。从而通过const引用传递auto_ptr可以避免传值所产生的风险。(“引用传递”替代“传值”的其他原因参见Effective C++条款22)。
在拷贝和赋值中,把对象的所有权从一个灵巧指针传递到另一个中去,这种思想很有趣,而且你可能已经注意到拷贝构造函数和赋值操作符不同寻常的声明方法同样也很有趣。这些函数同上会带有const参数,但是上面这些函数则没有。实际上在拷贝和赋值中上述这些代码修改了这些参数。也就是说,如果auto_ptr对象被拷贝或做为赋值操作的数据源,就会修改auto_ptr对象!
是的,就是这样。C++是如此灵活能让你这样去做,真是太好了。如果语言要求拷贝构造函数和赋值操作符必须带有const参数,你必须去掉参数的const属性(参见Effective C++条款21)或用其他方法实现所有权的转移。准确地说:当拷贝一个对象或这个对象做为赋值的数据源,就会修改该对象。这可能有些不直观,但是它是简单的,直接的,在这种情况下也是准确的。
如果你发现研究这些auto_ptr成员函数很有趣,你可能希望看看完整的实作。在291页至294页上有(只原书页码),在那里你也能看到在标准C++库中auto_ptr模板有比这里所描述的更灵活的拷贝构造函数和赋值操作符。在标准C++库中,这些函数是成员函数模板,不只是成员函数。(在本条款的后面会讲述成员函数模板。也可以阅读Effective C++条款25)。
灵巧指针的析构函数通常是这样的:
本文地址:http://www.45fan.com/a/luyou/70045.html