第二讲:计数引用AddRef与Release

来源:互联网 发布:淘宝账户因违规被冻结 编辑:程序博客网 时间:2024/04/30 08:11

来自:http://www.vckbase.com/index.php/video/listview/fid/2/sid/13

本节内容:

1、内存资源何时释放

2、引用计数的原理

3、AddRef与Release的实现与使用

4、引用计数的优化

1、内存资源何时释放

  在上一节的例子中,我们用到了new CA()deletepA。我们知道我们创建了一个组件,最终在不用这个组件的时候,是应该把它销毁了。不过,什么时候才是不用这个组件的时候呢?

      假设上例中,我们可能会把pIUnknown传给CB类的成员变量。如:

  if(...)  //可能为真,也可能为假

  {

  CB *pB =new CB(pIUnknown);

  }

       我们怎么知道何时我们不会再用到这个pA所指向的组件?当然,你可能会回答在主函数的最后面执行deletepA,因为那时pA一定不用了。

      在主函数的最后面执行deletepA确实是一个可行的办法。但却不是好办法。因为这样子最终是释放了pA的内存资源,不过却不是及时(pA所指的组件不用时)地释放内存资源。如果一个程序,所有的资源在不用时都没有及时释放,这个程序在运行中所占用的内存将是巨大的。如何解决这个问题呢?这就需要引用计数技术。

2、引用计数的原理

引用计数技术就是用来管理对象生命期的一种技术。
对象O可能同时被外界A,外界B,外界C引用。也就是说外界A,外界B,外界C可能都在使用对象O
每次当对象被外界引用时,计数器就自增1
每次当外界不用对象时,计数器就自减1
在计数值为零时,对象本身执行delete this,销毁自己的资源。
引用计数使得对象通过计数能够知道何时对象不再被使用,然后及时地删除自身所占的内存资源。
IUnknown接口的AddRefRelease就是引用计数的实现方法。
3、AddRef与Release的实现
查看Section2Demo1关于AddRefRelease的实现。

#include "stdafx.h"
#include <iostream>
#include <Unknwn.h>

using namespace std;

// {A348FBDD-E765-4b41-8477-6D8B7038FCC6}
static const IID IID_IX =
{ 0xa348fbdd, 0xe765, 0x4b41, { 0x84, 0x77, 0x6d, 0x8b, 0x70, 0x38, 0xfc, 0xc6 } };

// {10A90ED2-FCDE-4067-92DA-ABA38F5C1B12}
static const IID IID_IY =
{ 0x10a90ed2, 0xfcde, 0x4067, { 0x92, 0xda, 0xab, 0xa3, 0x8f, 0x5c, 0x1b, 0x12 } };


//接口IX
interface IX : public IUnknown

 virtual void Fx1() = 0;
 virtual void Fx2() = 0;
};

//接口IY
interface IY : public IUnknown

 virtual void Fy1() = 0;
 virtual void Fy2() = 0;
};

