Upload
conner
View
113
Download
0
Embed Size (px)
DESCRIPTION
从面向对象到面向 COM. 主讲 叶长青 华东师大教育信息技术学系 2006-3-1 丽娃河畔 [email protected]. 本课的内容及目标. 内容: 面向对象的一般概念 从面向对象到面向 COM COM 组件技术 目标: 了解程序设计技术的发展动向 提升程序设计能力 开阔专业视野. 课程参考书目. 《 COM 原理与应用》 《 COM 本质论》 《 COM 技术内幕》 《 Advanced CORBA Programming with C++ 》. 面向对象的基本概念. 程序设计的发展历程. - PowerPoint PPT Presentation
Citation preview
本课的内容及目标
内容:
• 面向对象的一般概念• 从面向对象到面向 COM• COM 组件技术
目标:• 了解程序设计技术的发展动向• 提升程序设计能力• 开阔专业视野
课程参考书目
《 COM 原理与应用》《 COM 本质论》《 COM 技术内幕》《 Advanced CORBA Programming with C++ 》
面向对象的基本概念
• 为节省每一个字节而努力的阶段例:用 PASCAL 语句写成的程序段 A[I] : =A[I] + A[T] ; A[T] : =A[I] – A[T] ; A[I] : =A[I] – A[T] ;目的是什么?我们现在习惯的写法是什么?• 不关心“空间的浪费”,更关心程序的清晰框架结构阶段例:用 FORTRAN 语言编写的程序段 DO 5 I=1,N DO 5 J=1,N5 V(I,J) = (I/J)*(J/I)
程序设计的发展历程
IF (X .LT. Y) GOTO 30IF (Y .LT. Z) GOTO 50SMALL = ZGOTO 7030 IF (X .LT. Z) GOTO 60SMALL = ZGOTO 7050 SMALL = YGOTO 7060 SMALL = X70 CONTINUE
SMALL = XIF (Y .LT. SMALL) SMALL = YIF (Z .LT. SMALL) SMALL = Z
• 注重“重用性”的问题。 面向对象• 注重“标准化元件”的阶段。 面向组件?软件企业分工细化阶段
类是具有相同属性特征事物的集合。计算机专业语境下,类是封装了状态(变量)和操作(对变量处理的过程和函数)的抽象数据类型。(对应于标准数据类型) 对象是实例,它反映了具体的事物。
鸟,是类的概念。麻雀是鸟类的一个实例。一只麻雀?
对象由类来定义 一个对象可以与多个具有继承关系的类相联系,即: A 是一个类, B 、 C 、 D 是在 A 的基础上扩展后的新类, E , F , G 则是在 C 类的基础上发展而来,而对象 x 是 F 类的一个实例。
有关术语
封装、继承与多态
面向对象概念的三个关键词
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。 C++ 对类和对象的描述:
Class student{Public:成员函数 1 ;成员函数 2 ;……Private:成员变量 1 ;成员变量 2 ;……} Int x,y;Student z;
类名
例一: C++ 中类的定义与实现说明:包含成员变量,成员函数,类的定义,对象的定义。
例二、例三:成员函数在类内、类外实现的情形
例四:成员函数是私有函数时的存取特性与意义
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。 在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。
从程序语言角度来看,在一个对象中代码和(或)数据可以是这个对象私有的,不能被对象外的部分直接访问。因而对象提供了一种高级保护以防止程序被无关部分错误修改或错误地使用了对象的私有部分。 当从对象外部试图直接对受保护的内部数据进行修改时,将被程序拒绝,只有通过对象所提供的对外服务函数才能够对其内部数据进行必要的加工,从而保证了数据加工的合法性。 从这一意义上讲,把这种代码和数据的联系称为“封装”。换句话说,封装是将对象封闭保护起来,是将内部细节隐蔽起来的能力。
实现的细节是“可变的部分”。如果“块”是单个类,那么可变的部分通常用 private: 或 protected: 关键字来封装。 “ 稳定的部分”是接口。好的接口提供了一个以用户的词汇简化了的视图,并且被从外到里的设计。(此处的“用户”是指其它开发者,而不是购买完整应用的最终用户)。 设计一个清晰的接口并且将实现和接口分离,只不过是允许用户使用接口并强迫用户使用接口。
如何才能防止其它程序员查看我的类的私有部分而破坏封装?
讨论
只要其它程序员写的代码不依赖于他们的所见,那么即使它们看了你的类的 private: 和 / 或 proteced: 部分,也不会破坏封装。换句话说,封装不会阻止人认识类的内部。封装只是防止他们写出依赖类内部实现的代码。倘若他们写的代码依赖于接口而不是实现,就不会增加维护成本。
不必这么做——封装是对于代码而言的,而不是对人。
封装是一种安全装置吗?
封装要防止的是错误,
而不是
封装 != 安全。 钱掉了!
成员函数、成员变量中的另类
例五:构造函数
例六:析构函数
例七:何时使用构造函数
例八:友元函数
例九:静态变量 2005-9-30
1.继承的概念及重要性 inheritance : 是软件重用的一种形式,将相关的类组织起来,并分亨其间的共通数据和操作行为。
最具吸引力的特点: 新类可以从现有的类库中继承。提倡建立与现有的类有许多共性的新类,添加基类的所没有的特点以及取代和改进从基类继承来的特点来实现软件的重用
单重继承形成树状层次结构,由基类和派生类构成了一种层次关系,继承的层次在系统的限制范围内是任意的。
2. 基类 父类定义了所有子类共通的对外接口和私有实现内容,父类被称为基类
成员函数: 基类的私有成员只能被基类的成员函数和友元访问,基类的受保护成员只能被基类及派生类的成员函数和友元访问
3. 派生类 新类继承预定义基类的数据成员和成员函数,而不必重新编写数据成员和成员函数,这种新类叫派生( derived )类
派生类永远不能直接访问基类的私有成员
重定义函数 : 派生类中无需继承的功能及要扩充的基类功能可以重定义成员函数,但在派生类再调用基类的同名函数时要用到作用域运算符Employee::print()
派生类的构造函数和析构函数: 由于派生类继承了基类的成员,所以在建立派生类的实例对象时,必须调用基类的构造函数来初始化派生类对象中的基类成员。可隐式的调用基类构造函数 , 也可在派生类的构造函数中通过给基类提供初始化值(成员初始化值列表)明确的调用构造函数。
构造函数调用顺序: 先执行基类的构造函数 -> 派生类构造函数 析构函数调用顺序正好相反。 派生类不继承基类的构造函数、析构函数和赋值运算符,但派生类的构造函数和赋值运算符能调用基类的构造函数和赋值运算符。
类指针:指向基类的指针,指向派生类的指针。
两者关系 可以直接用基类指针引用基类对象 可以直接用派生类指针引用派生类对象 可以用基类指针引用一个派生类对象, 但只能引用基类成员。 用派生类指针引用基类对象,绝对不行。 必须先强制转换为基类指针
例一:基类和派生类的构造函数说明:在继承关系中构造函数执行顺序。
例二:指向类的指针说明: int *p; // 指向整型的指针 P 。
例三:对不同的类使用相同的指针说明:程序从头到尾始终只用一个“万能”指针指引一切。
例四:使用指针时基类和派生类名字的冲突说明:如果基类和派生类中有同名函数,会怎样呢?
多态性是面向对象的核心,它的最主要的思想就是可以采用多种形式的能力,通过一个用户名字或者用户接口完成不同的实现。通常多态性被简单的描述为“一个接口,多个实现”。在 C++里面具体的表现为通过基类指针访问派生类的函数和方法。
动态联编: 联编就是将模块或者函数合并在一起生成可 执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。在编译阶段就将函数实现和函数调用关联起来称之为静态联编,静态联编在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。反之在程序执行的时候才进行这种关联称之为动态联编,动态联编对成员函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。
换一种说法:如果使用基类指针访问派生类中的同名函数,希望执行的是派生类中的成员函数,怎样实现?
下面我们看一个静态联编的例子:#include <iostream.h> class shape{ public: void draw(){cout<<“I am shape
"<<endl;} void fun(){draw();} };class circle:public shape { public: void draw(){cout<<“I am circle
"<<endl;} }; main(){ circle *oneshape; oneshape->fun(); }
程序的输出结果?
程序的输出结果我们希望是 "I am circle"
造成这个结果的原因是静态联编。解释: 静态联编需要在编译时候就确定函数的实现,但事实上编译器在仅仅知道 shape 的地址时候无法获取正确的调用函数,它所知道的仅是 shape::draw() ,最终结果只能是 draw操作束缚到 shape 类上。产生 "I am shape" 的结果就不足为奇了。
事实上却输出了“ I am shape”
为了能够引起动态联编,我们只需要将需要动态联编的函数声明为虚函数即可。动态联编只对虚函数起作用。我们在通过基类而且只有通过基类访问派生类的时候,只要这个基类中直接的或者间接(从上层继承)的包含虚函数,动态联编将自动唤醒。下面我们将上面的程序稍微改一下。 1. #include <iostream.h> 2. class shape{ 3. public: 4. virtual void draw(){cout<<"I am shape"<<endl;} 5. void fun(){draw();} 6. }; 7. class circle:public shape{ 8. public: 9. void draw(){cout<<"I am circle"<<endl;} 10. }; 11. main(){ 12. shape *x; 13. x = new circle;14. x->draw();15. }程序执行得到了正确的结果 "I am circle" 。
关键
动态联编过程 : 编译器在执行过程中遇到 virtual 关键字的时候,将自动安装动态联编需要的机制,首先为这些包含 virtual 函数的类(注意不是类的实例) --即使是祖先类包含虚函数而本身没有 --建立一张虚拟函数表 VTABLE 。在这些虚拟函数表中,编译器将依次按照函数声明次序放置类的特定虚函数的地址。同时在每个带有虚函数的类中放置一个称之为 vpointer 的指针,简称 vptr ,这个指针指向这个类的 VTABLE 。关于虚拟函数表,有几点必须声明清楚:1. C++ 编译时候编译器会在含有虚函数的类中加上一个指向虚拟函数表的指针 vptr 。2. 从一个类别诞生的每一个对象,将获取该类别中的 vptr 指针,这个指针同样指向类的 VTABLE 。
因此类、对象、 VTABLE 的层次结构可以用下图表示。其中 X类和 Y 类的对象的指针 都指向了 X,Y 的虚拟函数表,同时 X,Y 类自身也包含了指向虚拟函数的指针。
#include <iostream.h > class shape {public: virtual void draw(){cout<<"shape::draw()"<<endl;} ;virtual void area() {cout<<"shape::area()"<<endl; } ; void fun() {draw(); area(); } ; }; class circle:public shape{public: void draw() {cout<<"circle::draw()"<<endl;}; virtual void adjust() {cout<<"circle::adjust()"<<endl;}; }; main(){ shape oneshape; oneshape.fun(); circle circleshape; shape & baseshape=circleshape; baseshape.fun(); } 10.9
基类执行自己的成员函数
基类指针指向派生类对象
纯虚函数和抽象类
1 、基类中的纯虚函数声明如下: Virtual type function_name( 参数) = 0;这样定义意味着,谁继承了该函数所在的类,谁就要负责实现该函数。2 、如果一个类的定义中只包含纯虚函数,那么这个类称为抽象类。
例一:静态联编说明:静态联编时,函数的选择由指针决定
例二:动态联编说明:
例三:动态联编说明:程序对函数的选择
例四:使用虚拟函数说明:有什么特别之处吗?
例五:多态
从面向对象过度到面向从面向对象过度到面向 COMCOM
COMCOM 是什么?是什么?
COMCOM 不是什么?不是什么?
使用组件的优点:1. 应用程序的定制2. 组件库3. 分布式组件
对组件的需求:1 、动态链接2 、消息封装
COM 是一种跨应用和语言共享二进制代码的方法。
COM 明确指出了二进制模块( DLLs 和 EXEs )必须被编译成与指定的结构匹配。这个标准也确切地规定了在内存中如何组织 COM 对象。
COM 定义的二进制标准还必须独立于任何编程语言(如 C++ 中的命名修饰)。一旦满足了这些条件,就可以轻松地从任何编程语言中存取这些模块。由编译器负责所产生的二进制代码与标准兼容。这样使后来的人就能更容易地使用这些二进制代码。
使用和处理 COM 对象每一种语言都有其自己处理对象的方式。例如, C++ 是在栈中创建对象,或者用 new 动态分配。因为 COM 必须独立于语言,所以 COM库为自己提供对象管理例程。下面是对 COM 对象管理和 C++ 对象管理所做的一个比较:
创建一个新对象C++ 中,用 new 操作符,或者在栈中创建对象。COM 中,调用 COM库中的 API 。
删除对象C++ 中,用 delete 操作符,或将栈对象踢出。COM 中,所有的对象保持它们自己的引用计数。调用者必须通知对象什么时候用完这个对象。当引用计数为零时, COM 对象将自己从内存中释放。
由此可见,对象处理的两个阶段:创建和销毁,缺一不可。当创建 COM对象时要通知 COM库使用哪一个接口。如果这个对象创建成功, COM库返回所请求接口的指针。然后通过这个指针调用方法,就像使用常规 C++对象指针一样。
当你调用 CoCreateInstance() 时,它负责在注册表中查找 COM服务器
的位置,将服务器加载到内存,并创建你所请求的 coclass 实例。以下是一个调用的例子,创建一个 CLSID_ShellLink 对象的实例并请
求指向这个对象 IShellLink 接口指针。HRESULT hr; IShellLink* pISL; hr = CoCreateInstance ( CLSID_ShellLink, // coclass 的
CLSID NULL, // 不是用聚合 CLSCTX_INPROC_SERVER, // 服务器类型 IID_IShellLink, // 接口的 IID (void**) &pISL ); // 指向接口的指
针 if ( SUCCEEDED ( hr ) ) { // 用 pISL 调用方法 } else { // 不能创建 COM 对象, hr 为出错代码 }
创建 COM 对象为了创建 COM 对象并从这个对象获得接口,必须调用 COM库的 API 函数, CoCreateInstance() 。其原型如下:
HRESULT CoCreateInstance ( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID* ppv );
参数解释:rclsid : coclass 的 CLSID ,例如,可以传递 CLSID_ShellLink创建一个 COM 对象来建立快捷方式。pUnkOuter :这个参数只用于 COM 对象的聚合,利用它向现有的coclass添加新方法。参数值为 null 表示不使用聚合。dwClsContext :表示所使用 COM服务器的种类。最简单的 COM服务器,为一个进程内( in-process ) DLL ,所以传递的参数值为CLSCTX_INPROC_SERVER 。注意这里不要随意使用 CLSCTX_ALL(在 ATL 中,它是个缺省值),因为在没有安装 DCOM 的 Windows95系统上会导致失败。riid :请求接口的 IID 。例如,可以传递 IID_IShellLink获得 IShellLink接口指针。ppv :接口指针的地址。 COM库通过这个参数返回请求的接口。
当你调用 CoCreateInstance() 时,它负责在注册表中查找 COM服务器的位置,将服务器加载到内存,并创建你所请求的 coclass实例。以下是一个调用的例子,创建一个 CLSID_ShellLink 对象的实例并请求指向这个对象 IShellLink 接口指针。
HRESULT hr; IShellLink* pISL; hr = CoCreateInstance ( CLSID_ShellLink, // coclass 的
CLSID NULL, // 不是用聚合 CLSCTX_INPROC_SERVER, // 服务器类型 IID_IShellLink, // 接口的 IID (void**) &pISL ); // 指向接口的
指针 if ( SUCCEEDED ( hr ) ) { // 用 pISL 调用方法 } else { // 不能创建 COM 对象, hr 为出错代码 }
删除 COM 对象 前面说过,你不用释放 COM 对象,只要告诉它们你已经用完对象。 IUnknown 是每一个 COM 对象必须实现的接口,它有一个方法, Release() 。调用这个方法通知 COM 对象你不再需要对象。一旦调用了这个方法之后,就不能再次使用这个接口,因为这个 COM 对象可能从此就从内存中消失了。如果你的应用程序使用许多不同的 COM 对象,因此在用完某个接口后调用 Release()就显得非常重要。如果你不释放接口,这个 COM 对象(包含代码的 DLLs )将保留在内存中,这会增加不必要的开销。如果你的应用程序要长时间运行,就应该在应用程序处于空闲期间调用CoFreeUnusedLibraries() API 。这个 API将卸载任何没有明显引用的 COM服务器,因此这也降低了应用程序使用的内存开销。继续用上面的例子来说明如何使用 Release() :
// 像上面一样创建 COM 对象, 然后,
if ( SUCCEEDED ( hr ) ) { // 用 pISL 调用方法 // 通知 COM 对象不再使用它 pISL->Release(); }
基本接口—— IUnknown每一个 COM 接口都派生于 IUnknown 。这个名字有点误导人,其中没有未知( Unknown )接口的意思。它的原意是如果有一个指向某 COM 对象的IUnknown 指针,就不用知道潜在的对象是什么,因为每个 COM 对象都实现 IUnknown 。
IUnknown 有三个方法:
AddRef() – 通知 COM 对象增加它的引用计数。如果你进行了一次接口指针的拷贝,就必须调用一次这个方法,并且原始的值和拷贝的值两者都要用到。Release() – 通知 COM 对象减少它的引用计数。
QueryInterface() – 从 COM 对象请求一个接口指针。当 coclass 实现一个以上的接口时,就要用到这个方法。
当你用 CoCreateInstance()创建对象的时候,你得到一个返回的接口指针。如果这个 COM 对象实现一个以上的接口(不包括 IUnknown ),你就必须用QueryInterface()方法来获得任何你需要的附加的接口指针
从从 C++C++ 到到 COMCOM————客户重用客户重用 C++C++ 对象对象
C++C++客户重用客户重用 C++C++ 对象对象
功能介绍: 用字符串数组模拟数据库管理系统的工作原理。 实现对“数据库”的建立、删除 读、写 表或记录的定位实现方法: 定义 DB 类,将定义类的 .h文件单独放在一个文件夹中,假装自己是接口。文件夹起名为 interface 。 实现 DB 类,将实现类的 .CPP文件单独放在一个文件夹中,文件夹起名为 object 。 建一个 VC++工程,将上面两个文件加入工程,增加菜单映射函数,实现菜单功能。
生成 C++ 对象 CDB (Dbsev.h)class CDB {public:
HRESULT Read(short nTable, short nRow, LPTSTR lpszData);HRESULT Write(short nTable, short nRow, LPCTSTR lpszData);
HRESULT Create(short &nTable, LPCTSTR lpszName);HRESULT Delete(short nTable);
HRESULT GetNumTables(short &nNumTables);HRESULT GetTableName(short nTable, LPTSTR lpszName);HRESULT GetNumRows(short nTable, short &nRows);
private:CPtrArray m_arrTables; // Array of pointers to CStringArray (the database)CStringArray m_arrNames; // Array of table names
public:~CDB();
};#endif
CDB 类的实现文件 DBsrv.cpp:#include "stdafx.h"#include "..\Interface\DBsrv.h"
#define new DEBUG_NEW
// Database objectHRESULT CDB::Read(short nTable, short nRow, LPTSTR lpszData) { CStringArray *pTable; pTable=(CStringArray*) m_arrTables[nTable]; lstrcpy (lpszData, (*pTable)[nRow]); return NO_ERROR;}
HRESULT CDB::Write(short nTable, short nRow, LPCTSTR lpszData) { CStringArray *pTable; pTable=(CStringArray*) m_arrTables[nTable]; pTable->SetAtGrow(nRow, lpszData); return NO_ERROR;}
HRESULT CDB::Create(short &nTable, LPCTSTR lpszName) { CStringArray *pTable=new CStringArray; nTable=m_arrTables.Add(pTable); m_arrNames.SetAtGrow(nTable, lpszName); return NO_ERROR;}
HRESULT CDB::Delete(short nTable) { CStringArray *pTable; pTable=(CStringArray*) m_arrTables[nTable]; delete pTable; m_arrTables[nTable]=NULL; m_arrNames[nTable]=""; if (nTable==m_arrTables.GetSize()-1) {
m_arrTables.RemoveAt(nTable);m_arrNames.RemoveAt(nTable);
} return NO_ERROR;}……
客户程序1 、创建客户程序,起名为 DB 。2 、添加菜单项:• 建表:添加一个名称为“ Testing” 的表到文档的数据库对象中。• 写表:写一个字符串到新表的第一行。• 读表: 读出新表第一行的内容并放在 CDBDoc::m_csData 中,然后 由 CDBView将它显示在窗口客户区。3 、实现菜单函数4 、显示读表内容 pDC->TextOut(10,10,pDoc->m_csData);5 、 添加对象代码
程序运行效果演示程序运行效果演示
将将 C++C++ 对象打包成对象打包成 DLL——DB_cppdllDLL——DB_cppdll
要将对象的实现封装成 DLL ,必须考虑如下事情:• 成员函数的引出;• Unicode/ASCII兼容性。
引出函数的一个简单方法是用 _declspec(dllexport)例如: _declspec(dllexport) int MyFunction(int n);
_declspec(dllexport) 可用于任何函数,包括类的成员函数,它可以告诉编译器将入口放进引出函数表中。
要引出 CDC 类中的所有成员函数,只需在每个成员函数之前加上 _declspec(dllexport) 。
Unicode/ASCII兼容 问题的由来: 所有与 DB 工程相关的例程都可以创建成使用Unicode 和使用 ASCII 。但是有一些函数的参数是字符串类型,以 Unicode 或 ASCII形式编译,得到的二进制文件将有所不同。所以应将所有函数参数标准化为 Unicode ,因为 Unicode 是 ASCII 的超集。
步骤一:修改接口文件
#define DEF_EXPORT _declspec(dllexport)
class CDB { // Interfaces public:DEF_EXPORT Read(short nTable, short nRow, LPWSTR lpszData);DEF_EXPORT Write(short nTable, short nRow, LPCWSTR lpszData);
// Implementation private: CPtrArray m_arrTables; // pointers to CStringArray (the "database") CStringArray m_arrNames; // Array of table names
public:~CDB();};
• 为 CDB 类的每个成员函数添加 _declspec(dllexport) 声明。• 添加成员函数 Release(),该函数在对象不再被使用删除自己;• 声明类厂 CDBSrvFactory;• 声明返回类厂对象的引出函数 DllGetClassFactoryObject 。
步骤二:修改对象程序1 、创建 DLL工程框架 Win32 Dynamic-Link Library 。起名为 DB 。2 、添加对象文件 DBSrv.cpp 和 stdafx.h 到工程。3 、修改对象实现文件。在 CDB 对象的实现文件 DBSrv.cpp 中添加 CDB::Release() 的实现代码。
ULONG CDB::Release() {
delete this;return 0;
}
4 、 实现 CDBSrvFactory 。新建 DBSrvFactory.cpp及对象的引出函数DllGetClassFactoryObject 。5 、 将参数标准化为 Unicode.6 、 创建程序。生成引入库文件( .LIB) 和动态链接库文件 (.DLL) 。
步骤三:修改客户程序1 、修改对象删除方式。将 CDBDoc::~CDBDoc() 中的 delete m_pDB 改成 m_pDB->Release()2 、 通过类厂创建对象 CDB 。修改 CDBDoc::OnNewDocment()3 、 将参数标准化为 Unicode 。4 、 连接 DLL 。5 、 创建客户程序。将 DB.DLL拷贝到客户程序所在目录下,编译。
运行程序,观看效果运行程序,观看效果
过过渡渡
问题:私有成员变量被暴露问题:私有成员变量被暴露
解决方案:抽象基类 即,将“接口”头文件中定义的成员函数定义成虚函数,将成员变量删除。 将 CDB改成 IDB, CDBSrvFactory改成IDBSrvFactory 。
第二步:修改对象程序• 增加头文件 DBSRVIMP.H• 其中包含 dbsrv.h• 其他部分也做相应修改
第三步:修改对象实现文件• 将包含的头文件 dbsrv.h改成 dbsrvimp.h#include "stdafx.h"#include "DBsrvImp.h“HRESULT CDBSrvFactory::CreateDB(IDB** ppvDBObject) {
*ppvDBObject=(IDB*) new CDB; return NO_ERROR;
}ULONG CDBSrvFactory::Release() {
delete this;return 0;
}HRESULT DEF_EXPORT DllGetClassFactoryObject(IDBSrvFactory ** ppObject) { *ppObject=(IDBSrvFactory*) new CDBSrvFactory; return NO_ERROR; }
第四步:修改客户程序• 将 CDBDoc 的数据成员类型由 CDB*改成 IDB*public:
IDB *m_pDB;• 在 CDBDoc::OnNewDocument 函数中,将 CDBSrvFactory*改成 IDBSrvFactory*第五步: 新生成 DLL ,拷贝到客户程序下,运行效果和原来一样!
改由改由 COMCOM 库装载库装载 C++C++ 对象对象
前面的示例中, DLL声明了一个入口点 DllGetClassFactoryObject,客户程序调用此函数可以获得类厂对象,再由类厂创建真正的对象 DB 。
这样做隐含的问题:如何在一个 DLL 中实现多个对象(类)。解决办法:• 为每一个准备引出的类提供一个入口点;• 给一个标准入口点传递一个额外参数,表明所需要的类。
事实上, COM正是采用的第二种做法!它使用用统一的类厂获取函数:STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppObject)
REFCLSID rclsid 是个 128位的二进制数字标识,做为类标识。REFIID riid 也是个 128位的二进制数字标识,做为类的接口标识。void** ppObject 是返回的对应类的类厂指针。现在的例子实际上只有一个接口,如果一个类中嵌有多个类,那么内部的每一个类就是一个接口,这就是为什么除了类标识还要有接口标识的原因。
第一步:修改接口文件• 在 dbsrv.h 中增加类ID和接口ID 的说明• 删除 DllGetClassFactoryObject ,为使用统一的 DllGetClassObject做准备
第二步: 修改对象程序• 在 dbsrvfact.cpp 中定义 GUID 。•在 dbsrvfact.cpp 用 DllGetClassObject 替换DllGetClassFactoryObject
注意: DllGetClassObject 在系统提供的 objbase.h 中已经声明所以不用在申明。
第三步:手工注册• 运行 Regedit.exe,打开 HKEY_CLASSES_ROOT\CLSID• 增加一个子键 ,名称为 30DF3430-0266-11cf-BAA6-00AA003E0EED……
第四步 : 修改客户程序• 调用 COM库函数创建对象• 初始化COM库 • 增加 ID 定义
将将 C++C++ 对象变成对象变成 COMCOM 对象对象最后
妈呀,总算到头了
要将 C++ 对象变成一个真正的 COM 对象只要实现如下操作 :• 实现接口的引用计数。• 对象容许实现多个接口。• 类厂对象使用标准的 IClassFactory 。• 使用 _stdcall 调用约定( COM 对象在 Win32 下采用的标准调用约定)。• 实现 DLL 动态卸载。• 实现对象自注册。
比较枯燥的几个重要概念:1 、引用计数 ULONG AddRef(); ULONG Release();2 、多接口 如果每次都通过 IID 和 CoGetClassObject 调用接口,系统开销比较大为了方便在一个 COM里多个接口之间转换,对象可以提供一个接口查询函数: HRESULT QueryInterface(RIID riid, void ** ppObj);3 、 IUnKnown 接口 实现以上三个函数,定义在一个类里。其他接口继承这个类。4 、标准类厂接口 IClassFactory,改造 CreateDB(IDB *ppObj) 为:CreateInstance
5 、自动注册调用 DllRegisterServer 和 DllUnRegisterServer
具体操作如下:第一步:修改接口文件修改 IDB删除类厂说明和 IID_IDBSrvFactory 。
第二步:修改对象程序修改原来类厂有关说明增加 QueryInterface 、 AddRef 和 Release 三个成员函数实现QueryInterface 、 AddRef 和 Release
实现CreateInstance 等。编译后,注册 DB.DLL( 使用 regsvr32)
第三步:修改客户程序定义 IID修改对象创建过程
第一次使用自己的第一次使用自己的 COMCOM 组件组件
使用使用 ATLATL 开发开发 COMCOM 组件组件
创建并测试组件创建并测试组件