COM 引用计数问题

COM Reference Counting Questions

我正在编写使用 COM 接口的代码。我的代码基于我在网上找到的示例。在这种情况下,我不想使用智能指针,因为我想了解 COM 的基础知识,而不仅仅是让智能指针 class 为我完成所有工作。

为了提出我的问题,假设我有一个 class 类似于以下内容:

public class TestClass
{
    private:
        IUnknown *m_pUnknown;

    public:
        TestClass();
        void AssignValue();
}

TestClass::TestClass()
{
    m_pUnknown = NULL;
}

void TestClass::AssignValue()
{
    IUnknown *pUnknown  = NULL;

    //Assign value to pUnknown here - not relevant to my questions

    m_pUnknown = pUnknown;

    pUnknown->Release();
}

现在谈谈我的具体问题。

1) 我看到的示例在初始化值时不使用 AddRef(),例如在 class 构造函数中。当 COM 指针第一次被赋值时,AddRef() 是否在幕后发生 "automatically"?

2)虽然我的代码示例没有显示出来,但我的理解是在AssignValue()方法中,当你赋第二个值来覆盖pUnknown的值(原来设置在class 构造函数),Release() 被自动调用。将新值分配给 pUnknown 后,其引用计数为零。我需要在重新分配后立即调用 pUnknown->AddRef()。我的理解正确吗?

注意:我假设我们在这里为简单起见忽略了异常。如果这是真的,您会希望使用智能指针来帮助在出现异常时保持正常。同样,我不担心正确复制或销毁您的示例 class 或多线程的实例。 (你的原始指针不能像你想象的那样简单地从不同的线程使用。)

首先,您需要对 COM 进行任何必要的调用。任何事情都可能在幕后发生的唯一方法是 "automatically" 如果您使用智能指针来执行它们。

1) 您提到的示例必须从某个地方获取它们的 COM 接口指针。这将通过进行 COM 调用,例如 CoCreateInstance() 和 QueryInterface()。这些调用会传递原始指针的地址并将该原始指针设置为适当的值。如果它们也没有隐式添加引用,则引用计数可能为 0,并且 COM 可能会在您的程序对其执行任何操作之前删除关联的 COM 对象。因此,此类 COM 调用必须包含代表您的隐式 AddRef()。您有责任让 Release() 与您通过这些其他调用之一发起的隐式 AddRef() 相匹配。

2a) 原始指针是原始指针。在您安排将它​​们设置为有效值之前,它们的值是垃圾。特别是,给一个值赋值不会自动神奇地调用一个函数。分配给接口的原始指针不会调用 Release() - 您需要在适当的时间执行此操作。在您的 post 中,您似乎是 "overwriting" 一个之前被设置为 NULL 的原始指针,因此图片中没有现有的 COM 接口实例。不存在的东西上不能有 AddRef(),不存在的东西上不能有 Release()。

2b)

您在示例中的注释中指出的一些代码非常相关,但很容易推断出来。您有一个本地原始指针变量 pUnknown。在缺少的代码中,您大概使用了一个获取接口指针的 COM 调用,隐式地 AddRefs 它,并用适当的值填充您的原始指针以使用它。这使您在完成后负责一个相应的 Release()。

接下来,您使用相同的值设置成员原始指针变量 (m_pUnknown)。根据此成员变量以前的使用情况,您可能需要在执行此操作之前使用其以前的值调用 Release()。

由于 1 次隐式 AddRef() 调用,您现在有 2 个原始指针设置为使用此 COM 接口实例的值并负责一个 Release()。有两种方法可以解决这个问题,但都不是您样本中的方法。

第一个、最直接、最正确的方法(其他人已经正确指出并且我在这个答案的第一个版本中跳过了)是每个指针一个 AddRef() 和一个 Release()。 m_pUnknown,您的代码缺少此内容。这需要在分配给 m_pUnknown 之后立即添加 m_pUnknown->AddRef() 并在使用来自 m_pUnknown 的当前接口指针完成后对 Release() "someplace else" 进行 1 次相应调用]. "someplace else" 在您的代码中的一个常见候选者是在 class 析构函数中。

