770 likes | 938 Views
从面向对象到面向 COM. 主讲 叶长青 华东师大教育信息技术学系 2006-3-1 丽娃河畔 ycq@deit.ecnu.edu.cn. 本课的内容及目标. 内容: 面向对象的一般概念 从面向对象到面向 COM COM 组件技术 目标: 了解程序设计技术的发展动向 提升程序设计能力 开阔专业视野. 课程参考书目. 《 COM 原理与应用》 《 COM 本质论》 《 COM 技术内幕》 《 Advanced CORBA Programming with C++ 》. 面向对象的基本概念. 程序设计的发展历程.
E N D
从面向对象到面向COM 主讲 叶长青 华东师大教育信息技术学系 2006-3-1 丽娃河畔 ycq@deit.ecnu.edu.cn
本课的内容及目标 • 内容: • 面向对象的一般概念 • 从面向对象到面向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,N • 5 V(I,J) = (I/J)*(J/I)
小 谁 最 X,Y,Z SMALL = X IF (Y .LT. SMALL) SMALL = Y IF (Z .LT. SMALL) SMALL = Z IF (X .LT. Y) GOTO 30 IF (Y .LT. Z) GOTO 50 SMALL = Z GOTO 70 30 IF (X .LT. Z) GOTO 60 SMALL = Z GOTO 70 50 SMALL = Y GOTO 70 60 SMALL = X 70 CONTINUE
注重“重用性”的问题。 • 面向对象 • 注重“标准化元件”的阶段。 • 面向组件 • ?软件企业分工细化阶段
有关术语 类是具有相同属性特征事物的集合。计算机专业语境下,类是封装了状态(变量)和操作(对变量处理的过程和函数)的抽象数据类型。(对应于标准数据类型) 对象是实例,它反映了具体的事物。 鸟,是类的概念。麻雀是鸟类的一个实例。一只麻雀? 对象由类来定义 一个对象可以与多个具有继承关系的类相联系,即: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
继承的概念及重要性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 shape” 造成这个结果的原因是静态联编。 解释: 静态联编需要在编译时候就确定函数的实现,但事实上编译器在仅仅知道shape的地址时候无法获取正确的调用函数,它所知道的仅是shape::draw(),最终结果只能是draw操作束缚到shape类上。产生"I am shape"的结果就不足为奇了。
为了能够引起动态联编,我们只需要将需要动态联编的函数为了能够引起动态联编,我们只需要将需要动态联编的函数 • 声明为虚函数即可。动态联编只对虚函数起作用。我们在通过基类 • 而且只有通过基类访问派生类的时候,只要这个基类中直接的或者 • 间接(从上层继承)的包含虚函数,动态联编将自动唤醒。下面我 • 们将上面的程序稍微改一下。 • #include <iostream.h> • class shape{ • public: • virtual void draw(){cout<<"I am shape"<<endl;} • void fun(){draw();} • }; • class circle:public shape{ • public: • void draw(){cout<<"I am circle"<<endl;} • }; • main(){ • shape *x; • x = new circle; • x->draw(); • } • 程序执行得到了正确的结果"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、如果一个类的定义中只包含纯虚函数,那么这个类称为抽象类。
例一:静态联编 说明:静态联编时,函数的选择由指针决定 例二:动态联编 说明: 例三:动态联编 说明:程序对函数的选择 例四:使用虚拟函数 说明:有什么特别之处吗? 例五:多态
COM是什么? COM不是什么?
使用组件的优点: • 应用程序的定制 • 组件库 • 分布式组件 对组件的需求: 1、动态链接 2、消息封装
COM是一种跨应用和语言共享二进制代码的方法。COM是一种跨应用和语言共享二进制代码的方法。 COM明确指出了二进制模块(DLLs和EXEs)必须被编译成与指定的结构匹配。这个标准也确切地规定了在内存中如何组织COM对象。 COM定义的二进制标准还必须独立于任何编程语言(如C++中的命名修饰)。一旦满足了这些条件,就可以轻松地从任何编程语言中存取这些模块。由编译器负责所产生的二进制代码与标准兼容。这样使后来的人就能更容易地使用这些二进制代码。
使用和处理COM对象每一种语言都有其自己处理对象的方式。例如,C++是在栈中创建对象,或者用new动态分配。因为COM必须独立于语言,所以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服务器当你调用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()。其原型如下:创建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接口指针。当你调用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++对象 功能介绍: 用字符串数组模拟数据库管理系统的工作原理。 实现对“数据库”的建立、删除 读、写 表或记录的定位 实现方法: 定义DB类,将定义类的.h文件单独放在一个文件夹中,假装自己是接口。文件夹起名为interface。 实现DB类,将实现类的.CPP文件单独放在一个文件夹中, 文件夹起名为object。 建一个VC++工程,将上面两个文件加入工程,增加菜单 映射函数,实现菜单功能。