Windows桌面应用程序(1-2-3-9th) COM编码实践

本主题描述了使您的COM代码更加有效和健壮的方法。

  • __uuidof运算符
  • IID_PPV_ARGS宏
  • SafeRelease模式
  • COM智能指针

__uuidof运算符

当您构建您的程序时,您可能会遇到与以下类似的链接器错误:

unresolved external symbol “struct _GUID const IID_IDrawable”

无法解析的外部符号“struct _GUID const IID_IDrawable”

这个错误意味着一个GUID常量被声明为外部链接(extern),并且链接器找不到该常量的定义。 GUID常量的值通常从静态库文件中导出。 如果您使用的是Microsoft Visual C ++,则可以避免使用__uuidof运算符链接静态库。 该操作符是Microsoft语言扩展。 它从表达式返回一个GUID值。 该表达式可以是接口类型名称,类名称或接口指针。 使用__uuidof,您可以创建Common Item对话框对象,如下所示:

IFileOpenDialog *pFileOpen;
hr=CoCreateInstance(__uuidof(FileOpenDialog),NULL,CLSCTX_ALL,__uuidof(pFileOpen),reinterpret_cast<void**>(&pFileOpen));

编译器从头中提取GUID值,所以不需要导出库。

注意 GUID值通过在标题中声明__declspec(uuid(…))与类型名称关联。 有关更多信息,请参阅Visual C ++文档中的__declspec文档。

IID_PPV_ARGS宏

我们看到CoCreateInstanceQueryInterface都要求将最终参数强制为void **类型。 这造成了类型不匹配的可能性。 考虑下面的代码片段:

// Wrong!
IFileOpenDialog *pFileOpen;
hr=CoCreateInstance(__uuidof(FileOpenDialog),NULL,CLSCTX_ALL,
    __uuidof(IFileDialogCustomize), // The IID does not match the pointer type!
    reinterpret_cast<void**>(&pFileOpen) // Coerce to void**.
);

此代码要求提供IFileDialogCustomize接口,但传入一个IFileOpenDialog指针。 reinterpret_cast表达式绕开了C ++类型的系统,所以编译器不会捕获这个错误。 在最好的情况下,如果对象没有实现请求的接口,那么调用就会失败。 在最坏的情况下,函数成功并且你有一个不匹配的指针。 换句话说,指针类型与内存中的实际vtable不匹配。 正如你可以想象的那样,在这一点上什么都不会发生。

注意 vtable(虚拟方法表)是一个函数指针表。 vtable是COM如何在运行时将方法调用绑定到其实现。 不巧的是,vtables是大多数C ++编译器实现虚拟方法的方式。

IID_PPV_ARGS宏有助于避免这类错误。 要使用此宏,请使用此:

扫描二维码关注公众号,回复: 1979762 查看本文章
IID_PPV_ARGS(&pFileOpen)

替换下面的代码:

__uuidof(IFileDialogCustomize),reinterpret_cast<void**>(&pFileOpen)

宏自动为接口标识符插入__uuidof(IFileOpenDialog),所以它保证匹配指针类型。 这里是修改的(和正确的)代码:

IFileDialogCustomize *pCustom;
hr=pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));

SafeRelease模式

引用计数是编程中的其中一件事情,基本上很简单,但也很乏味,这很容易出错。 典型的错误包括:

  • 完成使用后,无法释放接口指针。 这类错误会导致程序泄漏内存和其他资源,因为对象不会被销毁。
  • 用无效指针调用Release。 例如,如果该对象从未创建,则会发生此错误。 这类错误可能会导致程序崩溃。
  • 调用Release之后解除引用接口指针。 该错误可能会导致程序崩溃。 更糟糕的是,它可能会导致您的程序在随后的一段时间内随机崩溃,导致很难追查原始错误。

避免这些错误的一种方法是通过安全释放指针的函数调用Release。 下面的代码显示了一个这样做的函数:

template<class T>
void SafeRelease(T **ppT){
    if(*ppT){
        (*ppT)->Release();
        *ppT=NULL;
    }
}

该函数将COM接口指针作为参数并执行以下操作:

  1. 检查指针是否为NULL。
  2. 如果指针不是NULL,则调用Release
  3. 将指针设置为NULL。