第二种方法更有效,但不太明显。即使您决定不使用它,您也可能会看到它,所以至少应该知道它。按照第一种方法,您将获得代码序列:

m_pUnknown = pUnknown;
m_pUnknown->AddRef();
pUnknown->Release();

由于 pUnknown 和 m_pUnknown 在这里设置相同,Release() 立即撤消 AddRef()。在这种情况下,省略这个 AddRef/Release 对是引用计数中立的,并且可以节省 2 次到 COM 的往返。我对此的心理模型是将接口和引用计数从一个指针传输到另一个指针。 (使用智能指针,它看起来像 newPtr.Attach( oldPtr.Detach() ); )这种方法让您看到 original/not 显示为隐式 AddRef()并且需要添加与第一个替代方案相同的 m_pUnknown->Release() "someplace else"。

在任何一种方法中,您都将 AddRefs(隐式或显式)与每个接口的 Releases 完全匹配,并且在完成接口之前永远不会达到 0 引用计数。一旦命中 0,就不会尝试使用指针中的值。

首先,我很抱歉。为了清晰起见,我试图简化我的代码,结果被误导了。但是,我相信我的问题已得到解答。可以的话我总结一下。

1) 任何分配了 NULL 以外的值的 COM 对象都需要紧跟 AddRef(),除非 AddRef() 被隐式处理(某些情况下就是这种情况) Windows API 调用)。

2) 如果 "before" 值不是 NULL,任何对 COM 指针的值重新分配必须立即由 Release() 进行。 AddRef() 然后将如#1 中所述需要。

3) 任何其值需要在其当前范围之外保留的 COM 变量都要求在退出其所述范围时其引用计数至少为 1。这可能意味着需要 AddRef()

这是一个公平的总结吗?我错过了什么吗?

Avi Berger 已经发布了一个很好的答案,但这里以另一种方式陈述同样的事情,以防它有助于理解。

在 COM 中,引用计数是在 COM 对象内完成的。 COM 运行时将销毁并释放引用计数达到 0 的对象。 (从计数达到 0 开始,这可能会延迟一段时间)。

其他一切都是惯例。 C++ COM 程序员之间通常的约定是原始接口指针应该被视为拥有指针。这个概念意味着任何时候指针指向 COM 对象时,指针 拥有 该对象。

使用这个术语,对象在任何时候可能有多个所有者,当没有人拥有它时,对象将被销毁。

但是,C++ 中的原始指针没有内置的所有权语义。因此您必须通过调用函数自己实现它:

  • 当接口指针获得对象的所有权时,在接口指针上调用 AddRef。 (您需要了解哪些 Windows API 函数或其他库函数已经执行此操作,以避免重复执行此操作)
  • 当接口指针即将停止拥有一个对象时,在接口指针上调用 Release

智能指针的好处是当接口指针停止拥有对象时,它们使您不可能忘记调用 Release。这包括以下情况:

  • 指针超出范围。
  • 使用赋值运算符使指针停止指向对象。

所以,看看你的示例代码。你有指针m_pUnknown。你想让这个指针获得对象的所有权,所以代码应该是:

m_pUnknown = pUnknown;
m_pUnknown->AddRef();

您还需要向 class 析构函数和 class 赋值运算符添加代码以调用 m_pUnknown->Release()。我 非常强烈 建议将这些调用包装在尽可能小的 class 中(也就是说,编写您自己的智能指针并使 TestClass 将该智能指针作为成员多变的)。当然,假设您出于教学原因不想使用现有的 COM 智能指针 class。

调用pUnknown->Release();是正确的,因为pUnknown当前拥有该对象,而指针即将停止拥有该对象,因为它会在函数块结束时被销毁。


您可能会发现可以删除行 m_pUnknown->AddRef()pUnknown->Release()。该代码的行为将完全相同。但是,最好遵循上面概述的约定。遵守约定有助于您自己避免错误,也有助于其他编码人员理解您的代码。

换句话说,通常的约定是将指针视为具有 01 的引用计数,即使引用计数实际上并未以这种方式实现.