COM读书笔记

对组件的要求:
1。动态链接
2。信息封装:组件必须可以在不妨碍已有用户的情况下被升级。一个组件的新版本必须既能够同老版本的客户一起使用,也可以同新版本的客户一起使用。
组件必须以二进制的形式发布。如果想将实现组件的编程语言隐藏起来,那么在发布时它们必须是已被编译、链接好并且马上可以投入使用的。

 

实现这种可动态改变组件的关键问题是信息的封装。对于封装,COM是通过组件和客户之间的连接或接口来实现。
客户甚至不必知道一个组件所提供的所有接口


接口的不变性:接口不会发生任何变化,一旦公布了一个接口,那么它将永远保持不变。当对组件进行升级时,一般不会修改已有的接口,而是加入一些新的接口。这些多重接口使得组件除了可以支持原有接口之外,还可以支持新的接口。因此多重继承为组件和客户可以智能地同对方的新版本进行交互提供了坚实的基础。


使用组件来构造应用程序的最大的优点在于可以复用应用程序的 结构。如果接口设计得好的话,将可以得到课复用极高的结构。例如,可以通过允许改变若干关键性的组件,同样的结构可以支持几种不同类型的应用。
但设计可复用的结构绝不是一件容易的事情。例如,它要求设计者应具有预测未来的能力

可以将抽象基类看作是一个表单,派生类所做的就是填充此表单中的空白。抽象基类指定了其派生类应提供哪些函数,而派生类则具体实现这些函数。对纯虚函数的继承被称作是接口继承,这主要是因为派生类所继承的只是基类对函数的描述。抽象类没有提供任何可供继承的实现细节。

COM接口在C++中是用纯抽象基类实现的
一个COM组件可以提供多个接口
一个C++类可以使用多继承来实现一个可以提供多个接口的组件

interface IX
{
      virtual void _stdcall fx1() =0;
      virtual void _stdcall fx2() =0;
}
interface IY
{
      virtual void _stdcall fy1() =0;
      virtual void _stdcall fy2() =0;
}
class CA : public IX,
           public IY
{
  public:
      virtual void _stdcall fx1() {cout<<"CA::fx1()";}
      virtual void _stdcall fx2() {cout<<"CA::fx2()";}
      virtual void _stdcall fy1() {cout<<"CA::fy1()";}
      virtual void _stdcall fy2() {cout<<"CA::fy2()";}
}
int main()
{
      CA* pA =new CA;
      IX* pIX=pA;
      pIX->fx1();
      pIX->fx2();

      IY* pIY=pA;
      pIY->fx1();
      pIY->fx2();

      delete pA;
      return 0;
}

客户main和组件是通过两个接口来通信的,在接口的实现中使用了两个纯抽象基类IX和IY。例中的组件是由类CA实现的,它继承了IX和IY,并实现了这两个接口中的所有成员。
非接口通信:
客户和组件之间只是通过接口进行通信。但上面的程序中的客户却没有遵循这一规则。在那里,客户与组件间的通信是通过一个指向类CA的指针而不是通过指向接口的指针完成的。使用指向CA的指针要求客户知道类CA的定义(通常是一个头文件)。在类CA的说明中有许多有关实现的细节。对这些实现细节的修改将使得客户必须被"重新编译"。但前面讲过,增加或减少组件的接口不应打断已有的客户。这就是我们为什么一再坚持客户和组件之间只应通过接口进行通信。记住,接口是由没有实现细节的虚纯基类实现的。

当然当客户和组件在同一源文件中时,并没有必要将它们分开。但是当客户和组件是在动态链接的情况下,此种隔离是必需的。在没有源代码的情况下更是如此。在第3章我们将对前面的例子进行修改,使之不再使用指向CA的指针。这样一来,客户将不再需要CA的类声明。
在客户程序中使用了new 和delete这些操作可以控制组件的生命期,第4章将介绍一种使用接口来删除组件的方法。
我们也将给出一种更有效的建立组件的方法。

 

 