//组件CA
class CA: public IX, public IY
{
//构造与析构
public:
 CA()
 {
  m_lCount = 0;

  //构造时,需要自增引用计数
  AddRef();
 }
 virtual ~CA()  //析构函数一般采用虚函数
 {
  cout << "我被释放啦!" << endl;
 }

//实现
public:
 virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **ppv)
 {
  //查看PPT中CA的内存结构讲解如下的转换过程

  if (iid == IID_IUnknown)
  {
   //即使CA继承了两个IUnknown接口,其中一个来自于IX,另一个来自于IY。我们一般返回第一个被继承的IX接口。
   *ppv = static_cast<IX*>(this);  
  }
  else if (iid == IID_IX)
  {
   //返回IX接口
   *ppv = static_cast<IX*>(this);  
  }
  else if (iid == IID_IY)
  {
   //返回IY接口
   *ppv = static_cast<IY*>(this);
  }
  else
  {
   //查询不到IID,*ppv返回NULL。
   *ppv = NULL;
   return E_NOINTERFACE; //函数返回值返回E_NOINTERFACE,表示组件不支持iid的接口。
  }

  //查询成功时,需要自增引用计数
  AddRef();  

  return S_OK; //返回S_OK
 }

 virtual ULONG STDMETHODCALLTYPE AddRef()
 {
  //简单实现方法
  return ++m_lCount;

  //多线程编程采用如下方法,这种方法确保同一个时刻只会有一个线程来访问成员变量
  //return InterlockedIncrement(&m_lCount);
 }

 virtual ULONG STDMETHODCALLTYPE Release()
 {
  //简单实现方法
  if (--m_lCount == 0)
  {
   delete this; //销毁自己
   return 0;
  }
  return m_lCount;

  ////多线程编程采用如下方法,这种方法确保同一个时刻只会有一个线程来访问成员变量
  //if (InterlockedDecrement(&m_lCount) == 0)
  //{
  // delete this;  //销毁自己
  // return 0;
  //}
  //return m_lCount;
 }

 virtual void Fx1()
 {
  cout << "Fx1" << endl;
 }

 virtual void Fx2()
 {
  cout << "Fx2" << endl;
 }

 virtual void Fy1()
 {
  cout << "Fy1" << endl;
 }

 virtual void Fy2()
 {
  cout << "Fy2" << endl;
 }

//数据
private:
 long m_lCount;  //引用计数,该计数只被该类管理,外界不可访问,访问权限设置为private

};


int main()
{
 HRESULT hr;

 CA *pA = new CA();  //引用计数1

 //从组件查询IUnknown接口
 IUnknown *pIUnknown = NULL;
 hr = pA->QueryInterface(IID_IUnknown, (void**)&pIUnknown);  //引用计数2
 if (SUCCEEDED(hr))  //对HRESULT返回值的判断,一般采用SUCCEEDED
 {
  pA->Release(); //pA不再使用,引用计数1
  pA = NULL;  //访止再不小心使用m_pA

  //从IUnknown查询IX接口
  IX *pIX = NULL;
  hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX); //引用计数2
  if (SUCCEEDED(hr))
  {
   //调用IX接口的方法
   pIX->Fx1();
   pIX->Fx2();
  }

  //从IUnknown查询IY接口
  IY *pIY = NULL;
  hr = pIUnknown->QueryInterface(IID_IY, (void**)&pIY); //引用计数3
  if (SUCCEEDED(hr))
  {
   //调用IY接口的方法
   pIY->Fy1();
   pIY->Fy1();
  }

  if ((void*)pIX != (void*)pIY)
  {
   cout << "pIX != pIY" <<endl;
  }

  if ((void*)pIUnknown != (void*)pIY)
  {
   cout << "pIUnknown != pIY" <<endl;
  }

  pIY->Release();  //pIY不再使用,引用计数2
  pIY = NULL;

  if ((void*)pIUnknown == (void*)pIX)
  {
   cout << "pIUnknown == pIX" <<endl;
  }

  //从IX查询IY
  IY *pIY2 = NULL;
  hr = pIX->QueryInterface(IID_IY, (void**)&pIY2);  //引用计数,引用计数3

  pIX->Release();  //pIX不再使用,引用计数2
  pIX = NULL;

  if (SUCCEEDED(hr))
  {
   pIY2->Fy1();
   pIY2->Fy2();
  }

  pIY2->Release(); //pIY不再使用,引用计数1
  pIY2 = NULL;

 }

 //目前引用计数为1,因为pIUnknown还在使用。

 IX *pIX2 = NULL;
 hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX2);  //引用计数为2
 if (SUCCEEDED(hr))
 {
  IX *pIX3 = NULL;
  pIX3 = pIX2;  //执行了赋值
  pIX3->AddRef();  //由于上句执行了赋值,所以引用计数需要自增,引用计数为3

  pIX3->Fx1();
  pIX3->Fx2();

  pIX3->Release();  //pIX3不再使用,引用计数为2
  pIX3 = NULL;
 }
 pIX2->Release();  //pIX2不再使用,引用计数为1
 pIX2 = NULL;

 pIUnknown->Release();  //pIUnknown不再使用,引用计数为0,Release函数里执行了delete this,销毁组件的内存资源
 pIUnknown = NULL;

 //释放组件? no!
 //delete pA;  //不再需要写delete代码

 return 0;
}

