共用方式為


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宏

我們看到 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 如何在運行時間將方法呼叫系結至其實作。 並非巧合的是,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 介面指標作為參數,並執行下列動作:

  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 式很有用,但它需要您記住兩件事:

  • 初始化 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 函式。

下一步

COM 中的錯誤處理