COM参考文献计数问题



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

为了构造我的问题,让我们假设我有一个类似于下面的类:

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()的示例,例如在类构造函数中。AddRef()发生"自动"在幕后,当一个COM指针首次分配一个值?

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

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

首先,您需要对COM进行必要的调用。只有当你使用智能指针时,才有可能在幕后"自动"发生任何事情。

1)你提到的例子必须从某处获得他们的COM接口指针。这将是通过COM调用,例如,CoCreateInstance()和QueryInterface()。这些调用传递原始指针的地址,并将原始指针设置为适当的值。如果它们也没有被隐式地寻址,引用计数可能为0,COM可以在你的程序对它做任何事情之前删除关联的COM对象。所以这样的COM调用必须代表你包含一个隐式的AddRef()。你要负责Release()来匹配这个隐式的AddRef(),这个隐式的AddRef()是你用这些其他调用之一发起的。

2a)原始指针就是原始指针。它们的值是垃圾,除非您将它们设置为有效的值。特别是,将值赋给一个函数不会自动神奇地调用一个函数。给一个指向接口的原始指针赋值不会调用Release()——你需要在适当的时候调用Release()。在你的帖子中,似乎你正在"覆盖"一个以前被设置为NULL的原始指针,因此在图片中没有现有的COM接口实例。不可能在不存在的东西上有AddRef(),也不能在不存在的东西上有Release()。

2 b)

您在示例中通过注释指出的一些代码非常相关,但很容易推断出来。您有一个本地原始指针变量pUnknown。在缺席的代码中,您可能会使用COM调用来获取接口指针,隐式地对其进行addresfs,并使用正确的值填充原始指针以使用它。当您完成Release()时,这给了您一个相应Release()的责任。

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

你现在有2个原始指针被设置为与这个COM接口实例一起工作的值,并且由于1个隐式AddRef()调用而负责一个Release()。有两种方法可以处理这个问题,但这两种方法都不是您示例中的方法。

第一个,最直接,最正确的方法(其他人已经正确指出&我跳过了这个答案的第一个版本)是每个指针一个AddRef()和一个Release()。你的代码缺少这个m_pUnknown。这需要在m_pUnknown赋值后立即添加m_pUnknown->AddRef(),并相应地调用Release()"其他地方",当你完成使用当前接口指针从m_pUnknown。在代码中的"其他地方"通常是在类析构函数中。

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

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

由于此处的pUnknown和m_pUnknown设置相同,Release()会立即撤销AddRef()。在这种情况下,省略这个addresf/Release对是引用计数中立的,并且节省了到COM的两次往返。我的心智模型是将接口和引用计数从一个指针转移到另一个指针。(使用智能指针,它看起来像newPtr。这种方法留给您原始/未显示的隐式AddRef(),并且需要添加相同的m_pUnknown->Release()" somewhere else"

无论哪种方法,您都可以将每个接口的AddRefs(隐式或显式)与release精确匹配,并且在完成接口之前永远不会达到0引用计数。一旦达到0,就不能尝试使用指针中的值。

Avi Berger已经发布了一个很好的答案,但这里是同样的事情的另一种说法,如果它有助于理解。

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

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

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

然而,c++中的原始指针没有内置的所有权语义。所以你必须通过调用函数来实现它:

  • 在接口指针上调用AddRef,当该指针获得对象的所有权时。(你需要知道哪些Windows API函数或其他库函数已经这样做了,以避免你做两次)
  • 当接口指针即将停止拥有一个对象时,调用Release

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

  • 指针超出作用域
  • 使用赋值操作符使指针停止指向对象。

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

m_pUnknown = pUnknown;
m_pUnknown->AddRef();

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

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


您可能会注意到可以删除m_pUnknown->AddRef()pUnknown->Release()这两行。代码的行为将完全相同。但是,最好遵循上面概述的约定。遵守约定可以帮助你避免错误,也可以帮助其他程序员理解你的代码。

换句话说,通常的约定是认为指针的引用计数为01,尽管引用计数实际上并不是这样实现的。

首先,我道歉。我为了清晰而简化代码的尝试被证明是错误的。不过,我相信我的问题得到了解答。如果可以的话,我将总结一下。

1)任何COM对象被赋值NULL以外的值需要立即后跟AddRef(),除非AddRef()被隐式处理(就像一些Windows API调用的情况一样)。

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

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

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

最新更新