第四章:引用计数

C++类的头文件表示了这个类所提供的服务及函数,但这一点同COM组件是非常不同的。同C++类比较来,COM组件更具隐蔽性。客户必须向组件提出一系列的问题类判断接口功能。客户无法像c++一样由头文件直接得知它所提供的功能而得知一个COM组件的全部细节,从而也就无法知道一个组件的实现细节。
这种不让客户了解组件身份特征的能力可以使得客户能够尽可能少地受组件变化的影响。但由于客户对组件知道的仅仅是其接口,因此客户也就无法直接控制整个组件的生命期。在这一章中,我们就来介绍一种通过直接控制单个接口生命期来间接控制组件生命期的方法。

生命期控制

客户为什么不应直接控制组件的生命期?在客户的代码中,可能会有若干个指向此组件接口的指针,客户的某一部分可能会使用接口IX,而另外一部分则使用接口IY。在使用完IX之后可能还希望继续使用IY。这种情况下,当使用完一个接口后接着仍要使用另外一个接口时,肯定是不能将组件释放掉的。由于很难知道两个接口指针是否指向同一组件,因此决定何时可以安全地释放一个组件将是极为复杂的。得知两个接口指针是否是指向同一对象的唯一方法是查询这两个接口的IUnknown,然后对结果进行比较。当程序越来越复杂时,决定何时可以释放一个组件就会越来越复杂的。解决这个问题的最为简单的方法是在整个程序的运行期内一直装载相应的组件,但这种方法的效率可以想见不是很高。
所以就有如下的方法:我们可以通知组件何时需要使用它的某个接口以及何时用完此接口,而不是直接将接口删除。一般我们总能精确地知道何时开始使用一个接口,并且在大多数情况下,可以精确地知道何时将用完此接口。但正如前面所看到的,决定何时将用完整个组件并不是一件容易的事情。因此,当用完某个接口后给组件发出指示将比在使用完整个组件更有意义。对组件的释放可以由组件在客户使用完其各个接口之后自己完成。
IUnknown的另外两个成员函数AddRef和Release的作用就是给客户提供一种让它指示何时处理完一个接口的手段。前一章中我们曾经给出过IUnknown的定义。为清楚起见,在此我们再次给出此定义。

interface IUnknown
{
	virtual HRESULT _stdcall QueryInterface(const IID& iid,void* * ppv) = 0;
	virtual ULONG _stdcall AddRef() = 0;
	virtual ULONG _stdcall Release() = 0;
}

本章我们将讨论AddRef和Release如何可以实现让组件自己来管理其生命期,以及客户又如何可以借助于这两个函数以使它们只需关心接口。
首先,我们将简要介绍引用计数的概念,然后我们再讨论客户可以如何使用AddRef和Release。在熟悉了AddRef 和Release之后,我们将实现一个组件中的这两个成员。最后我们将讨论在什么情况下可以省去对 AddRef和Release的调用以提高性能,并将我们所讨论的结果总结成一些简单的规则。

引用计数简介

AddRef和Release实现的是一种名为引用计数的内存管理技术。引用计数是使组件能够自己将自己删除的最简单同时也是效率最高的方法。COM组件将维护一个称作是引用计数的数值。当客户从组件取得一个接口时,此引用计数值将增1。当客户使用完某个接口后,组件的引用计数值将减1。当引用计数值为0时,组件即可将自己从内存中删除。当创建某个已有接口的另外一个引用时,客户也将会增大相应组件的引用计数值。读者大概已经可以猜出,AddRef可增大引用计数值,而Release将减少这一值。为正确地使用引用计数,读者需要了解以下三条简单的规则:

  1. 在返回之前调用AddRef:
    对于那些返回接口指针的函数,在返回之前应用相应的指针调用AddRef。这些函数包括 QueryInterfaceCreateInstance。这样当客户从这种函数得到一个接口后,它将无需调用AddRef

  2. 使用完接口之后调用Release:
    在使用完某个接口之后应调用此接口的Release函数

  3. 在赋值之后调用AddRe:
    在将一个接口指针赋给另外一个接口指针时,应调用AddRef。换句话说,在建立接口的另外一个引用之后应增加相应组件的引用计数