#define STDMETHOD _stdcall
#define interface struct
HRESULT _stdcall QueryInterface(const IID& iid, void** ppv)
所有的COM接口都继承了IUnknown,每个接口的vtbl的前三个函数都是QueryInstance,AddRef和Release


IUnknown指针的获取:
IUnknown *CreateInstance();
接口查询:
客户同组件的交互都是通过一个接口完成的,在客户查询组件的其他接口时,也是通过接口完成的,这个接口就是IUnknown
客户可以通过调用QueryInterface来查询组件是否支持某个特定的接口
AddRef和Release可以用来控制接口的生命期


COM客户并不知道一个组件所支持的接口,为知道某个组件是否支持某个特定的接口,客户可以在运行时询问组件
客户对其所用的组件的全部功能也一样是不甚了知。
客户对于组件的了解可以说是非常有限的,客户可以询问组件是否支持某个特定的接口,在进行多次这种询问之后,客户对于组件的认识将越来越清晰
我们本来就希望客户对于组件要尽可能地少了解一些。事实上客户对于组件知道的越少,那么在不影响客户正常运行的情况下组件就可能最大限度地发生变化。
若客户只是在它需要使用某种接口时才询问组件,那么只是在改变这些接口时才会影响到客户。在不改变与客户接口的情况下,可以用新的组件来代替已有的组件而不会对客户造成任何影响。对于客户不使用的接口,新组件用不着一定要支持。


虽然C++可以直接操作和使用实例数据,但COM组件绝不会访问任何实例数据。在COM中,对一个组件的访问只能通过函数完成,而绝不能直接通过变量。这一点同我们对COM组件的定义是相符的。纯抽象基类只有虚拟函数,而没有任何实例数据。

三:QueryInterface
QueryInterface遵循的一些规则:

IUnknown *pIUnknown=CreateInstance();
pIY->QueryInterface(IID_IUnknown,(void**)&pIUnknownFromIY)
pIUnknown==pIUnknownFromIY
同一IUnknown:
QueryInterface返回的总是同一IUnknown指针。组件的 实例只有一个IUnknown接口。因为当查询组件实例的IUnknown接口时,不论通过哪个接口,所得到的均是同一指针值。为确定两个接口是否指向同一组件,可以通过这两个接口查询IUnknown接口,然后将返回值进行比较。
bool SameComponents(IX* pIX,IY* pIY)
{
         IUnknown* pI1=NULL;
         IUnknown* pI2=NULL;

         pIX->QueryInterface(IID_IUnknown,(void**)&pI1);
         pIY->QueryInterface(IID_IUnknown,(void**)&pI2);
 
         return pI1==pI2;
}

我们知道在COM中接口是不会发生变化的。当组件发布一个接口并被某个客户使用之后,此接口将绝不会发生任何变化,而将永远保持不变。每一个接口都有一个唯一的接口标识符(IID)。一般情况下,我们不会改变接口,而可以建立一个新接口并为之指定一个新的IID。当QueryInterface接收到对老IID的查询时,它将返回老的接口。而当它收到对新的IID的查询时,它将返回新的接口。新接口可以继承老接口,也可以同老接口完全不同。由于老的接口仍然保持不变,已有的客户的运行将不会受到任何影响。而新客户则可以自行决定是使用老接口还是新接口,因它可以自由决定到底是查询哪个接口。


何时需要建立一个新版本:
当改变了下列条件中的任何一个时,就应给新接口指定新的ID:
接口中函数的数目、接口中函数的顺序……总之,只要是所做的修改将会导致已有客户的正常运行,都应为接口指定新的IID


四:引用计数
COM通过直接控制单个接口的生命期来间接控制组件生命期的方法
客户为什么不应直接控制组件的生命期:
我们可以通知组件何时需要使用它的某个接口以及何时用完此接口,而不是直接将接口删除。一般我们总能精确地知道何时开始使用一个接口,并且在大多数情况下,可以精确地知道何时将用完此接口。但决定何时将用完整个组件并不是一件容易的事情。因此,当用完某个接口后给组件发出指示将比使用完整个组件更有意义。对组件的释放可以由组件在客户使用完其各个接口之后自己完成。

 

