开发者 : 编程
很久以前,在非常黑暗的过去,Nigel Thompson写了一系列关于被称为"OLE傻瓜书"的OLE编程的技术笔记。当时走廊的上上下下都能听到他痛苦地叫喊着忘记了要正确地添加或释放一个接口。我想本应该有一些方法在使用C++中巧妙的指针时能自动释放组件对象模型(COM)元件,使得组件对象模型元件的使用更为容易一些。不过,我开始研究的却是Microsoft 基础类库(MFC),在这个课题中引用计算并不是一个重点,因为它隐藏在MFC类之中。
在写完文章"MFC/COM对象8:重访无MFC的多重继承"之后,我决定再考虑使用巧妙的指针类简化使用组件对象模型(COM)接口。研究的结果并没有实现我的愿望,我开始怀疑是否能在我自己的代码中使用巧妙的指针接口。不过,你的组件对象模型(COM)项目可能与我的不一样,所以我决定不用组件对象模型(COM)元件也许并不会影响你。
在这篇文章中,我将讨论下列主题:
- 创建一个灵巧的接口指针类的原因
- 如何创建一个巧妙的接口指针类
- 使用巧妙的接口指针类
- 我不喜欢巧妙的接口指针类的原因
在这篇文章的源代码中,我使用前缀PI指示一个指向接口的指针,例如:
IPersist* pIPersist ; I use the prefix SI to refer to a smart interface pointer: CSmartInterfaceSIPersist ;
创建一个巧妙的接口指针类的原因
我想要一个巧妙的接口指针类的原因是要自动地添加和释放接口指针。
使用组件对象模型(COM)接口指针时,你必须要遵循几条规则。首先,绝不要在一个接口指针上调用删除(delete)来代替Release。下面的代码是不正确的:
IDraw* pIDraw ; CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ; pIDraw->Draw(0,0,100,100) ; delete pIDraw ; // Don't delete an interface pointer. |
下面的代码是正确的:
IDraw* pIDraw ; CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ; pIDraw->Draw(0,0,100,100) ; pIDraw->Release() ; |
C++程序设计员通常delete一个对象指针。因为这个原因,C++程序员容易忘记并且在一个接口指针上用delete代替Release。它也是C++程序员很难发现的一个错误,因为删除指针太自然了。
第二条规则是在创建新的指针时调用AddRef 。下面的代码是不正确的:
IDraw* pIDraw ; CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ; pIDraw->Draw(0,0,100,100) ; delete pIDraw ; // Don't delete an interface pointer. |
下面的代码是正确的:
IDraw* pIDraw ; CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ; pIDraw->Draw(0,0,100,100) ; pIDraw->Release() ; |
上面的例子很小,但是,在复杂的代码中,这个错误是很难跟踪到的。
使用巧妙的接口指针类并不只有唯一一个原因。DonBox在C++报道中提到了其他的一些原因(请参阅本篇文章末尾的书目)。
如何创建一个巧妙的接口指针类
一个巧妙的接口指针类开始创建的方式与一个巧妙的指针类是一样的:通过为一个类执行操作符-》。这个处理过程也可被称为委派。通过覆盖操作符->,我们可以做一个类模仿一个指针调用。例如:
void Draw() { CSmartInterface<idraw> SIDraw; CoCreateInstance(...,IID,_IDraw, (void**)&SIDraw) ; SIDraw->Draw() ; } |
在上面的代码中有几点需要注意。首先,一个模板类被用于执行巧妙的接口指针。这使得巧妙的指针接口类是个安全类型。第二,就象我们就要看到的,操作符-已经被超载去返回一个包括在CsmantInterface中的指针的地址。第三,即使SIDraw不是一个指针,我们也使用操作符->去调用Idraw接口中的成员。第四,我们不调用Release是因为在栈上已经创建了CxmartInterface,析构函数会自动调用Release。
下面是CsmartInterface头文件中重要的部分,所以的成员函数和操作符都在头文件中稍后的部分中定义了。
CsmartInterface包含着一个指向一个接口的指针。为了类型安全,它被定义为一个模板函数。CsmartInterface的实质是超载操作符->。
template <class i> class CSmartInterface { public: // Construction CSmartInterface(I* pI = NULL) ; // Copy Constructor CSmartInterface(const CSmartInterface<i>& rSI) ; // Destruction ~CSmartInterface() ; // Assignment from I* CSmartInterface& operator=(I* pI) ; // // Operators // // Conversion operator I*() ; // Deref I* operator->() ; // Address of I** operator&() ; // Equality BOOL operator==(I* pI) const; // Inequality BOOL operator!=(I* pI) const; // Negation BOOL operator!() const ; protected: I* m_pI ; }; |
所以,来自前一个例子的SIDraw->Draw()导致了对SIDraw.m_PI->Draw()的调用。SIDraw把Draw调用委派给m-PI指向的接口。这种方式的强大之处在于CsmartInterface
为了使CsmartInterface成为纯粹的C++指针的一个更为令人可信的模拟,处理操作符 ->还需要定义别的操作符。事实上,做一个巧妙的指针类的最困难的事情是确保所有用在指针上的操作符都已经定义并且有意义。例如:当我把下面的代码从if(PISimple==NULL)转变成if (SISimple == NULL)。。。。。。,我就必须为我的巧妙的指针类 定义操作符==。生成的代码编译没有错误,不过,它包含着一个错误因为它把NULL 与SISimple比较而不是和我已经扩展的SISimple.m_PI比较。在我定义了操作符==后,这个错误消失了。你喜爱的C++编程书的目录应该列出了所以你需要定义的操作符,从而充当一个方便的查询表。为了安全起见,我定义了我认为我不需要的操作符--这样如果我视图用他们就会得到一条错误信息。类似于C/C++用户月刊中的Robert Mashlan的"C++中被查过的指针 "的文章能真正帮助你理解巧妙的指针。超载绝大部分需要的操作符使很直接明白的。最有趣的是操作符=:
template <class i> inline CSmartInterface<i>& CSmartInterface<i>::operator=(I* pI) { if (m_pI != pI) //OPTIMIZE: Same pointer AddRef/Release not needed. { if (m_pI != NULL) m_pI->Release() ; //Smart Pointers don't use smart pointers :-) m_pI = pI ; if (m_pI != NULL) m_pI->AddRef() ; } return *this ; } |
操作符=会自动地为你添加和释放接口。如果CsmartInterface已经指向一个接口,它将会释放它并且添加新的接口。这个操作符=的定义允许下列操作:
void DrawThemAll() { CSmartInterface<idraw> SIDraw ; for (int i = 0 ; i < MAX ; i++ ) { SIDraw = pIDraw[i] ; SIDraw->Forward(x) ; } } |
上面的代码依靠CsmartInterface的析构函数去释放指针,方法是:
template <class i> inline CSmartInterface<i>::~CSmartInterface() { if (m_pI != NULL) { m_pI->Release(); } } |
我们使用恰当的构造函数可以更好地利用析构函数
template <class i> inline CSmartInterface<i>::CSmartInterface(I* pI /*=NULL*/) : m_pI(pI) { if (m_pI != NULL) { // AddRef if we are copying an existing interface pointer. m_pI->AddRef() ; } } |
现在我们的例子可以改为:
void DrawThemAll() { for (int i = 0 ; i < MAX ; i++ ) { CSmartInterface<idraw> SIDraw(pIDraw[i]) ; SIDraw->Forward(x) ; } } |
上面的代码运行过一个Idraw接口指针的列表,添加他们,使用他们和释放他们。
关于这一点你还可以作的更多。DonBox在他的专题里把这一点阐述得更为深入。他定义了他的CsmartInterface的等价物去接受接口的ID和类型。然后他定义了一个构造函数,一旦有了来自不同接口的赋值,这个接口函数将调用Query Interface:
CSmartInterface(IUnknown* pI) : m_pI(NULL) { if (pI != NULL) pI->QueryInterface(*piid, (void**)&m_pI) ; } |
上面的构造函数允许我们把例子改为:
void DrawThemAll() { for (int i = 0 ; i <<IDraw, &IID_Draw> MAX ; i++ ) { CSmartInterface SIDraw(pIUnknown[i]) ; SIDraw->Forward(x) ; } } |
这个例子开始表现这种技术的强大之处,上面的代码类似于:
void DrawThemAll() { IDraw* pIDraw ; for (int i = 0 ; i < MAX ; i++ ) { pIUnknown[i]->QueryInterface(IID_Draw, (void**)&pIDraw) ; pIDraw->Forward(x) ; pIDraw->Release() ; } } |
操作符=也能用同样的方式扩展,所以类似于SIDraw=PIUnknown;这样的赋值将会调用QueryInterface。我并不热衷于把执行藏在看起来无害的操作符后 ,尽管我不得不说用这种方式超载操作符=是非常令人信服的方式。Visual Basic 4.0版本在把一个组件对象模型分配给另一个时就要调用QueryInterface。
使用巧妙的接口指针类
使用CsmartInterface有两个主要的规则。首先,不要在CsmartInterface对象上调用Release。
void Draw() { CSmartInterface<idraw> SIDraw; CoCreateInstance(...,IID,_IDraw, (void**)&SIDraw) ; SIDraw->Draw() ; SIDraw->Release() ; // Will compile, but is a bug. } |
当SIDraw->Release被调用,则SIDraw->m_pl被释放。当SIDraw->m_PI的析构函数被调用时,只要接口还没有 被释放,SIDraw->m_PI将再一次被释放。这个问题并不是难以调试,如果你有关于引用计数的问题,你可以查找一下所有发生的Release。另一个途径是适用#define Release BOGUS_DO_NOT_CALL_RELEASE!但是这样使用Release则会产生一个错误。当然,如果你的应用程序里其他带有"Release"这个词的函数(就象许多Win32应用程序接口[APIS],这个方法将行不通。
第二条规则是避免把CsmartInterface对象使用成指针。如果你这样做,情况将很混乱:
CSmartInterface<isimple>* pSISimple = new CSmartInterface<isimple>(m_pISimple) ; (*pSISimple)->Inc() ; delete pSISimple ; |
使用typedef将能将这种情况稍稍整理一下:
typedef CSmartInterface<isimple> CSmartISimple ; CSmartISimple* pSISimple = new CSmartISimple(m_pISimple) ; (*pSISimple)->Inc() ; delete pSISimple ; |
不过,这并不能改变(*PSISimple)->Incc);实际上不是非常简单的事实。
如果我们考虑一下为什么我们想要一个指向接口的指针,我们可能会发现环绕着问题的一条路。我们想在程序中的一些任意点释放接口--而不是要等到CsmartInterface已经不在范围之内。那么问题将变成我们如何释放包含在CsmartInterface中的接口。答案非常的简单:SISimple=NULL;虽然这是一句很典型的C代码,但是这儿到底发生了什么却一点都不明显。
pISimple->Release() ; and delete pSISimple ; |
都是更明显地指示对象已经消失的方法。
我不喜欢巧妙的接口指针类的原因
我为什么不打算使用巧妙的指针类有几点原因。所有这些理由的要点是CsmartInterface并不象C++。在一个对象上使用操作符->而不是指向对象的指针,这真的很奇怪。
一个相关的原因是使用指向CsmartInterface的指针并不直截了当--事实上它非常混乱。我的绝大部分组件对象模型使用隐藏的接口指针的容器,而且接口的生命周期也很少被限制在函数的范围内。我为一个接口调用QueryInterface,把它存储在容器里,使用它,最后释放它--所以这些都来自我的代码中的不同地方。使用这种结构类型,我需要分配和释放栈上的巧妙接口,而这就象我在前面章节中说明的那样非常混乱。
一个C++的程序员可能要删除一个接口指针,而一个组件对象模型(COM)程序员则可能要释放一个CsmartInterface对象。没有什么方便的方法可以阻止这一点。所以,我们的解决方法已经替换了一个带有类似相等问题的问题。当然,至少目前为止,C++程序员的人数还是要比组件对象模型(COM)程序员的人数要多。
我已经决定使用接口包装代替巧妙的接口指针。在我的"用接口包装调用组件对象模型(COM)对象"一文中对接口包装进行了描述。
总结
巧妙的接口指针是一种强有力的技术,它使得用组件对象模型(COM)对象工作更为简单并且更加bug_free。不过,我发现巧妙的接口指针是一种非常奇怪的东西。他们既不是纯粹的指针也不是纯粹的对象。他们也不能象我所期望的那样适合我的应用程序结构。不过,我还是强烈建议你试试在你的应用程序里使用巧妙的接口指针,看看他们是如何为你工作的。他们可能只是你需要让你的应用程序更快且问题更少地技巧而已。