这些就是关于引用计数的三条简单规则。下面我们来看两个例子:
第一个例子将说明前面两条规则。在下面的代码中,我们建立了一个组件并取得了一个指向接口IX的指针。
由于在CreateInstance 和QueryInterface 中已经调用了AddRef,因此这里不再调用它。但对于 Createlnstance 所返回的IUnknown接口以及Querylnterface 所返回的IX接口,却都需要调用 Release。

// Create a new component.创建一个新组件
IUnknown * pIUnknown = CreateInstance();
// Get interface IX.获取接口IX。
IX * pIX = NULL;
HRESULT hr =pIUnknown-> QueryInterface(IID_IX, (void* *)&pIX);
if (SUCCEEDED(hr))
{
	pIX->Fx();// Use interface IX.使用接口IX
	pIX -> Release() ;// Done with IX.完成了IX
}
pIUnknown -> Release() ;// Done with IUnknown.完成IUnknown

在上面的例子中,实际上在调用完Querylnterface之后,对IUnknown接口的使用也就结束了。所以需要直接将IUnknown释放。

// Create a new component.创建一个新组件
IUnknown * pIUnknown = CreateInstance() ;
// Get interface IX.获取接口IX。
IX * pIX = NULL;
HRESULT hr = pIUnknown-> QueryInterface(IID_IX, (void* *)&pIX);
// Done with IUnknown.完成IUnknown。
pIUnknown -> Release();
// Use IX if we succeeded in getting it.使用IX,如果我们成功地得到它。
if (SUCCEEDED(hr))
{
	pIX->Fx();// Use interface IX.使用接口IX。
	pIX-> Release(); //Done with IX.完成了IX。
}

第三条规则是许多人都会忘记的。在下面的代码段中,我们建立了对接口IX的另外一个引用。一般而言,每当复制一个接口的指针时,都应像前面的第三条规则所说的那样相应地增加引用计数。

IUnknown* pIUnknown = CreateInstance() ;
IX * pIX = NULL;
HRESULT hr =
pIUnknown-> QueryInterface(IID_IX,(void* *)&pIX) ;
pIUnknown-> Release();
if (SUCCEEDED(hr))
{
	pIX->Fx();// Use interface IX.使用接口IX。
	IX* pIX2 = pIX ;// Make a copy of pIX.复制一份pIX。
	pIX2-> AddRef() ;// Increment the reference count.增加引用计数。
	pIX2->Fx() ;
	// Do something with pIX2.用pIX2做点什么
	pIX2->Release(); // Done with IXZ.完成了IXZ。
	pIX-> Release() ; // Done with IX.完成了IX
}

看完上述代码之后,读者的第一个反应也许是:在本例中必须对plX2调用AddRef和Release吗?或怎么才能记住在每次复制指针时都去调用AddRef和Release呢?某些读者可能会同时产生这样两个疑问。对于第一个问题的答案是:不,在上例中并不一定要对plX2调用AddRef和Release。在像本例这样一个简单的例子中,很容易看出通过pIX2增大然后又减少引用计数并不是必要的。因为plX2的生命期和plX的生命期是一样的。在本章的后面我们将介绍一些如何优化引用计数的指导性原则。但是一般而言,在给某个接口指定一个别名时,都应调用AddRef。在实际程序中,很难决定某些没有加上的AddRef及Release调用到底是优化还是程序中的错误。由于很多人经常花费许多天的时间以试图找出程序中不正确计数的原因,因此,可以肯定地讲引用计数引起的问题并不是那么容易就能够找出来的。
但在第10章中我们将看到,智能指针类可以把引用计数完全封装起来。在使用智能指针时,程序员几乎可以完全将引用计数的问题忘掉。我们知道当客户调用QueryInterface时,它实际上是告诉相应的组件它想要使用某个接口。在上面的代码中我们看到,QueryInterface 将调用所请求接口上的AddRef。当客户用完某个接口之后,它将调用此接口上的Release。只要组件的引用计数值不为零,那么它将一直处于内存中。当引用计数为零时,组件将把它从内存中删除。

引用计数接口

在客户看来,引用计数是处于接口级上而不是组件级上的。客户并不了解组件的全部,它看到的只是组件的一个个接口。因此在客户看来,每一个接口都有一个引用计数。虽然从客户的角度看来是接口而非组件被记录下其引用计数(参见图4-1),但从实现的角度来看,谁的引用计数被记录下来实际上并没有什么关系。**组件可以对其每一个接口分别维护一个引用计数,也可以对整个组件维护单个的引用计数。**只要能够使客户相信组件将记录每个接口本身维护引用计数值,使用何种实现方法是无关紧要的。由于在组件的实现中可能会选择对每一个接口分别维护一个引用计数,因此客户不应作相反的假设。
在这里插入图片描述