可以考虑在整个程序的运行期内一直装载相应的组件。但这种方法的效率不是很高。(在多组件或者是多组件实例程序中尤其明显)

AddRef和Release的作用就是给客户提供一种让它指示何时处理完一个接口的手段。
AddRef和Release如何可以实现让组件自己来管理其生命期,以及客户又如何借助于这两个函数以使它们只需关心接口。

正确的使用引用计数需要了解的三条规则:……
智能指针类可以把引用计数完全封装起来。
引用计数接口:
在客户看来,引用计数是处于接口级上而不是组件级上的,每个接口调用AddRef和Release,


五:动态链接

如何将组件放入动态链接库(DLL)中?这并不是我们要将一个组件变成一个DLL。一个组件实际上并不是一个DLL,将组件当成DLL来看是非常肤浅的。DLL只是一个组件服务器,或者说是一种发行组件的方式。组件实际上应看成是在DLL中所实现的接口集。DLL只是一种形式,而组件才是实质。

理解.DEF文件中的输出函数用来干什么?怎么被调用?
自注册:
用户可以使用程序regsvr32.exe来注册某个组件。它实际上是通过调用.DEF文件中的DllRegisterServer,这一过程实际上非常简单,只需用LoadLibrary装载相应的DLL,然后用GetProcAddress获取此函数地址,再调用此函数即可。


由此我想在CoGetClassObject及CoCreateInstance中都有对LoadLibrary和GetProcAddress进行调用。其中LoadLibrary的参数为DLL的名称,而CoGetClassObject及CoCreateInstance则使用它们的第一个参数(一个CLSID),组件可以用CLSID作为索引在Windows注册表中发布包含它们的DLL文件名称。CoGetClassObject及CoCreateInstance将用CLSID作为关键字在注册表中查询所需的DLL文件名称。

DllGetClassObject的参数与CoGetClassObject相同


六、理解组件的创建过程:
首先是客户,它将通过调用CoGetClassObject来启动组件的创建过程。其次是COM库,它实现了CoGetClassObject函数。第三格角色是DLL,其中实现了被CoGetClassObject调用的DllGetClassObject函数。DllGetClassObject的任务就是创建客户所请求的类厂。


当客户同组件动态地进行链接时,它必须将组件所在的DLL装载到内存中,这可以由LoadLibrary完成。当使用完某个DLL之后,需要将其从内存中释放。将那些不再需要的组件放到内存中将是极为低效的。为此COM库实现了一个名为CoFreeUnusedLibraries函数,以释放那些不再需要的库所占用的内存空间。在程序的空闲期间,客户应周期性地调用
CoFreeUnusedLibraries,同时在DEF文件中也要实现相应的DllFreeUnusedLibraries,DllFreeUnusedLibraries何以知道客户不再需要某个DLL了并可以将其从内存中释放呢?为此CoFreeUnusedLibraries将调用DllCanUnloadNow以询问DLL它是否可以被卸载掉。若DLL不再提供任何组件了,那些CoFreeUnusedLibraries就可以将此DLL卸载掉。DLL为决定它是否仍在提供对组件的支持,它将维护一个关于组件的计数值,为此需要在DLL函数实现文件中加入如下代码:
static long g_cComponents =0;
然后IClassFactory::CreateInstance或组件的构造函数可以将g_cComponents值增大,而组件的析构函数则可将此值减小。若g_cComponents的值为零,那些DllCanUnloadNow将给出肯定的回复。

将某个拥有正在运行的类厂的DLL从内存中卸载将会给客户带来一些麻烦。假定客户中有一个指向某个正在运行的类厂的指针,在相应的DLL被卸载之后,若客户试图使用IClassFactory指针,那么显然将会出现错误。客户需要一种手段,以防止当它想在某个函数的作用域范围之外使用一个IClassFactory指针时DLL被从内存中卸载掉。IClassFactory::LockServer的作用正在于此。它给客户提供了一种将服务器保存在内存中、直至使用完毕的方法。此时,客户只需要调用LockServer(TRUE)以锁住相应的服务器,并在使用完毕之后调用LockServer(FALSE)将其解锁。
LockServer的实现很简单,只需相应地增大或缩小g_cComponents计数值即可。当然也可以对组件和加锁使用不同的计数值。此时,DllCanUnloadNow应同时检测这两个计数值。

