起步篇
在本文的第一部分,我们简要介绍了ATL的一些背景知识以及ATL所面向的开发技术和环境。在这一部分 将开始走进ATL,讲述ATL编程的基本方法、原则和必须要注意的问题。
理解ATL最容易的方法是考察它对客户端编程的支持。对于COM编程新手而言,一个棘手的主要问题之一是正确管理接口指针的引用计数。COM的引用计数法则是没有运行时强制 性的,也就是说每一个客户端必须保证对对象的承诺。
有经验的COM编程者常常习惯于使用文档中(如《Inside OLE》)提出的标准模式。调用某个函数或方法,返回接口指针,在某个时间范围内使用这个接口指针,然后释放它。下面是使用这种模式的代码例子:
void f(void) {
IUnknown *pUnk = 0;
// 调用
HRESULT hr = GetSomeObject(&pUnk);
if (SUCCEEDED(hr)) {
// 使用
UseSomeObject(pUnk);
// 释放
pUnk->Release();
}
}
这个模式在COM程序员心中是如此根深蒂固,以至于他们常常不写实际使用指针的语句,而是先在代码块末尾敲入Release语句。这很像C程序员使用switch语句时的条件反射一样,先敲入break再说。void f(void) {
IUnknown *rgpUnk[3];
HRESULT hr = GetObject(rgpUnk);
if (SUCCEEDED(hr)) {
hr = GetObject(rgpUnk + 1);
if (SUCCEEDED(hr)) {
hr = GetObject(rgpUnk + 2);
if (SUCCEEDED(hr)) {
UseObjects(rgpUnk[0], rgpUnk[1],
rgpUnk[2]);
rgpUnk[2]->Release();
}
rgpUnk[1]->Release();
}
rgpUnk[0]->Release();
}
}
像这样的语句常常促使程序员将TAB键设置成一个或两个空格,甚至情愿使用大一点的显示器。但事情并不总是按你想象的那样,由于种种原因项目团队中的COM组件编程人员往往得不到 所想的硬件支持,而且在公司确定关于TAB键的使用标准之前,程序员常常求助于使用有很大争议但仍然有效的“GOTO”语句:void f(void) {
IUnknown *rgpUnk[3];
ZeroMemory(rgpUnk, sizeof(rgpUnk));
if (FAILED(GetObject(rgpUnk)))
goto cleanup;
if (FAILED(GetObject(rgpUnk+1)))
goto cleanup;
if (FAILED(GetObject(rgpUnk+2)))
goto cleanup;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
cleanup:
if (rgpUnk[0]) rgpUnk[0]->Release();
if (rgpUnk[1]) rgpUnk[1]->Release();
if (rgpUnk[2]) rgpUnk[2]->Release();
} 这样的代码虽然不那么专业,但至少减少了屏幕的水平滚动。void f(void) {
IUnknown *rgpUnk[3];
ZeroMemory(rgpUnk, sizeof(rgpUnk));
__try {
if (FAILED(GetObject(rgpUnk))) leave;
if (FAILED(GetObject(rgpUnk+1))) leave;
if (FAILED(GetObject(rgpUnk+2))) leave;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
} __finally {
if (rgpUnk[0]) rgpUnk[0]->Release();
if (rgpUnk[1]) rgpUnk[1]->Release();
if (rgpUnk[2]) rgpUnk[2]->Release();
}
可惜Win32 SHE在C++中的表现并不如想象得那么好。较好的方法是使用内建的C++异常处理模型,同时停止使用没有加工过的指针。标准C++库有一个类:auto_ptr,在其析构函数中定 死了一个操作指针的delete调用(即使在出现异常时也能保证调用)。与之类似,ATL有一个COM智能指针,CComPtr,它的析构函数会正确调用Release。 CComPtr<IUnknown> unk;
CComPtr<IClassFactory> cf;
缺省的构造函数将这个原始指针数据成员初始化为NULL。智能指针也有构造函数,它的参数要么是原始指针,要么是相同类型的智能参数。不论哪种情况,智能指针都调用AddRef控制引用。CComPtr的赋值操作符 既可以处理原始指针,也可以处理智能指针,并且在调用新分配指针的AddRef之前自动释放保存的指针。最重要的是,CComPtr的析构函数释放保存的接口(如果非空)。请看下列代码:void f(IUnknown *pUnk1, IUnknown *pUnk2) {
// 如果非空,构造函数调用pUnk1的AddRef
CComPtr unk1(pUnk1);
// 如果非空,构造函数调用unk1.p的AddRef
CComPtr unk2 = unk1;
// 如果非空,operator= 调用unk1.p的Release并且
//如果非空,调用unk2.p的AddRef
unk1 = unk2;
//如果非空,析构函数释放unk1 和 unk2
}
除了正确实现COM的AddRef 和 Release规则之外,CComPtr还允许实现原始和智能指针的透明操作,参见附表二所示。也就是说下面的代码按照你所想象的方式运行:void f(IUnknown *pUnkCO) {
CComPtr cf;
HRESULT hr;
// 使用操作符 & 获得对 &cf.p 的存取
hr = pUnkCO->QueryInterface(IID_IClassFactory,(void**)&cf);
if (FAILED(hr)) throw hr;
CComPtr unk;
// 操作符 -> 获得对cf.p的存取
// 操作符 & 获得对 &unk.p的存取
hr = cf->CreateInstance(0, IID_IUnknown, (void**)&unk);
if (FAILED(hr)) throw hr;
// 操作符 IUnknown * 返回 unk.p
UseObject(unk);
// 析构函数释放unk.p 和 cf.p
}
除了缺乏对Release的显式调用外,这段代码像是纯粹的COM代码。有了CComPtr类的武装,前面所遇到的麻烦问题顿时变得简单了:void f(void) {
CComPtr<IUnknown> rgpUnk[3];
if (FAILED(GetObject(&rgpUnk[0]))) return;
if (FAILED(GetObject(&rgpUnk[1]))) return;
if (FAILED(GetObject(&rgpUnk[2]))) return;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
}
由于CComPtr对操作符重载用法的扩展,使得代码的编译和运行无懈可击。 CComQIPtr<IDataObject, &IID_IDataObject> do;
CComQIPtr<IPersist, &IID_IPersist> p;
CComQIPtr的优点是它有重载的构造函数和赋值操作符。同类版本(例如,接受相同类型的接口)仅仅AddRef右边的赋值/初始化操作。这实际上就是CComPtr的功能。异类版本(接受类型不一致的接口)正确调用QueryInterface来决定是否这个对象确实支持所请求的接口:
void f(IPersist *pPersist) {
CComQIPtr<IPersist, &IID_IPersist> p;
// 同类赋值 - AddRef''s
p = pPersist;
CComQIPtr<IDataObject, &IID_IDataObject> do;
// 异类赋值 - QueryInterface''s
do = pPersist;
}
在第二种赋值语句中,因为pPersist是非IDataObject *类型,但它是派生于IUnknown的接口指针,CComQIPtr通过pPersist调用QueryInterface来试图获得这个对象的IDataObject接口指针。如果QueryInterface调用成功,则此智能指针将含有作为结果的原始IDataObject指针。如果QueryInterface调用失败,则do.p将被置为null。如果QueryInterface返回的HRESULT值很重要,但又没有办法从赋值操作获得其值时,则必须显式调用QueryInterface。CComPtr<IUnknown> unk;从功能上将它等同于
CComQIPtr<IUnknown, &IID_IUnknown> unk;前者正确。后者是错误的用法。如果你这样写了,C++编译器将提醒你改正。
void f(void) {
IFoo *pFoo = 0;
HRESULT hr = GetSomeObject(&pFoo);
if (SUCCEEDED(hr)) {
UseSomeObject(pFoo);
pFoo->Release();
}
} 将它自然而然转换到使用CComPtr。 void f(void) {
CComPtr<IFoo> pFoo = 0;
HRESULT hr = GetSomeObject(&pFoo);
if (SUCCEEDED(hr)) {
UseSomeObject(pFoo);
pFoo->Release();
}
}
注意CComPtr 和 CComQIPtr输出所有受控接口成员,包括AddRef和Release。可惜当客户端通过操作符->的结果调用Release时,智能指针很健忘 ,会二次调用构造函数中的Release。显然这是错误的,编译器和链接器也欣然接受了这个代码。如果你运气好的话,调试器会很快捕获到这个错误。 void f(IUnknown *pUnk) {
CComPtr unk = pUnk;
// 隐式调用操作符IUnknown *()
CoLockObjectExternal(unk, TRUE, TRUE);
}
这段代码能正确运行,但是下面的代码也不会产生警告信息,编译正常通过: HRESULT CFoo::Clone(IUnknown **ppUnk) {
CComPtr unk;
CoCreateInstance(CLSID_Foo, 0, CLSCTX_ALL,
IID_IUnknown, (void **) &unk);
// 隐式调用操作符IUnknown *()
*ppUnk = unk;
return S_OK;
}
在这种情况下,智能指针(unk)对原始值针**ppUnk的赋值触发了与前面代码段相同的强制类型转换。在第一个例子中,不需要用AddRef。在第二个例子中,必须要用AddRef。