培训
使用 COM 编程 DirectX
Microsoft 组件对象模型 (COM) 是一种面向对象的编程模型,由多种技术使用,包括大部分 DirectX API 图面。 因此, (作为 DirectX 开发人员,) 编程 DirectX 时不可避免地使用 COM。
备注
主题 使用 C++/WinRT 的 COM 组件 介绍了如何使用 DirectX API (和任何 COM API,为此,) 使用 C++/WinRT。 这是迄今为止最方便、最推荐使用的技术。
或者,可以使用原始 COM,这就是本主题的内容。 你需要基本了解使用 COM API 所涉及的原则和编程技术。 尽管 COM 以困难和复杂著称,但大多数 DirectX 应用程序所需的 COM 编程非常简单。 这在一定程度上是因为你将使用 DirectX 提供的 COM 对象。 无需创作自己的 COM 对象,这通常是产生复杂性的地方。
COM 对象实质上是一个封装的功能组件,应用程序可以使用它来执行一个或多个任务。 对于部署,一个或多个 COM 组件打包到名为 COM 服务器的二进制文件中;通常不是 DLL。
传统的 DLL 导出自由函数。 COM 服务器也可以执行相同的操作。 但 COM 服务器中的 COM 组件公开属于这些接口的 COM 接口和成员方法。 应用程序创建 COM 组件的实例,从中检索接口,并在这些接口上调用方法,以便从 COM 组件中实现的功能中受益。
在实践中,这感觉类似于在常规 C++ 对象上调用方法。 但也有一些差异。
- COM 对象强制实施比 C++ 对象更严格的封装。 不能只创建 对象,然后调用任何公共方法。 而是将 COM 组件的公共方法分组到一个或多个 COM 接口中。 若要调用方法,请创建 对象,并从对象中检索实现该方法的接口。 接口通常实现一组相关的方法,这些方法提供对对象特定功能的访问权限。 例如, ID3D12Device 接口表示虚拟图形适配器,它包含可用于创建资源的方法,例如,以及许多其他与适配器相关的任务。
- COM 对象的创建方式与 C++ 对象不同。 有几种方法可以创建 COM 对象,但都涉及特定于 COM 的技术。 DirectX API 包括各种帮助程序函数和方法,可简化大多数 DirectX COM 对象的创建。
- 必须使用特定于 COM 的技术来控制 COM 对象的生存期。
- COM 服务器 (通常不需要显式加载 DLL) 。 为了使用 COM 组件,也不链接到静态库。 每个 COM 组件都有一个唯一的注册标识符 (全局唯一标识符或 GUID) ,应用程序使用该标识符来标识 COM 对象。 应用程序标识组件,COM 运行时会自动加载正确的 COM 服务器 DLL。
- COM 是二进制规范。 COM 对象可以用多种语言编写和访问。 无需了解有关对象的源代码的任何信息。 例如,Visual Basic 应用程序通常使用用 C++ 编写的 COM 对象。
了解组件、对象和接口之间的区别非常重要。 在随意使用中,你可能会听到组件或对象通过其主体接口的名称引用。 但是这些术语不可互换。 组件可以实现任意数量的接口;和 对象是组件的实例。 例如,虽然所有组件都必须实现 IUnknown 接口,但它们通常至少实现一个附加接口,并且可能实现多个接口。
若要使用特定的接口方法,您不仅必须实例化对象,还必须从该对象获取正确的接口。
此外,多个组件可能实现相同的接口。 接口是一组执行逻辑相关操作的方法。 接口定义仅指定方法的语法及其常规功能。 需要支持一组特定操作的任何 COM 组件都可以通过实现合适的接口来实现。 某些接口高度专用化,仅由单个组件实现;其他组件在各种情况下很有用,由许多组件实现。
如果组件实现接口,则必须支持接口定义中的每个方法。 换句话说,必须能够调用任何方法,并确信它存在。 但是,特定方法的实现方式的详细信息可能因组件而异。 例如,不同的组件可能使用不同的算法来得出最终结果。 也不保证以非平凡的方式支持方法。 有时,组件实现常用接口,但它只需要支持方法的子集。 你仍然可以成功调用剩余的方法,但它们将返回 HRESULT (这是一种标准 COM 类型,表示包含 值E_NOTIMPL的结果代码) 。 应参考其文档,了解接口如何由任何特定组件实现。
COM 标准要求接口定义在发布后不得更改。 例如,作者无法向现有接口添加新方法。 作者必须改为创建一个新接口。 虽然该接口中必须包含哪些方法没有限制,但常见做法是让下一代接口包含旧接口的所有方法以及任何新方法。
接口具有几代并不罕见。 通常,所有代都执行大致相同的整体任务,但它们在细节上有所不同。 通常,COM 组件实现给定接口世系的每个当前和上一代。 这样做允许较旧的应用程序继续使用对象的旧接口,而较新的应用程序可以利用较新接口的功能。 通常,一组接口都具有相同的名称,外加一个指示生成的整数。 例如,如果原始接口名为 IMyInterface (表示第 1 代) ,则接下来的两代将称为 IMyInterface2 和 IMyInterface3。 对于 DirectX 接口,通常以 DirectX 的版本号命名连续几代。
GUID 是 COM 编程模型的关键部分。 在最基本的情况下,GUID 是一个 128 位结构。 但是,创建 GUID 的方式是保证没有两个 GUID 是相同的。 COM 广泛使用 GUID 以实现两个主要目的。
- 唯一标识特定 COM 组件。 分配用于标识 COM 组件的 GUID 称为 (CLSID) 的类标识符,如果要创建关联 COM 组件的实例,请使用 CLSID。
- 唯一标识特定 COM 接口。 分配用于标识 COM 接口的 GUID 称为 IID) (接口标识符,当从组件实例请求特定接口时,可以使用 IID (对象) 。 无论哪个组件实现接口,接口的 IID 都是相同的。
为方便起见,DirectX 文档通常通过其描述性名称 (例如 ID3D12Device) 而不是按其 GUID 引用组件和接口。 在 DirectX 文档的上下文中,没有歧义。 从技术上讲,第三方可以创作具有描述性名称 ID3D12Device 的接口, (它需要具有不同的 IID 才能) 有效。 不过,为了清楚起见,我们不建议这样。
因此,引用特定对象或接口的唯一明确方法是使用其 GUID。
虽然 GUID 是一种结构,但 GUID 通常以等效的字符串形式表示。 GUID 字符串形式的一般格式为 32 个十六进制数字,格式为 8-4-4-4-12。 即 {xxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxx},其中每个 x 对应于一个十六进制数字。 例如, ID3D12Device 接口的 IID 字符串形式为 {189819F1-1DB6-4B57-BE54-1821339B85F7}。
由于实际的 GUID 使用起来有些笨拙,并且容易键入错误,因此通常还会提供等效的名称。 在代码中,可以在调用函数时使用此名称而不是实际结构,例如,将参数的参数 riid
传递给 D3D12CreateDevice 时。 常规命名约定是分别在接口或对象的描述性名称前面追加IID_或CLSID_。 例如, ID3D12Device 接口的 IID 的名称IID_ID3D12Device。
备注
DirectX 应用程序应与 dxguid.lib
和 uuid.lib
链接,以提供各种接口和类 GUID 的定义。 Visual C++ 和其他编译器支持 __uuidof 运算符语言扩展,但还支持与这些链接库的显式 C 样式链接,并且完全可移植。
大多数 COM 方法返回名为 HRESULT 的 32 位整数。 对于大多数方法,HRESULT 本质上是一个包含两个主要信息段的结构。
- 方法是成功还是失败。
- 有关 方法执行的操作结果的更多详细信息。
某些方法从 中Winerror.h
定义的标准集返回 HRESULT 值。 但是,方法可以自由返回具有更专业信息的自定义 HRESULT 值。 这些值通常记录在方法的引用页上。
在方法的引用页上找到的 HRESULT 值列表通常只是可能返回的可能值的子集。 该列表通常仅涵盖特定于 方法的值,以及那些具有一些特定于方法含义的标准值。 应假定方法可能会返回各种标准 HRESULT 值,即使这些值未显式记录。
虽然 HRESULT 值通常用于返回错误信息,但不应将它们视为错误代码。 指示成功或失败的位与包含详细信息的位分开存储的事实使 HRESULT 值具有任意数量的成功和失败代码。 按照约定,成功代码的名称以 S_ 为前缀,失败代码以 E_ 作为前缀。 例如,两个最常用的代码是S_OK和E_FAIL,分别指示简单成功或失败。
COM 方法可能返回各种成功或失败代码,这意味着必须谨慎测试 HRESULT 值的方式。 例如,如果成功,请考虑一个假设方法,其中记录的返回值为 S_OK;如果成功,则E_FAIL。 但请记住,方法也可能返回其他失败或成功代码。 以下代码片段演示了使用简单测试的危险性,其中 hr
包含 方法返回的 HRESULT 值。
if (hr == E_FAIL)
{
// Handle the failure case.
}
else
{
// Handle the success case.
}
只要在失败的情况下,此方法仅返回E_FAIL (,而不返回其他一些失败代码) ,则此测试会起作用。 但是,实现给定方法以返回一组特定的故障代码(可能E_NOTIMPL或E_INVALIDARG)更为现实。 使用上面的代码,这些值将被错误地解释为成功。
如果需要有关方法调用结果的详细信息,则需要测试每个相关的 HRESULT 值。 但是,你可能只对方法是成功还是失败感兴趣。 测试 HRESULT 值是否指示成功还是失败的可靠方法是将值传递到 Winerror.h 中定义的以下宏之一。
- 对于成功代码,宏
SUCCEEDED
返回 TRUE,对失败代码返回 FALSE。 - 对于失败代码,宏
FAILED
返回 TRUE,成功代码返回 FALSE。
因此,可以使用 宏修复前面的代码片段 FAILED
,如以下代码所示。
if (FAILED(hr))
{
// Handle the failure case.
}
else
{
// Handle the success case.
}
此更正的代码片段正确地将E_NOTIMPL和E_INVALIDARG视为失败。
尽管大多数 COM 方法返回结构化 HRESULT 值,但少数方法使用 HRESULT 返回简单整数。 隐式地,这些方法始终成功。 如果将此类 HRESULT 传递给 SUCCEEDED 宏,则该宏始终返回 TRUE。 不返回 HRESULT 的常用方法的一个示例是 IUnknown::Release 方法,该方法返回 ULONG。 此方法将对象的引用计数递减 1,并返回当前引用计数。 有关引用计数的讨论,请参阅 管理 COM 对象的生存期 。
如果查看一些 COM 方法引用页,可能会跨如下所示的内容运行。
HRESULT D3D12CreateDevice(
IUnknown *pAdapter,
D3D_FEATURE_LEVEL MinimumFeatureLevel,
REFIID riid,
void **ppDevice
);
虽然普通指针对任何 C/C++ 开发人员来说都很熟悉,但 COM 通常使用额外的间接级别。 这第二个间接级别由两个星号指示, **
在类型声明之后,变量名称通常具有前缀 pp
。 对于上述函数, ppDevice
参数通常称为指向 void 的指针的地址。 实际上,在此示例中, ppDevice
是 指向 ID3D12Device 接口的指针的地址。
与 C++ 对象不同,你不会直接访问 COM 对象的 方法。 相反,必须获取指向公开 方法的接口的指针。 若要调用 方法,请使用与调用指向 C++ 方法的指针基本相同的语法。 例如,若要调用 IMyInterface::D oSomething 方法,应使用以下语法。
IMyInterface * pMyIface = nullptr;
...
pMyIface->DoSomething(...);
需要第二级间接连接的原因是你不直接创建接口指针。 必须调用各种方法之一,例如上面所示的 D3D12CreateDevice 方法。 若要使用此类方法获取接口指针,请将变量声明为指向所需接口的指针,然后将该变量的地址传递给 方法。 换句话说,将指针的地址传递给 方法。 方法返回时,变量指向请求的接口,你可以使用该指针调用接口的任何方法。
IDXGIAdapter * pIDXGIAdapter = nullptr;
...
ID3D12Device * pD3D12Device = nullptr;
HRESULT hr = ::D3D12CreateDevice(
pIDXGIAdapter,
D3D_FEATURE_LEVEL_11_0,
IID_ID3D12Device,
&pD3D12Device);
if (FAILED(hr)) return E_FAIL;
// Now use pD3D12Device in the form pD3D12Device->MethodName(...);
可通过多种方式创建 COM 对象。 这是 DirectX 编程中最常用的两个。
- 通过调用为你创建 对象的 DirectX 方法或函数间接。 方法创建 对象,并返回 对象上的接口。 以这种方式创建对象时,有时可以指定应返回哪个接口,而其他情况下,接口是隐含的。 上面的代码示例演示如何间接创建 Direct3D 12 设备 COM 对象。
- 直接将对象的 CLSID 传递给 CoCreateInstance 函数。 函数创建 对象的实例,并返回指向指定接口的指针。
一次,在创建任何 COM 对象之前,必须通过调用 CoInitializeEx 函数来初始化 COM。 如果要间接创建对象,则对象创建方法将处理此任务。 但是,如果需要使用 CoCreateInstance 创建对象,则必须显式调用 CoInitializeEx 。 完成后,必须通过调用 CoUninitialize 取消初始化 COM。 如果调用 CoInitializeEx ,则必须将其与对 CoUninitialize 的调用匹配。 通常,需要显式初始化 COM 的应用程序在其启动例程中执行此操作,并在其清理例程中取消初始化 COM。
若要使用 CoCreateInstance 创建 COM 对象的新实例,必须具有对象的 CLSID。 如果此 CLSID 公开可用,可在参考文档或相应的头文件中找到它。 如果 CLSID 不公开可用,则无法直接创建对象。
CoCreateInstance 函数有五个参数。 对于将用于 DirectX 的 COM 对象,通常可以按如下所示设置参数。
rclsid 将此设置为要创建的对象的 CLSID。
pUnkOuter 设置为 nullptr
。 仅当聚合对象时,才使用此参数。 有关 COM 聚合的讨论不在本主题的讨论范围内。
dwClsContext 设置为 CLSCTX_INPROC_SERVER。 此设置指示对象作为 DLL 实现,并作为应用程序进程的一部分运行。
riid 设置为要返回的接口的 IID。 函数将创建 对象,并在 ppv 参数中返回请求的接口指针。
Ppv 将此设置为指针的地址,该指针将设置为函数返回时指定的 riid
接口。 应将此变量声明为指向所请求接口的指针,并且对参数列表中的指针的引用应强制转换为 LPVOID *) (。
如上面的代码示例所示,间接创建对象通常要简单得多。 向对象创建方法传递接口指针的地址,然后该方法创建对象并返回接口指针。 间接创建对象时,即使无法选择该方法返回的接口,通常仍可以指定有关如何创建对象的各种内容。
例如,可以向 D3D12CreateDevice 传递一个值,该值指定返回的设备应支持的最低 D3D 功能级别,如上面的代码示例所示。
创建 COM 对象时,创建方法将返回接口指针。 然后,可以使用该指针访问接口的任何方法。 语法与用于指向 C++ 方法的指针的语法相同。
在许多情况下,从创建方法接收的接口指针可能是唯一需要的指针。 事实上,对象仅导出 IUnknown 以外的一个接口相对常见。 但是,许多对象导出多个接口,可能需要指向其中多个接口的指针。 如果需要的接口多于创建方法返回的接口,则无需创建新对象。 相反,请使用对象的 IUnknown::QueryInterface 方法请求另一个接口指针。
如果使用 CoCreateInstance 创建对象,则可以请求 IUnknown 接口指针,然后调用 IUnknown::QueryInterface 来请求所需的每个接口。 但是,如果只需要单个接口,则此方法不方便;如果使用的对象创建方法不允许指定应返回的接口指针,此方法根本不起作用。 实际上,通常不需要获取显式 IUnknown 指针,因为所有 COM 接口都扩展 了 IUnknown 接口。
扩展接口在概念上类似于从 C++ 类继承。 子接口公开父接口的所有方法,以及其自己的一个或多个方法。 事实上,你经常会看到使用“继承自”而不是“extends”。 需要记住的是,继承是 对象内部的。 应用程序无法从 对象接口继承或扩展。 但是,可以使用子接口调用子级或父级的任何方法。
由于所有接口都是 IUnknown 的子级,因此可以在已为对象使用的任何接口指针上调用 QueryInterface 。 执行此操作时,必须提供正在请求的接口的 IID,以及方法返回时将包含接口指针的指针的地址。
例如,以下代码片段调用 IDXGIFactory2::CreateSwapChainForHwnd 来创建主交换链对象。 此对象公开多个接口。 CreateSwapChainForHwnd 方法返回 IDXGISwapChain1 接口。 然后,后续代码使用 IDXGISwapChain1 接口调用 QueryInterface 以请求 IDXGISwapChain3 接口。
HRESULT hr = S_OK;
IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
hWnd,
&swapChainDesc,
nullptr,
nullptr,
&pDXGISwapChain1));
if (FAILED(hr)) return hr;
IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;
备注
在 C++ 中, IID_PPV_ARGS
可以使用宏而不是显式 IID 和强制转换指针: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));
。
这通常用于创建方法和 QueryInterface。 有关详细信息,请参阅 combaseapi.h 。
创建对象时,系统会分配必要的内存资源。 当不再需要对象时,应将其销毁。 系统可以将该内存用于其他目的。 使用 C++ 对象,可以在在该级别操作时直接使用 new
和 delete
运算符控制对象的生存期,或者仅使用堆栈和范围生存期。 COM 不允许你直接创建或销毁对象。 此设计的原因是,同一对象可能由应用程序的多个部分使用,或者在某些情况下,由多个应用程序使用。 如果其中一个引用要销毁对象,则其他引用将变为无效。 相反,COM 使用引用计数系统来控制对象的生存期。
对象的引用计数是请求其一个接口的次数。 每次请求接口时,引用计数都会递增。 当不再需要该接口时,应用程序会释放该接口,从而减少引用计数。 只要引用计数大于零,对象就保留在内存中。 当引用计数达到零时,对象会销毁自身。 无需了解对象的引用计数。 只要正确获取并释放对象的接口,该对象将具有相应的生存期。
正确处理引用计数是 COM 编程的关键部分。 否则,很容易造成内存泄漏或崩溃。 COM 程序员最常犯的错误之一是无法发布接口。 发生这种情况时,引用计数永远不会达到零,并且对象将无限期地保留在内存中。
备注
Direct3D 10 或更高版本已稍微修改对象的生存期规则。 具体而言,派生自 ID3DxxDeviceChild 的对象永远不会超过其父设备 (也就是说,如果拥有 的 ID3DxxDevice 命中 0 refcount,则所有子对象都立即无效,) 。 此外,使用 Set 方法将对象绑定到呈现管道时,这些引用不会增加引用计数, (也就是说,它们是弱引用) 。 实际上,最好通过确保在释放设备之前完全释放所有设备子对象来解决此问题。
每当获取新的接口指针时,必须通过调用 IUnknown::AddRef 来递增引用计数。 但是,应用程序通常不需要调用此方法。 如果通过调用对象创建方法或调用 IUnknown::QueryInterface 获取接口指针,则对象会自动递增引用计数。 但是,如果以其他某种方式(例如复制现有指针)创建接口指针,则必须显式调用 IUnknown::AddRef。 否则,释放原始接口指针时,对象可能会被销毁,即使你仍需要使用指针的副本。
无论你还是对象是否递增引用计数,都必须释放所有接口指针。 如果不再需要接口指针,请调用 IUnknown::Release 以递减引用计数。 一种常见做法是初始化指向 nullptr
的所有接口指针,然后在释放它们时将其设置回 。nullptr
该约定允许你在清理代码中测试所有接口指针。 那些未 nullptr
处于活动状态的那些项,你需要先释放它们,然后再终止应用程序。
以下代码片段扩展了前面所示的示例,以说明如何处理引用计数。
HRESULT hr = S_OK;
IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
hWnd,
&swapChainDesc,
nullptr,
nullptr,
&pDXGISwapChain1));
if (FAILED(hr)) return hr;
IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;
IDXGISwapChain3 * pDXGISwapChain3Copy = nullptr;
// Make a copy of the IDXGISwapChain3 interface pointer.
// Call AddRef to increment the reference count and to ensure that
// the object is not destroyed prematurely.
pDXGISwapChain3Copy = pDXGISwapChain3;
pDXGISwapChain3Copy->AddRef();
...
// Cleanup code. Check to see whether the pointers are still active.
// If they are, then call Release to release the interface.
if (pDXGISwapChain1 != nullptr)
{
pDXGISwapChain1->Release();
pDXGISwapChain1 = nullptr;
}
if (pDXGISwapChain3 != nullptr)
{
pDXGISwapChain3->Release();
pDXGISwapChain3 = nullptr;
}
if (pDXGISwapChain3Copy != nullptr)
{
pDXGISwapChain3Copy->Release();
pDXGISwapChain3Copy = nullptr;
}
到目前为止,代码已显式调用 Release
和 AddRef
以使用 IUnknown 方法维护引用计数。 此模式要求程序员努力记住在所有可能的代码路径中正确维护计数。 这可能会导致复杂的错误处理,并且启用 C++ 异常处理可能特别难以实现。 使用 C++ 的更好解决方案是使用 智能指针。
winrt::com_ptr 是由 C++/WinRT 语言投影提供的智能指针。 这是建议用于 UWP 应用的 COM 智能指针。 请注意,C++/WinRT 需要 C++17。
Microsoft::WRL::ComPtr 是由Windows 运行时 C++ 模板库 (WRL) 提供的智能指针。 此库是“纯”C++,因此可用于通过 C++/CX 或 C++/WinRT) 以及 Win32 桌面应用程序 (Windows 运行时应用程序。 此智能指针也适用于不支持 Windows 运行时 API 的旧版 Windows。 对于 Win32 桌面应用程序,可以使用
#include <wrl/client.h>
仅包含此类,还可以选择定义预处理器符号__WRL_CLASSIC_COM_STRICT__
。 有关详细信息,请参阅 重新访问 COM 智能指针。CComPtr 是由 活动模板库 (ATL) 提供的智能指针。 Microsoft::WRL::ComPtr 是此实现的较新版本,可解决许多微妙的使用问题,因此不建议在新项目中使用此智能指针。 有关详细信息,请参阅 如何创建和使用 CComPtr 和 CComQIPtr。
若要将活动模板库 (ATL) 与 DirectX 9 配合使用,必须重新定义接口以确保 ATL 兼容性。 这样,就可以正确使用 CComQIPtr 类来获取指向接口的指针。
如果不重新定义 ATL 的接口,你将知道,因为会看到以下错误消息。
[...]\atlmfc\include\atlbase.h(4704) : error C2787: 'IDirectXFileData' : no GUID has been associated with this object
以下代码示例演示如何定义 IDirectXFileData 接口。
// Explicit declaration
struct __declspec(uuid("{3D82AB44-62DA-11CF-AB39-0020AF71E433}")) IDirectXFileData;
// Macro method
#define RT_IID(iid_, name_) struct __declspec(uuid(iid_)) name_
RT_IID("{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}", IDirectXFileData);
重新定义接口后,必须使用 Attach 方法将接口附加到 ::D irect3DCreate9 返回的接口指针。 否则,智能指针类将无法正确释放 IDirect3D9 接口。
CComPtr 类在创建对象并将接口分配给 CComPtr 类时,在接口指针上内部调用 IUnknown::AddRef。 若要避免泄漏接口指针,请不要在从 ::D irect3DCreate9 返回的接口上调用 **IUnknown::AddRef。
以下代码在不调用 IUnknown::AddRef 的情况下正确释放接口。
CComPtr<IDirect3D9> d3d;
d3d.Attach(::Direct3DCreate9(D3D_SDK_VERSION));
使用前面的代码。 请勿使用以下代码,该代码调用 IUnknown::AddRef 后跟 IUnknown::Release,并且不会释放 由 ::D irect3DCreate9 添加的引用。
CComPtr<IDirect3D9> d3d = ::Direct3DCreate9(D3D_SDK_VERSION);
请注意,这是 Direct3D 9 中唯一必须以这种方式使用 Attach 方法的位置。
有关 CComPTR 和 CComQIPtr 类的详细信息,请参阅它们在头文件中的定义 Atlbase.h
。