(一般而言,在单组件程序中,组件的析构函数调用(一般在程序结束时)后,组件将释放内存,也就是说不需要再使用组件拉,而使用IClassFactory指针一般是用它的CreateInstance函数创建组件啊?但或许它还要创建同一组件的另外一个实例呢?呵呵,这时如果在组件的析构函数调用前没有LockServer(TRUE),问题就来拉)


七、类厂
为什么要引入类厂?
CoCreateInstance是创建组件最简单也是使用最多的一种方法,但是它的灵活性却不足以使之能够满足所有组件的要求。1、若想用不同于IClassFactory的某个创建接口来创建组件,则“必须”使用CoGetClassObject。因此如果想使用IClassFactory2来创建组件,就应使用CoGetClassObject。2、若需要创建同一组件的多个实例,那么使用CoGetClassObject将可以获得更高的效率(我想这一条并不是引入类厂的真正原因,如果不引入类厂,可以通过直接create组件实现,也简单啊!)。
IClassFactory2接口在IClassFactory的基础上增加了许可或权限功能。此时,为使类厂能够创建所需的组件,客户必须通过IClassFactory2给类厂提供正确的关键字或许可。通过使用IClassFactory2,类厂可以保证客户只能获得它能合法访问的组件(我想这才是类厂引入的真正原因,微软主要是想对组件的使用权限进行控制,所以在中间加个类厂,你想要是直接create组件,组件一创建好就可以使用拉,还怎么控制?)

类厂不过是一个创建其它组件的简单组件。
类厂的一个实例将只能创建同某个CLSID相应的组件。这一点从CoGetClassObject将接收一个CLSID而IClassFactory::CreateInstance却不接收此参数可以看出来。
类厂为什么要做成组件形式呢?
别忘了类厂的实现中有创建组件的语句
CFilter *afilter=new CFilter;其中CFilter为组件
为了实现客户与组件的彻底分离,类厂也设计成了组件形式。


八、包容与聚合“组件复用、扩展和定制”
COM是否支持继承呢?对这个问题的回答是:可以又不可以。COM并不支持“实现继承”,这种继承指的是类继承其基类的代码和实现。但COM却支持“接口继承”,这种继承指的是类继承其基类的类型或接口。
COM不支持实现继承的原因在于这种继承方式将使得一个对象的实现同另外一个对象的实现紧紧地关联起来。在这种情况下,当基类的实现被修改之后,派生类将无法正常运行而必须被修改。
抽象基类是一种最纯粹的接口继承,并且正好也被用来实现COM接口。必须有一种严格的方法来保证组件的客户不会因组件的变化而受到影响,显然实现继承无法提供客户所需的保护。


COM并不支持实现继承,但这并不会使COM的功能有任何损失,因我们可以用组件包容来完全模拟实现继承。
当拿到一个组件之后,我们可能会想办法对之加以扩展或改造以使之符合自己的使用需要,并可能会希望用此改造后的组件来代替原有的组件。在C++中,对类的改造是用包容和继承来实现的。在COM中,则可使用包容和聚合来对组件进行改造。
包容和聚合实际上是使一个组件使用另外一个组件的一种技术。在COM中,同其他内容类似,包容也是在接口级完成的。外部组件包含指向内部组件接口的指针。此时外部组件只是内部组件的一个客户,它将使用内部组件的接口来实现它自己的接口。

为什么要引入包容和聚合?它们的主要作用是什么?怎么实现?在什么情况下使用?

QueryInterface的实现中如果不是查询最晚接口该怎么实现?
static_cast const_cast reinterpret_cast的区别?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值