COM学习笔记(三)

COM的IUnknown接口

  COM定义的每一个接口都必须从IUnknown继承过来,因为IUnknown接口提供了两个非常重要的特性:生存期控制和接口查询。当客户程完成对对象的操作后,应该将对象释放掉以提高资源利用率。IUnknown中引入了“引用计数”的方法可以有效控制对象的生存周期。另外一方面如果一个COM对象实现了多个接口,而客户程序一开始只拥有一个接口指针,这时就可以借助IUnknown的“接口查询”方法来完成接口之间的跳转。

IUnknown的C++定义如下:

class IUnknown
{
public:
  virtual HRESULT _stdcall QueryInterface(const IID& idd, void ** ppv) = 0;
  virtual ULONG _stdcall AddRef() = 0;
  virtual ULONG _stdcall Release() = 0;
};

  IUnknown包含了三个成员函数:QueryInterface、AddRef和Release。函数QueryInterface用于查询COM对象的其他接口指针,函数AddRef和Release用于对引用计数进行操作。

引用计数

  当客户程序得到一个指向该对象的接口指针时,引用计数值增1,当客户程序用完了该接口指针后,引用计数减1。当引用计数减到0时,COM对象就应该把自己从内存中清除。按照COM规范,一个COM组件可以实现多个COM对象,并且每个COM对象又可以支持多个COM接口,这种多层次的结构为引用计数提供了多种选择方案:

  • 设置一个针对整个组件全局的引用计数;
  • 为每个COM对象设置一个引用计数;
  • 为每个接口设置一个引用计数。

  整个组件全局的引用计数的缺点是当创建多个COM对象时,只有当所有的对象都使用完了才可以将这些对象都释放掉,否则这些对象总是占着内存。为每个接口设置一个引用计数是每当一个接口的引用计数减到0时就给其对象发出通知,对象在接收到通知后需要判断是否它所有的接口的引用计数都减为0,若是就将自己释放掉,然后再进一步通知组件程序,组件程序再判断是否它的所有对象都被释放然后再决定自己是否可被清除出内存。这种方法需要传递和判断的次数过多,所以折中看来在对象一级设置引用计数较为合理。因为引入了IUnknown,所以重新定义IMyInterface和CMyInterfaceClass。具体的实现如下:

class IMyInterface:public IUnknown
{
public:
  virtual BOOL MyFunc1() = 0;
  virtual BOOL MyFunc2() = 0;
  virtual void MyFunc3() = 0;
  virtual BOOL MyFunc4() = 0;
};

class CMyInterfaceClass:public IMyInterface
{
public
  CMyInterfaceClass();
  ~CMyInterfaceClass();
public:
  virtual HRESULT QueryInterface(const IID& iid,void** ppv);
  ULONG AddRef();
  ULONG Release();
  virtual BOOL MyFunc1();
  virtual BOOL MyFunc2();
  virtual void MyFunc3();
  virtual BOOL MyFunc4();
private:
  BYTE* m_MyData;
  char* m_MyName;
  int m_Ref; //引用计数
  //other private information
};


CMyInterfaceClass::CMyInterfaceClass()
{
  m_Ref = 0; //先在CMyInterfaceClass的构造函数中初始化m_Ref
  //other initialization
}

ULONG CMyInterfaceClass::AddRef()
{
  m_Ref ++;
  return (ULONG)m_Ref;
}
ULONG CMyInterfaceClass::Release()
{
  m_Ref --;

  if(m_Ref == 0)
  {
    delete this;
    return 0;
  }
  return (ULONG)m_Ref;
}

接口查询

  接口查询主要用于对象的多个接口之间的联系。QueryInterface定义如下:

HRESULT QueryInterface(
  [in] REFIID riid,
  [out] void **ppvObject
);

  函数的输入参数iid为接口标识符IID,输出参数ppv为查询得到的结果接口指针,如果对象没有实现iid所标识的接口,则输出参数ppv会指向NULL。当客户程序创建了COM对象之后,创建函数总会返回一个接口指针,因为所有的接口都继承于IUnknown,所以所有的接口都有QueryInterface成员函数,于是当我们得到了初始的接口指针之后,就可以通过它的QueryInterface函数来获得该对象所支持的任何一个接口。COM规范给出了一些有关接口查询的原则:

  • 对于同一个对象的不同接口指针,查询得到的IUnknown接口必须完全相同;
  • 接口的自反性(对一个接口查询其自身一定是成功的);
  • 接口的对称性(从接口1查询到接口2,也必然可以从接口2查询到接口1);
  • 接口的传递性(从接口1查询到接口2,从接口2查询到接口3,则可以从接口1查询到接口3,并可以从接口3查询到接口1);
  • 接口查询时间无关性(某个时间可以查询到,就一直都可以)。

  举例,假设一个对象实现了两个接口IMyInterface_1和IMyInterface_2(当然,这两个接口都继承于IUnknown接口)。则QueryInterface函数可以如下实现:

HRESULT CMyInterfaceClass::QueryInterface(conse IID& iid, void** ppv)
{
  if(iid == IID_IUnknown)
  {
    *ppv = (IMyInterface_1*)this;
    ((IMyInterface_1*)(*ppv))->AddRef();
  }
  else if(iid == IDD_IMyInterface_1)
  {
    *ppv = (IMyInterface_1*)this;
    ((IMyInterface_1*)(*ppv))->AddRef();
  }
  else if(iid == IDD_IMyInterface_2)
  {
    *ppv = (IMyInterface_2*)this;
    ((IMyInterface_2*)(*ppv))->AddRef();
  }
  else
  {
    *ppv = NULL;
    return E_NOINTERFACE;
  }
  return S_OK;
}

  QueryInterface函数对于iid的三种可能值分别进行了处理,都会返回相应的接口指针。将this指针转换为基类指针格式,所得到的指针正好指向接口的vtable,所以用类型转换函数就可以得到每个接口的vtable(接口指针)。CMyInterfaceClass的内存结构如下图所示。

image

图3.1  类CMyInterfaceClass的内存结构

  另外值得注意的是,代码第一个if中并没有将this指针直接转换成IUnknown指针,只是因为在C++语法中IMyInterface_1和IMyInterface_2继承于两个IUnknown节点,直接将this指针转换成IUnknown指针存在二义性。因此可以先把this指针转换成IMyInterface_1指针或者IMyInterface_2指针,再转换成IUnknown指针,但必须保证每次查询到的IUnknown接口完全一致(这是两个接口同属于一个对象所要求的)。

posted @ 2012-01-05 19:35  J_Outsider  阅读(524)  评论(1编辑  收藏  举报