对于客户而言,每一个接口都分别维护一个引用计数意味着客户应该对它将要使用的那个指针调用AddRef,而不是其他的什么指针。对于使用完了的指针,客户应调用其Release。例如,不要编写如下形式的代码:

IUnknown * pIUnknown = CreateInstance() ;

IX * pIX = NULL ;
pIUnknown-> QueryInterface(IID_IX, &pIX) ;
pIX->Fx();

pIX* pIX2 = pIX ;
pIUnknown-> AddRef() ;// Should be pIX2->AddRef();应该是pIX2->AddRef()

pIX2->Fx();
pIX2->Release();
plUnknown-> Release() ;// Should be pIX-> Release();
pIUnknown-> Release() ;

上面的代码假定像使用IX接口指针那样对plUnknown接口调用AddRef 和 Release。根据组件实现方式的不同,这可能会引起一些问题。
那么为什么要选择为每一个接口单独维护一个引用计数而不是针对整个组件维护引用计数呢?这上要有两个原因:一是使程序调试更为方便;另外一个原因是支持资源的按需获取。

一、程度调试

假定在程序中忘了对某个接口调用 Release,这样组件将永远不会被删除掉,因为只是在引用计数值为0时delete才会被调用。这时就需要找出接口在何时何处应该被释放掉的会相当困难。如果在只对整个组件维护一个接口的情况下就必须检查使用了此组件所提供的所有接口的代码。但若组件支持对每一个接口分别维护一个引用计数,那么可以把查找的范围限制在某个特定的接口上,在某些情况下这可以节省大量的时间。

二、资源的按需获取

在实现某个接口时可能需要大量的内存或其他资源时。对于此种情况,可以在QueryInterface的实现中,在客户请求此接口时完成资源的分配。
但若只对整个组件维护一个引用计数,组件将无法决定何时可以安全地将同此接口相关联的内存释放。但若对每一个接口分别维护一个引用计数,那么决定何时可以将此内存释放将会容易得多。
另外一种作法是(大多数情况下这种作法更优)是在另外一个单独的组件中实现此种需要大量资源的接口,然后将指向此组件的接口传给客户。这种被称作是聚合的技术将在第8章中详细讨论。

为使本书中所用的例子尽量地简单,我们将对整个组件维护单个的引用计数值。下面我们来看一下如何实现这一点。

AddRef 和Release 的实现

AddRef 和Release的实现实际上是非常简单的。实际上可以通过增大和减少某个数值而实现之。例如下面的代码给出了一个实现的例子。

ULONG_stdcall AddRef()
{
	return ++m_cRef;
}
ULONG _stdcall Release()
{
	if ( --m_cRef == 0)
	{
		delete this;
		return 0;
	}
	return m_ cRef;
}

AddRef将记录有组件引用计数值的成员变量m_cRef增大,而Release则将此值减少,并在此值为0时将组件删除。在许多情况下,许多程序员可能会使用Win32的函数InterlockedIncrementInterlockedDecrement来实现AddRef和Release。这两个函数可以确保在同一时刻只会有同一个线程来访问成员变量。根据COM对象所使用的线程模型的不同,多线程可能会引起一些问题。关于线程,我们将在第12章中讨论。

ULONG _stdcall AddRef()
{
	return InterlockedIncrement(&m_cRef) ;
}
ULONG _stdcall Release()
{
	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this ;
		return 0;
	}
	return m_cRef;
}

另外要注意的是AddRef和Release的返回值没有什么意义,只是在程序调试中才可能会用得上。客户不应将此值当成是组件或其接口的精确引用数。若读者仔细阅读过第3章中给出的示例代码,可以发现在QueryInterface和 Createlnstance 中都使用了AddRef。