这里是一个如何使用SafeRelease的例子:

void UseSafeRelease(){
    IFileOpenDialog *pFileOpen=NULL;
    HRESULT hr=CoCreateInstance(__uuidof(FileOpenDialog),NULL,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&pFileOpen));
    if(SUCCEEDED(hr)){
        // Use the object.
    }
    SafeRelease(&pFileOpen);
}

如果CoCreateInstance成功,则对SafeRelease的调用将释放指针。 如果CoCreateInstance失败,则pFileOpen保持为NULL。 SafeRelease函数检查这个并跳过对Release的调用。

在同一个指针上多次调用SafeRelease也是安全的,如下所示:

// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);

COM智能指针

SafeRelease功能很有用,但它需要你记住两件事:

  1. 将每个接口指针初始化为NULL。
  2. 在每个指针超出范围之前调用SafeRelease。

作为一名C ++程序员,你可能认为你不应该记住这些事情。 毕竟,这就是为什么C ++具有构造函数和析构函数的原因。 有一个包装底层接口指针的类并自动初始化并释放指针会很好。 换句话说,我们想要这样的东西:

// Warning: This example is not complete.
template<class T>
class SmartPointer{
    T* ptr;
    public:
        SmartPointer(T *p):ptr(p){}
        ~SmartPointer(){
            if(ptr)
                ptr->Release();
        }
};

此处显示的类定义不完整,不能如图所示使用。 至少,你需要定义一个拷贝构造函数,一个赋值操作符和一个访问底层COM指针的方法。 幸运的是,您不需要执行任何此类工作,因为Microsoft Visual Studio已经提供了一个智能指针类作为活动模板库(ATL)的一部分。

ATL智能指针类被命名为CComPtr。 (还有一个CComQIPtr类,这里不讨论这个。)下面是使用CComPtr重写的Open Dialog Box示例。

#include<windows.h>
#include<shobjidl.h> 
#include<atlbase.h>// Contains the declaration of CComPtr.
int WINAPI wWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR pCmdLine,int nCmdShow){
    HRESULT hr=CoInitializeEx(NULL,COINIT_APARTMENTTHREADED|COINIT_DISABLE_OLE1DDE);
    if(SUCCEEDED(hr)){
        CComPtr<IFileOpenDialog> pFileOpen;
        // Create the FileOpenDialog object.
        hr=pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
        if(SUCCEEDED(hr)){
            // Show the Open dialog box.
            hr=pFileOpen->Show(NULL);
            // Get the file name from the dialog box.
            if(SUCCEEDED(hr)){
                CComPtr<IShellItem> pItem;
                hr=pFileOpen->GetResult(&pItem);
                if(SUCCEEDED(hr)){
                    PWSTR pszFilePath;
                    hr=pItem->GetDisplayName(SIGDN_FILESYSPATH,&pszFilePath);
                    // Display the file name to the user.
                    if(SUCCEEDED(hr)){
                        MessageBox(NULL,pszFilePath,L"File Path",MB_OK);
                        CoTaskMemFree(pszFilePath);
                    }
                }
                // pItem goes out of scope.
            }
            // pFileOpen goes out of scope.
        }
        CoUninitialize();
    }
    return 0;
}

此代码与原始示例之间的主要区别在于此版本未显式调用Release。 当CComPtr实例超出作用域时,析构函数会在基础指针上调用Release

CComPtr是一个类模板。 模板参数是COM接口类型。 在内部,CComPtr保存这种类型的指针。 CComPtr覆盖了operator->()operator&(),这样该类就像底层的指针一样。 例如,下面的代码等同于直接调用IFileOpenDialog::Show方法:

hr=pFileOpen->Show(NULL);

CComPtr还定义了一个CComPtr::CoCreateInstance方法,该方法使用一些默认参数值调用COM CoCreateInstance函数。 唯一需要的参数是类标识符,如下例所示:

hr=pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));

CComPtr::CoCreateInstance方法纯粹是为了方便而提供的; 如果您愿意,您仍然可以调用COM CoCreateInstance函数。

下一个
COM中的错误处理


原文链接:COM Coding Practices

猜你喜欢

转载自blog.csdn.net/qq_37422196/article/details/79771846
今日推荐