管理引用计数的规则

使用引用计数管理对象的生存期允许多个客户端获取和释放对单个对象的访问,而无需相互协调来管理对象的生存期。 只要客户端对象符合某些使用规则,该对象实际上提供此管理。 这些规则指定如何管理对象之间的引用。 (COM 不指定对象的内部实现,尽管这些规则是对象内策略的合理起点。

从概念上讲,接口指针可以视为驻留在包含接口指针的所有内部计算状态的指针变量中。 这包括内存位置和内部处理器寄存器中的变量以及程序员生成的变量和编译器生成的变量。 指针变量的赋值或初始化涉及创建已存在的指针的新副本。 如果某个变量中有一个指针副本(赋值/初始化中使用的值),则现在有两个。 对指针变量的赋值会破坏变量中当前存在的指针副本,变量本身的销毁也一样。 (也就是说,找到变量的范围(如堆栈帧)将被销毁。)

从 COM 客户端的角度来看,始终为每个接口执行引用计数。 客户端不应假设对象为所有接口使用相同的计数器。

默认情况是必须为接口指针的每个新副本调用 AddRef,并且必须为接口指针的每个销毁调用 Release,除非以下规则以其他方式允许:

  • 函数的输入输出参数。 调用方必须对参数调用 AddRef,因为当 out 值存储在其上时,它将在实现代码中释放(调用 Release)。
  • 提取全局变量。 从全局变量中指针的现有副本创建接口指针的本地副本时,必须在本地副本上调用 AddRef,因为另一个函数可能会在本地副本仍然有效时销毁全局变量中的副本。
  • 从“thin air”合成新指针。使用特殊内部知识(而不是从其他源获取)合成接口指针的函数最初必须在新合成的指针上调用 AddRef。 此类例程的重要示例包括实例创建例程、QueryInterface 实现等。
  • 检索内部存储指针的副本。 当函数检索被调用对象在内部存储的指针的副本时,该对象的代码必须在函数返回之前对指针调用 AddRef。 检索指针后,原始对象无法确定其生存期与指针内部存储副本生存期的关系。

默认情况的唯一例外是管理代码知道指向对象上同一接口的指针的两个或多个副本的生存期的关系,并且只需通过允许对象的引用计数变为零来确保对象不会被销毁。 通常有两种情况,如下所示:

  • 指针的一个副本已存在并且随后创建第二个副本,然后在第一个副本仍然存在时销毁,这时可以省略对第二个副本的 AddRefRelease 的调用。
  • 指针的一个副本存在并且创建第二个副本,然后在第二个副本创建好之前销毁第一个副本,这时可以省略对第二个副本调用 AddRef 和对第一个副本调用 Release

以下是这些情况的具体示例,前两个尤其常见:

  • 函数的输入参数。 作为参数传递给函数的接口指针的副本的生存期嵌套在用于初始化值的指针的生存期中,因此无需对参数使用单独的引用计数。
  • 从函数中传出参数,包括返回值。 若要设置 out 参数,函数必须具有接口指针的稳定副本。 返回时,调用方负责释放指针。 因此,out 参数不需要单独的引用计数。
  • 局部变量。 方法实现控制堆栈帧上分配的每个指针变量的生存期,并可用于确定如何省略冗余 AddRef/Release 对。
  • 反向指针。 某些数据结构包含两个对象,每个对象都有指向另一个对象的指针。 如果已知第一个对象的生存期包含第二个对象的生存期,则无需对第二个对象指向第一个对象的指针进行引用计数。 通常,避免此周期对于维护适当的解除分配行为非常重要。 但是,由于处理远程处理的操作系统部分无法知道此关系,因此,使用不计其数的指针应非常谨慎。 因此,在几乎所有情况下,让反向指针看到第一个指针的第二个“friend”对象(从而避免循环)是首选的解决方案。 例如,COM 的可连接对象体系结构使用此方法。

实现或使用引用计数对象时,人工引用计数可能会很有用,这可以保证对象在处理函数期间保持稳定性。 在实现接口的方法时,可以调用有机会将引用计数递减到对象的函数,从而导致对象的过早释放和实现失败。 避免这种情况的可靠方法是在开始方法实现时插入对 AddRef 的调用,并在方法返回前将其与对 Release 的调用配对。

在某些情况下,AddRefRelease 的返回值可能不稳定,不应对其产生依赖;它们应仅用于调试或诊断目的。

通过引用计数管理对象生存期