HRESULT_stdcall CA :: QueryInterface(const IID& iid, void* * ppv)
{
	if (iid == IID_IUnknown)
	{
		*ppv = static_cast<IX*>(this);
	}
	else if (iid == IID_ IX)
	{
		*ppv = static_cast < IX* >(this);
	}
	else if (iid == IID_IY)
	{
		*ppv = static_cast<IY*>(this);
	}
	else
	{
		*ppv = NULL;
	return E_NOINTERFACE;
	}
	static_cast <IUnknown *> (* ppv)-> AddRef();
	return S_OK;
}
IUnknown* CreateInstance()
{
	IUnknown * pI = *ppv = static_cast<IX* >(new CA);
	pl-> AddRef();
	return pI;
}

这样当建立一个新组件时,实际上也将建立一个对此组件的引用。因此创建组件时,在将指针返回给客户之前,应该增大组件的引用计数值。这使得程序员可以不必在调用CreateInstance 或 QueryInterface 之后记着去调用AddRef。同时,在某些情况下也可以省去对AddRef和Release的调用。

完整代码


// RefCount.cpp
// To compile, use / cl RefCount.cpp uuip.lib

#include <iostream>
#include <objbase.h>
using namespace std;
void trace(const char* msg)
{
	cout << msg << endl;
}
//Forvard references for GUIDs
extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;

//Interfacas
interface IX : IUnknown
{
	virtual void _stdcall Fx() = 0;
};


interface IY : IUnknown
{
	virtual void _stdcall Fy() = 0;
};
	

interface IZ : IUnknown
{
	virtual void _stdcall Fz() = 0;
};
	

//Component
class CA : public IX, public IY
{
	// IUnknown implementation IUnknown实现
	virtual HRESULT _stdcall QueryInterface(const IID& iid, void** ppv);
	virtual ULONG _stdcall AddRef();
	virtual ULONG _stdcall Release();
	// Interface IX implementation 接口IX实现
	virtual void _stdcall Fx()
	{
		cout << "Use interface IX(使用接口IX)" << endl;
	}
	//Interface IY implementation 接口IY实现
	virtual void _stdcall Fy()
	{
		cout << "Use interface IY(使用接口IY)" << endl;
	}
public:
	CA() :m_cRef(0) {}
	~CA() 
	{
		trace("CA:	Destroy self.(数据自毁)");
	}

private:
	long m_cRef;
};
HRESULT _stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
	if (iid == IID_IUnknown)
	{ 
		trace("CA QI:  Return pointer to IUnknown.(返回指向IUnknown的指针)");
		*ppv = static_cast <IX*>(this);
	}
	else if (iid == IID_IX)
	{
		trace("CA QI:  Return pointer to IX.(返回指向IX的指针)");
		*ppv = static_cast <IX*>(this); 
	}
	else if (iid == IID_IY)
	{
		trace("CA QI:  Return pointer to IY.(返回指向IY的指针)");
		*ppv = static_cast<IY*>(this);
	}
	else
	{
		trace("CA QI: Interface not supported.(不支持接口)");
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	reinterpret_cast <IUnknown*>(*ppv)->AddRef();
	return S_OK;
}
ULONG _stdcall CA::AddRef()
{
	cout << "CA:	AddRef = " << m_cRef + 1 << '.' << endl;
	return InterlockedIncrement(&m_cRef);
}

ULONG _stdcall CA::Release()
{
	cout << "CA:	Release = " << m_cRef - 1 << '.' << endl;

	if (InterlockedDecrement(&m_cRef) == 0)
	{
		delete this;
		return 0;
	}
	return m_cRef;
}

// Creation function

IUnknown* CreateInstance()
{
	IUnknown* pI = static_cast <IX*>(new CA);
	pI->AddRef();
	return pI;
}

