COM 程式代碼撰寫做法
本主題描述讓您的 COM 程式代碼更有效率且健全的方法。
__uuidof運算符
當您建置程式時,可能會收到類似下列的連結器錯誤:
unresolved external symbol "struct _GUID const IID_IDrawable"
此錯誤表示 GUID 常數是使用外部連結宣告的,而且連結器找不到常數的定義。 GUID 常數的值通常會從靜態庫檔案導出。 如果您使用 Microsoft Visual C++,您可以使用 __uuidof 運算符來避免鏈接靜態庫的需求。 此運算子是 Microsoft 語言延伸模組。 它會從表達式傳回 GUID 值。 表達式可以是介面類型名稱、類別名稱或介面指標。 使用 __uuidof,您可以建立 Common Item Dialog 物件,如下所示:
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宏
我們看到 CoCreateInstance 和 QueryInterface 都需要將最終參數強制轉換為 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 如何在運行時間將方法呼叫系結至其實作。 並非巧合的是,vtable 是大部分 C++ 編譯程式實作虛擬方法的方式。
IID_PPV_ARGS宏有助於避免這個錯誤類別。 若要使用此宏,請取代下列程序代碼:
__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)
取代為這個:
IID_PPV_ARGS(&pFileOpen)
宏會自動插入 __uuidof(IFileOpenDialog)
介面標識符,因此保證它符合指標類型。 以下是修改過的 (和正確) 程式代碼:
// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
IID_PPV_ARGS(&pFileOpen));
您可以搭配 QueryInterface 使用相同的宏:
IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));
保管庫 Release 模式
參考計數是程序設計中的其中一項,基本上很容易,但也很乏味,這使得很容易出錯。 一般錯誤包含:
- 當您完成使用介面指標時,無法釋出介面指標。 這個 Bug 類別會導致程式流失記憶體和其他資源,因為物件不會損毀。
- 使用無效的指標呼叫 Release。 例如,如果從未建立物件,就會發生此錯誤。 此類別的錯誤可能會導致程序當機。
- 在呼叫 Release 之後取值介面指標。 這個錯誤可能會導致程序當機。 更糟的是,它可能會導致程式在稍後隨機損毀,因此很難追蹤原始錯誤。
避免這些 Bug 的其中一個方法是透過可安全地釋放指標的函式呼叫 Release。 下列程式代碼顯示執行這項作業的函式:
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
此函式會採用 COM 介面指標作為參數,並執行下列動作:
- 檢查指標是否為 NULL。
- 如果指標不是 NULL,則呼叫 Release。
- 將指標設定為 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
式很有用,但它需要您記住兩件事:
- 初始化 NULL 的每個介面指標。
- 在每個指標超出範圍之前呼叫
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 已經提供智慧型手機類塊類別作為 Active Template Library (ATL) 的一部分。
ATL 智慧型手機類別名為 CComPtr。 (也有此處未討論的 CComQIPtr 類別。以下是重寫為使用 CComPtr 的開啟對話框範例。
#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 函式。
下一步