Release的意义与使用

Release,使组件引用计数自减1,如果引用计数为零,释放本身的内存资源。
在接口使用完之后,调用Release
查看Section2Demo1,关于AddRefRelease的使用,并适当注释一些AddRefRelease查看CA的析构函数是否运行或会不会出现对无效指针(野指针)的操作。

4、引用计数的优化

刚才的例子中,我们看到了

if (SUCCEEDED(hr))

{

  IX *pIX3 = NULL;

  pIX3 = pIX2;

  pIX3->AddRef(); 

  pIX3->Fx1();

  pIX3->Fx2();

  pIX3->Release();

  pIX3 = NULL;

  }

对于pIX2pIX3来说,都是同一个接口,生命期是一样的,这个接口在一个块({})中,执行了一次的AddRef,一次的Release,其实就相当于没有执行AddRef,Release的效果。是否可以优化为下一页的代码呢?

if (SUCCEEDED(hr))

  {

  IX *pIX3 = NULL;

  pIX3 = pIX2;

  pIX3->Fx1();

  pIX3->Fx2();

  }

这种优化可行吗?答案是可行的!因为这种优化符合了引用计数优化的局部变量原则
引用计数的优化原则:

  一、输入参数原则:

输入参数指的是给函数传递某个值的参数。在函数体中将会使用这个值但却不会修改它或将其返回给调用者。在C++中,输入参数实际上就是那些按值传递的参数。
对传入函数的接口指针,无需调用AddRefRelease

  二、局部变量原则

              对于局部复制的接口指针,由于它们只是在函数的生命期内才存在,因此无需调用AddRefRelease

输入参数原则:

 void Fun(IX *pIXParam//参数传递存在赋值过程

{

  //pIXParam->AddRef();  //可优化,注释掉

  pIXParam->Fx1();

  pIXParam->Fx2();

  //pIXParam->Release(); //可优化,注释掉

}

局部变量原则:

void Fun(IX *pIX)

{

  IX*pIX2 = pIX;

  //pIX2->AddRef(); //可优化,注释掉

  pIX2->Fx1();

  pIX2->Fx2();

  //pIX2->Release();  //可优化,注释掉

}

以下代码可以优化吗?

void Fun(IX **ppIX)

{

  (*ppIX)->Fx1();

  (*ppIX)->Fx2();

  (*ppIX)->Release(); //可以优化吗?

  *ppIX =m_pIXOther;

  (*ppIX)->AddRef(); //可以优化吗?

  (*ppIX)->Fx1();

  (*ppIX)->Fx2();

}

答案是否定的!因为它不是输入参数原则,而是输入-输出参数原则。此原则下,引用计数不能优化!

//以上两句务必要运行,因为*ppIXm_pIXOther不一个属性同一个组件。

//比如假设*ppIX是指向第一次的new CA(),而m_pIXOther却是指向第二次的new CA()

//或者*ppIX是指向new CA(),而m_pIXOther是指向new CZ()CACZ的共同点,只是都继承了IX接口而已。

引用计数,带来了高效的内存资源管理方法,能及时地释放不再使用的资源。但却带来了编码的麻烦。在后续的讲解中,会讲到对引用计数的封装,也就是智能指针,到时组件的客户不再编写AddRefRelease代码,也不需要编写delete代码,便可以方便,舒心地进行内存资源的管理。