// IIDS
//{32bb8320-b41b-1icf-a6bb-0080c7b2d682}
const IID IID_IX =
{ 0x32bb8320, 0xb41b, 0x11cf,
{0xa6,0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };
//{32bb8321-b41b-11cf-a6bb-0080c7b2d682}
const IID IID_IY =
{ 0x32bb8322, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };
//{32bb8322-b41b-11cf-a6bb-0080c7b2d682}
const IID IID_IZ =
{ 0x32bb8322, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

// Client

int main()
{
	HRESULT hr;
	trace("Client: Get an IUnknown pointer.(获取一个IUnknown指针)");
	IUnknown* pIUnknown = CreateInstance();
	trace("Client: Get interface IX.(获取接口IX)");
	IX* pIX = NULL;
	hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
	if (SUCCEEDED(hr))
	{
		trace("Client. Succeeded getting IX.(成功获得IX)");	
		pIX->Fx();// Use interface IX.
		pIX->Release();
	}
	trace("client: Get interface IY.(获取接口IY)");
	IY* pIY = NULL;
	hr = pIUnknown->QueryInterface(IID_IY, (void**)&pIY);
	if (SUCCEEDED(hr))
	{
		trace("Client: Succeeded getting IY.(成功获得IY)");
		pIY->Fy();	// Use interface IY.
		pIY->Release();
	}
	trace("Client: Ask for an unsupported interface.(请求不支持的接口)");
	IZ* pIZ = NULL;
	hr = pIUnknown->QueryInterface(IID_IZ, (void**)&pIZ);
	if (SUCCEEDED(hr))
	{
		trace("client: Succeeded in getting interface IZ.(成功获取接口IZ)");
		pIZ->Fz();
		pIZ->Release();
	}
	else
	{
		trace("Client: Could not get interface IZ.(无法拿到接口IZ)");
	}
	trace("Client: Release IUnknown interface.(释放IUnknown接口)");
	pIUnknown->Release();
	return 0;
}

运行结果

Client: Get an IUnknown pointer.(获取一个IUnknown指针)
CA:     AddRef = 1.
Client: Get interface IX.(获取接口IX)
CA QI:  Return pointer to IX.(返回指向IX的指针)
CA:     AddRef = 2.
Client. Succeeded getting IX.(成功获得IX)
Use interface IX(使用接口IX)
CA:     Release = 1.
client: Get interface IY.(获取接口IY)
CA QI:  Return pointer to IY.(返回指向IY的指针)
CA:     AddRef = 2.
Client: Succeeded getting IY.(成功获得IY)
Use interface IY(使用接口IY)
CA:     Release = 1.
Client: Ask for an unsupported interface.(请求不支持的接口)
CA QI:  Return pointer to IY.(返回指向IY的指针)
CA:     AddRef = 2.
client: Succeeded in getting interface IZ.(成功获取接口IZ)
Use interface IY(使用接口IY)
CA:     Release = 1.
Client: Release IUnknown interface.(释放IUnknown接口)
CA:     Release = 0.
CA:     Destroy self.(数据自毁)

上例同第3章中给出的例子是一样的,只不过在其中加入了引用计数的功能。就组件而言,它们实现了AddRef和Release。从客户的角度来讲,唯一的不同在于加上了一些Release 调用以告知组件客户用完了各接口。另外还可以注意到,客户不再使用delete操作符了。并且在上例中,由于CreateInstance和QueryInterface用它们所返回的指针调用了AddRef,因此客户无需再去调用此函数。

何时进行引用计数

下面我们将看到在某些情况下可以省略对 AddRef和Release的调用,从而可以优化程序代码。将前面给出的关于引用计数的一些规则同此处关于优化的一些考虑结合起来,将可以得到关于引用计数一些一般性规则。

引用计数的优化

现在我们思考一个问题:是否在复制一个接口指针时都需要增大其引用计数。为说明这个问题我们先看一下下面的代码:

HRESULT hr ;
IUnknown * pIUnknown = CreateInstance() ;
IX * pIX= NULL;
hr = pIUnknown-> QueryInterface(IID_IX, (void* *)&pIX) ;
pIUnknown-> Release() ;
if(SUCCEEDED(hr))
{
	IX * pIX2 = pIX ; // Make a copy of pIX.制作pIX的副本。
	// The life of pIX2 is nested in the life of pIX.pIX2的生命嵌套在pIX的生命中。
	pIX2-> AddRef();// Increment the reference count.增加引用计数。
	pIX->Fx() ;// Use interface IX.使用接口IX。
	DIX2->Fx();// Do something with pIX2.用pIX2做点什么。
	pIX2-> Release();// Done with pIX2.pIX2完成。
	pIX->Release() ;// Done with IX.IX完成。
					// Also done with the component.组件也完成。
}

对于上面的这段代码,只有当客户将pIX释放时组件才会被从内存中删除。而客户只是在用完了pIX和plX2之后才会将pIX释放。正是由于组件只是在plX被释放之后才被从内存中删除掉,因此可以保证在plX2的生命期内相应的组件将一直在内存中。因此对pIX2并不真的需要调用AddRef和Release。
为将上例中的组件保持在内存中,plX的单个引用计数就足够了。之所以会这样,关键之处在于plX2的生命期包含在pIX的生命期内。为强调这一点,在上面的代码中我们故意增大了使用plX2的那些代码行的缩进量。图4-2图示了pIX和pIX2生命期的嵌套关系。图中各灰色条表示各接口指针及组件本身的生命期,时间是由上到下的。图的左边列出的是那些将影响各生命期的操作。水平虚线则表示出这些操作如何开始和结束各接口的生命期。
在这里插入图片描述
从图4-2中可以很容易地看出,接口plX2的生命期起于plX之后而止于pIX之前。因此pIX的引用计数将足以保证在plX2的生命期内,组件将一直被保持在内存中,若pIX2的生命期不包含在pIX的生命期内,那么就需要对pIX2进行引用计数。
例如在下面的代码中,plX2的生命期同pIX的生命期是重叠的:

IUnknown * pIUnknown = CreateInstance() ;
IX * pIX = NULL;
HRESULT hr =
pIUnknown->QueryInterface(IID_IX,(void ** )&pIX) ;
pIUnknown-> Release() ;
if(SUCCEEDED(hr))
{
	IX* pIX2=pIX;// Make a copy of pIX.
	pIX2-> AddRef() ;// pIX2 starts life.
	pIX->Fx();
	pIX->Release();// pIX ends life.
	pIX2->Fx();
	pIX2->Release();// pIX2 ends life.
					// Also done with the component.
}

在上例中,必须对pIX2调用AddRef,因为plX2释放是在pIX的释放之后。图4-3图示了这种情况。
在这里插入图片描述

在上面给出的这些简单的例子中,我们一眼就能看出是否需要进行引用计数。但实际开发过程中要找出嵌套的生命期则不是一件容易的事情。但不管怎么样,在某些情况下,如函数等,接口的生命期还是比较明显的。在下面的代码中,显然foo的生命期包含在pIX的生命期中。因此对于传递给foo的接口指针,无需调用AddRef和Release。

void foo(pIX* pIX2)
{
	pIX2->Fx();// Use interface IX.
}
void main()
{
	HRESULT hr;
	IUnknown * pIUnknown = CreateInstance() ;
	IX* pIX ;
	hr = pIUnknown-> QueryInterface(IID_IX,(void ** )&pIX) ;
	pIUnknown->Release() ;
	if(SUCCEEDED(hr))
	{
		foo(pIX) ;// Pass pIX to procedure.
		pIX-> Release();// Done with IX.
	}					// Also done with the component.
}

在函数中,无需对存在于局部变量的接口指针进行引用计数。因为局部变量的生命期同函数的生命期是一样的,因此也将包含在调用者的生命期内。但当从某个全局变量或向某个全局变量复制一个指针时,则需要对此指针进行引用计数,这是因为全局变量可以从任意函数中的任意地方被释放

从上面的讨论可以看到,为对引用计数进行优化,关键是找出那些生命期嵌套在引用同一接口的指针生命期内的接口指针。在实际的代码中,找出这种嵌套生命期可能是比较困难的。下一节列出的规则将给出在哪些地方可以省略AddRef/Release对,而不会冒太大的风险。

引用计数规则

本节给出的规则将综合前一节及本章开头给出的一些引用计数优化策略。在学习这部分内容时,应记住客户必须像每一个接口具有一个单独的引用计数值那样来处理各接口。因此,客户必须对不同的接口分别进行引用计数,即使它们的生命期是嵌套的。

一、输出参数规则

输出参数指的是给函数的调用者传回一个值的函数参数,在函数体中将设置此输出此参数的值而不会使用调用者传进来的值。从这一点上讲,输出参数的作用同函数的返问值是类似的。例如QucryInterface的第二个参数就是一个输出参数。

HRESULT QueryInterface(const IID&, void * *) ;

任何在输出参数中或作为返回值返回一个新的接口指针的函数必须对此接口指针调用AddRef。这一规则同本章开头给出的“在返回之前调用AddRef规则是一样的,只不过是换了一种提法。QueryInterface 函数的实现就遵循了这一规则,它在返回之前对接口指针调用了AddRef。我们所实现的组件的Crealelnslance函数也遵循了这一规则。

二、输入参数规则

输入参数指的是给函数传递某个值的参数。在函数体中将会使用这个值但却不会修改它或将其返叫给调用者。在C++中,输入参数实际上就是那些按值传递的参数或常量。下面的代码就是将接口指针作为输入参数的一个例子。

void foo(pIX* pIX)
{
	pIX-> Fx();
}

对传入函数的接口指针,无需调用AddRef和Release,这是因为函数的生命期嵌套在调用者的生命期内。为记住这一点,可以想象将函数的代码以内联方式展开的情形。
例如:

IX* plX = CreateInstance () ; //Automatic AddRef 自动AddRef
foo(pIX);
pIX-> Release() ;

在将foo以内联方式展开时,上面的代码将变成:

IX* pIX = CreateInstance (); //Automatic AddRef
//foo(pIX) ;
pIX->Fx(); //Expanded foo function 扩展foo函数
pIX-> Release() ;

如此将可以清楚地看出foo的生命期是嵌套在调用者的生命期之内的。

三、输入-输出参数规则

输入-输出参数同时具有输入参数及输出参数的功能。在函数体中可以使用输入-输出参数的值,然后可以对这些值进行修改并将其返回给调用者。
在函数中,对于用输入-输出参数传递进来的接口指针,必须在给它赋另外一个接口指针值之前调用其Releasee在函数返回之前,还必须对输出参数中所保存的接口指针调用AddRef
如下例所示:

vois ExchangeForCache Ptr(int i, IX* * ppIX)
{
	(*ppIX) -> Fx();// Do something with in-parameter.
	(*ppIX)-> Release() ;// Release in parameter.
	*ppIX = g_Cache[i] ;// Get cached pointer.获取缓存指针。
	(*ppIX) -> AddRef() ;// AddRef pointer.
	(*ppIX) -> Fx() ;// Do something with out-parameter.
}

四、局部变量规则

对于局部复制的接口指针,由于它们只是在函数的生命期内才存在,因此无需调用AddRef和Release。这条规则实际是输入参数规则的直接结果。在下面的例子中,plX2只是在函数foo的生命期内才存在,因此可以保证其生命期将嵌套在所传入的pIX指针的生命期内,因此无需对pIX2调用AddRef和Release。

void foo(IX* pIX)
{
	IX* pIX2= pIX ;
	pIX2->Fx() ;
}

五、全局变量规则

对于保存在全局变量中的接口指针,在将其传递给另外一个函数之前,必须调用其AddRef。由于此变量是全局性的,因此任何函数都可以通过调用其Release来终止其生命期。对于保存在成员变量中的接口指针,也应按此种方式进行处理。因为类中的任何成员函数都可以改变此种接口指针的状态。

六、不能确定时的规则

对干任何不能确定的情形,都应调用AddRef和Release对。省略AddRef和Release对的调用不会对性能或内存的分配带来什么改善,却极容易使得某个组件永远不会被从内存中释放掉。并且这样做还会使程序员在跟踪由于引用计数错误而引起的错误方面花费大量的时间。在这种情况下可以用一个测试程序来测试一下此种优化是否真的有什么效果。另外,在决定要进行优化时,应给那些没有进行引用计数的指针加上相应的注释。否则其他程序员在修改代码时,将可能会增大接口指针的生命期,从而使引用计数的优化遭到破坏。
忘记调用Release造成的错误可能比不调用AddRef造成的错误更难检测。而这一点正是C++程序员易犯的一个毛病,大多在某些情况下他们可能会使用delele操作符。在第9章中我们将看到智能指针如何将引用计数完全封装起来。

本章小结

IUnknown的成员函数使得程序员可以对接口进行完全的控制。在上一章中,我们已经看到如何通过QueryInterface来获取某个组件所支持的接口。在这一章中,我们重点讨论了如何通过AddRef和Release来控制得到的接口的生命期。AddRef实际上是告诉组件我们想要使用某个接口,而Release则告诉组件我们用完了某个接口。同时Release也给组件提供了一些控制其生命期的能力。客户并不需要直接地将组件从内存中删除,相反Release将告诉组件客户使用完了某个接口。若没有任何客户还在使用组件的任何一个接口,那些组件可以将自己从内存中删除。
虽然对于接口我们现在可以进行完全的控制了。但另外有一个非常重要的问题我们还没有讨论到:动态链接。一个没有动态链接能力的组件就如同一个没有套装、头盔及长筒靴的消防队员。在下一章中我们将讨论如何给组件加上动态链接的能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值