面向对象与现代软件组件系统
(V0.01)
本文档从较低层的角度分析面向对象和现代软件组件系统,文档中部分看法与目前流行看法可能有不同,仅代表笔者个人观点。本文档适合对面向对象或者COM有一定基础的读者阅读。
本文档中的“属性(Property)”一词,如果未特殊说明“COM接口属性”、“自动化属性”等与COM/ActiveX相关的“属性”,相当于C++中的成员变量(Member Variable)或者Object Pascal/Delphi中的字段(Field),通常泛指对象的特征,与COM/ActiveX中通常相当于一对Get/Set方法的属性不同。
本文档中,未经特殊说明,不加区分地使用“属性(Property)”/“成员变量(Member Variable)”和“方法(Method)”/“成员函数(Member Function)”词汇。
本文档中的COM,未经特殊说明,泛指COM/DCOM/MTS/COM+ 1.0。
本文档中的源程序,未经特殊说明,均使用Visual C++ .NET(Visual C++ 7.0)开发。
第1篇 面向对象
第1章 对象和类的本质
1.1.对象(Object)的本质
许多面向对象程序设计或者C++的书上都会这样说:“对象有属性(Property)和方法(Method)。”
但是在真实世界中,说“对象”有“属性”说得通,但是“对象”有“方法”吗?假设有一辆自行车,把自行车看作“对象”,那么“自行车对象”确实有“属性”,例如自行车是26的还是28的,是男式的还是女式的,是黑色的还是银灰色的……这都是实实在在的“属性”,也就是具体对象的特征。但是如果说“自行车对象”有“方法”,这话可就有些不通了,例如说自行车对象有“行驶”或者“停止”“方法”,那倒要问问:谁能从一辆自行车上“看出”它有“行驶”或者“停止”“方法”?除非他有特异功能!
属性是静态的(但是不是不可变的),反映了一个具体对象的特征,但是方法是动态的,是对象的使用者(或者调用者)对于对象具体的操作,也就是说方法是针对对象的,只能说方法可用于(或者适合)对象,也可以说对象支持方法,不应该说对象有方法。对象只有属性(或者特征),但是对象支持方法,而且通常方法可以操作属性。
一般程序员不容易接受笔者的看法,因为编写程序时,是通过对象调用方法的,为此笔者编写了一个简单的C++实例程序,试图说明该问题。程序通过简单的类抽象模拟“神舟”飞船(仿照“Inside Visual C++”/《Visual C++技术内幕》一书),并创建模拟飞船对象。程序工程是简单的Win32控制台(Console)工程,在命令行(MS—DOS方式)下执行,主程序源程序如下(ShenZhou_ClassAndObject.cpp):
// ShenZhou_ClassAndObject.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
file://模拟“神舟”飞船类
class CShenZhou
{
public:
int m_position;
CShenZhou();
void Fly(); file://模拟飞船飞行
int GetPosition(); file://模拟航天测控(获取飞船位置)
};
CShenZhou::CShenZhou()
{
m_position=100;
}
void CShenZhou::Fly()
{
_tprintf(_T("Fly!\n"));
}
int CShenZhou::GetPosition()
{
return m_position;
}
int _tmain(int argc, _TCHAR* argv[])
{
CShenZhou ShenZhou1; file://模拟飞船对象
_tprintf(_T("The size of the ShenZhou1 object: %d\n"),sizeof(ShenZhou1));
ShenZhou1.Fly();
_tprintf(_T("The position of the ShenZhou 1 spaceship: %d\n"),ShenZhou1.GetPosition());
return 0;
}
程序运行结果如下:
The size of the ShenZhou1 object: 4
Fly!
The position of the ShenZhou 1 spaceship: 100
有1个成员变量和3个成员函数(其中1个是构造函数)的类的对象,大小(所占内存空间)只有4字节,正好相当于1个成员变量(int数据类型)的大小,显然,成员函数并不包含在对象中。
上例可以证明:对象中只包含属性,并不包含方法。
那么为什么编写程序时,是通过对象调用方法呢?将上例的工程编译成EXE文件(使用Release配置),然后用IDA Pro V4.17反汇编,检查部分反汇编程序(部分注释是笔者加的):
……
; CShenZhou类的构造函数
sub_0_401000 proc near ; CODE XREF: sub_0_401030+5p
mov eax, ecx
mov dword ptr [eax], 64h
retn
sub_0_401000 endp
……
; CShenZhou::Fly成员函数
sub_0_401010 proc near ; CODE XREF: sub_0_401030+1Dp
push offset aFly ; "Fly!\n"
call sub_0_401070
pop ecx
retn
sub_0_401010 endp
……
; CShenZhou::GetPosition成员函数
sub_0_401020 proc near ; CODE XREF: sub_0_401030+26p
mov eax, [ecx]
retn
sub_0_401020 endp
……
; _tmain函数
sub_0_401030 proc near ; CODE XREF: start+16Bp
push ecx ; 给ShenZhou1对象分配内存空间(4字节)
lea ecx, [esp+0]
call sub_0_401000 ; 调用CShenZhou类的构造函数
push 4
push offset aTheSizeOfTheSh ; "The size of the ShenZhou1 object: %d\n"
call sub_0_401070
add esp, 8
lea ecx, [esp+0]
call sub_0_401010 ; 调用CShenZhou::Fly成员函数
lea ecx, [esp+0]
call sub_0_401020 ; 调用CShenZhou::GetPosition成员函数
push eax
push offset aThePositionOfT ; "The position of the ShenZhou 1 spaceshi"...
call sub_0_401070
xor eax, eax
add esp, 0Ch
retn
sub_0_401030 endp
……
检查上述反汇编程序后,可以发现以下几点:
⑴编译成二进制代码(机器代码)后,对象和普通数据类型变量并没有区别,都是一段数据(或者内存空间)。
⑵对象中只包含成员变量,并不包含成员函数。
⑶类的成员函数(包括构造函数、析构函数等),编译成二进制代码后,与一般的全局函数并没有本质区别,都是机器语言(或者汇编语言)过程,使用CALL指令调用。
⑷通过对象调用类的成员函数(对象支持的成员函数),编译成二进制代码后,与调用一般的全局函数并没有本质区别,都是直接使用CALL指令对相应的机器语言过程地址调用(静态的)。
⑸类的成员函数与一般的全局函数区别在于通过对象调用类的成员函数时,要将对象的地址(指针/引用)传递给类的成员函数(通过ECX寄存器传递)。
上述事实可以说明以下几点:
⑴对象中只包含属性,并不包含方法。对象可以认为是包含属性数据的一段数据。
⑵编译后的二进制代码中,类(或者类的定义)实际上消失了,类只在源程序中存在。
⑶在运行时只通过对象本身(只包含属性),实际上一般是调用不了方法的,在源程序中通过对象调用方法,编译后生成的二进制代码,一般是直接对方法相应地址的调用。这种调用是一种直接调用(静态的),称为编译时绑定(Compile-time Binding)或者前期绑定。
⑷方法针对对象,通过对象调用方法时,实际上是将对象的引用(或者指针)传递给方法,或者说实际上方法接受对象的引用作为一个隐含参数,方法针对对象操作。
以后为了简单起见,按照习惯,有时仍然称对象(或者类)的属性和方法,以及调用对象的方法(或者调用对象),不过一定要记住:实际上对象只有属性,对象没有方法,但是对象支持方法。
1.2.类(Class)的本质
面向对象程序设计中,通常是先定义类,然后通过类创建对象(类的实例化)。但是在真实世界中,先存在对象,然后再抽象出类。
通常说来,类是对象的抽象,是一类对象属性和方法的抽象(或者总结),类是一个抽象的概念,例如抽象的“自行车类”是各种“自行车对象”属性和方法的总结,抽象的“自行车类”可以仅仅是一个印象,也可以写在纸上说明,但是它不是真正的“自行车对象”。对象是类的实例(Instance),或者说类可以实例化(Instantiating)成为对象,例如“自行车对象”是“自行车类”的实例。
但是从另一个角度看,对象并没有方法,对象只是支持方法而已,所以也可以说,类将对象的属性和支持的方法捆绑在一起,也就是将对象的特征和能支持的操作捆绑在一起,并加以总结,构成一个抽象的概念或者数据类型。
没有类,对象能存在吗?当然可以,例如没有“自行车类”,但是“自行车对象”依然存在,但是这个对象不是传统面向对象意义上的对象。很多使用了面向对象思想的系统(例如Windows)中存在许多“对象”,例如窗口(Window)对象、GDI对象等,确实可以认为是对象,有“属性”,支持“方法”(例如有专门针对窗口对象、GDI对象等的Windows API),但是并没有相应的类。
面向对象编程语言(C++、Object Pascal等)中,通常认为类分为两部分,类的定义和类的实现。类的定义是一个抽象的,源程序级的概念,在编译后的二进制代码中并不存在类的定义。类的实现实际上是对类中定义的方法的实现,但是对象是没有方法的,对象只支持方法(针对对象的操作),那么方法的实现(类的实现)实际上只是实现了针对对象的操作,当源程序编译成二进制代码后,方法的实现与全局函数(或者过程)在二进制代码形式上没有实质区别,区别在于方法的实现针对某一具体对象,或者说方法的实现实际上接受某一具体对象(实际上是对象引用/对象指针)作为一个隐含参数。可以认为类实际上只包含类的定义,以后提到的类,一般按照习惯指类的定义和类的实现,但是也可以指类的定义。
既然类(类的定义)是一个抽象的,源程序级的概念,那么通过类的定义(或者类名——类的定义的标识)使用类,实际上是在源程序级使用类。面向对象程序设计中,类的功能实际上就是创建对象(实例化),通过类名创建对象(直接定义对象或者通过C++ new语句等创建对象),实际上也是在源程序级使用类。1.1.中的实例程序的源程序(ShenZhou_ClassAndObject.cpp)中,直接在源程序中通过CShenZhou类定义ShenZhou1对象,CShenZhou类直接以类名的形式出现在源程序中,或者说CShenZhou类以硬编码的形式被使用。
一般在源程序中通过对象调用方法,也必须有类的定义,因为在编译时,要根据类的定义,确定对方法的调用,编译后生成对方法相应地址的调用的二进制代码。
即使类编译成类库,也只是方法的实现(类的实现)编译成了二进制代码,在源程序中创建类的对象还是必须有类的定义,例如C++必须包含(#include指令)包含类的定义的源程序的头文件(例如使用MFC,必须包含MFC头文件,MFC头文件包含大量MFC类的定义的源程序)。
释放对象的内存(删除对象)是简单的内存释放,是语言(源程序)无关的。
少数面向对象编程语言,例如Object Pascal/Delphi,类的定义和类的实现一起被编译成单元(Unit),似乎不存在类的定义的源程序,但是实际上在单元中,仍然存在类的定义的描述(Description),可以认为相当于.NET中的元数据(Metadata)。类的定义的描述虽然不是完全以源程序的形式出现,但是作用与类的定义的源程序相同。可以认为Object Pascal/Delphi的单元是自描述的。
第2章 虚函数和接口
2.1.虚函数(Virtual Function)的本质
虚函数[或者虚拟方法(Virtual Method)]是一种特殊的成员函数,虚函数和重写(Override,某些书上译为重载或者改写)是实现面向对象程序设计中多态性(Polymorphism)的重要方法。在基类中定义的虚函数可以在派生类中被重写,指向基类的对象的指针可以指向派生类的对象,这样可以实现多态性。
那么虚函数的本质是什么?虚函数和普通成员函数有什么区别?笔者编写了一个C++实例程序来说明该问题。程序工程是Win32控制台(Console)工程,在命令行(MS—DOS方式)下执行,主程序源程序如下(ShenZhou_VirtualFunction.cpp):
// ShenZhou_VirtualFunction.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
file://模拟“神舟”飞船类
class CShenZhou
{
public:
int m_position;
CShenZhou();
virtual void __stdcall Fly(); file://模拟飞船飞行
virtual int __stdcall GetPosition(); file://模拟航天测控(获取飞船位置)
};
CShenZhou::CShenZhou()
{
m_position=100;
}
void CShenZhou::Fly()
{
_tprintf(_T("Fly!\n"));
}
int CShenZhou::GetPosition()
{
return m_position;
}
int _tmain(int argc, _TCHAR* argv[])
{
CShenZhou* pShenZhou1; file://指向模拟飞船类的对象的指针
pShenZhou1=new CShenZhou; file://创建模拟飞船对象
_tprintf(_T("The size of the object of the CShenZhou class: %d\n"),sizeof(CShenZhou));
pShenZhou1->Fly();
_tprintf(_T("The position of the ShenZhou 1 spaceship: %d\n"),pShenZhou1->GetPosition());
delete pShenZhou1; file://删除模拟飞船对象
return 0;
}
源程序与1.1.中的实例程序的源程序(ShenZhou_ClassAndObject.cpp)基本相同,主要修改之处是将CShenZhou::Fly成员函数和CShenZhou::GetPosition成员函数改为虚函数,并将其调用方式改为Win32标准函数调用方式(__stdcall),以及将ShenZhou1对象改为pShenZhou1指向对象的指针,并使用new/delete语句创建/删除对象。程序运行结果如下:
The size of the object of the CShenZhou class: 8
Fly!
The position of the ShenZhou 1 spaceship: 100
可以看到,对象大小变成了8字节。将工程编译成EXE文件(使用Release配置),然后用IDA Pro V4.17反汇编,检查部分反汇编程序(部分注释是笔者加的):
……
; CShenZhou类的构造函数
sub_0_401000 proc near ; CODE XREF: sub_0_401030+11p
mov eax, ecx
mov dword ptr [eax], offset off_0_4060EC ; 对象的起始4字节[指向虚函数表的指针(VPTR)]指向虚函数表(VTBL)
mov dword ptr [eax+4], 64h
retn
sub_0_401000 endp
……
; CShenZhou::Fly虚函数
sub_0_401010 proc near ; DATA XREF: .rdata:004060ECo
push offset aFly ; "Fly!\n"
call sub_0_401080
pop ecx
retn 4
sub_0_401010 endp
……
; CShenZhou::GetPosition虚函数
sub_0_401020 proc near ; DATA XREF: .rdata:004060F0o
arg_0 = dword ptr 4
mov eax, [esp+arg_0]
mov eax, [eax+4]
retn 4
sub_0_401020 endp
……
; _tmain函数
sub_0_401030 proc near ; CODE XREF: start+16Bp
push esi
push 8
call sub_0_4010B6 ; 创建CShenZhou类的对象,给对象分配内存空间(8字节)。
add esp, 4
test eax, eax ; EAX寄存器指向创建的CShenZhou类的对象
jz short loc_0_40104A
mov ecx, eax
call sub_0_401000 ; 调用CShenZhou类的构造函数
mov esi, eax ; ESI寄存器指向创建的CShenZhou类的对象(ESI寄存器就相当于pShenZhou1指针,这是优化编译的结果)
jmp short loc_0_40104C
……
loc_0_40104A: ; CODE XREF: sub_0_401030+Dj
xor esi, esi
loc_0_40104C: ; CODE XREF: sub_0_401030+18j
push 8
push offset aTheSizeOfTheOb ; "The size of the object of the CShenZhou"...
call sub_0_401080
mov eax, [esi]
add esp, 8
push esi
call dword ptr [eax] ; 调用CShenZhou::Fly成员函数
mov ecx, [esi]
push esi
call dword ptr [ecx+4] ; 调用CShenZhou::GetPosition成员函数
push eax
push offset aThePositionOfT ; "The position of the ShenZhou 1 spaceshi"...
call sub_0_401080
push esi
call sub_0_4010B1 ;删除CShenZhou类的对象
add esp, 0Ch
xor eax, eax
pop esi
retn
sub_0_401030 endp ; sp = -8
……
; 虚函数表
off_0_4060EC dd offset sub_0_401010 ; DATA XREF: sub_0_401000+2o
dd offset sub_0_401020
……
检查上述反汇编程序后,除了发现成员函数(虚函数)的调用方式改为Win32标准函数调用方式以后,参数传递方式发生了变化,以及成员函数(虚函数)的隐含参数——对象引用/对象指针通过堆栈传递,而不再通过ECX寄存器传递以外,可以发现以下几点:
⑴编译成二进制代码后,对象大小增加了4字节,对象的起始处不再是第1个成员变量,插入了4字节,这4字节是一个指针,创建对象,运行构造函数时,这个指针被初始化,指向一个名为虚函数表(VTBL)的数据结构。这个指针称为指向虚函数表的指针(VPTR)。
⑵类有几个虚函数,虚函数表中就有几项,一个虚函数对应一项,虚函数表中的每一项是一个指针,指向一个虚函数(虚函数相应地址),实际上是一个指向虚函数的指针。
⑶通过对象(或者指向对象的指针)调用虚函数,编译成二进制代码后,是在运行时通过对象中指向虚函数表的指针定位虚函数表,再在虚函数表中定位虚函数对应的项,获取指向虚函数的指针,通过指针间接调用虚函数(动态的)。
上述事实可以说明:
⑴如果类定义了虚函数,那么类的对象的起始处是指向虚函数表的指针,指向虚函数表,虚函数表中的每一项是一个指向虚函数的指针,指向一个虚函数。
⑵通过对象调用虚函数,编译成二进制代码后,并不生成对虚函数相应地址的直接调用的二进制代码,生成的二进制代码在运行时通过对象定位虚函数表,再在虚函数表中获取虚函数对应的指向虚函数的指针,通过指针间接调用虚函数。这种调用是一种间接调用(动态的),称为运行时绑定(Run-time Binding)或者后期绑定。
如果类定义了虚函数,那么在运行时通过类的对象本身(包括虚函数表)就可以调用虚函数,不需要类的定义,也就是说不需要源程序级的类的定义,在运行时通过二进制代码级的类的对象本身,就可以调用类定义的虚函数。这样就相当于解决了语言(源程序)无关的对象的方法的调用问题。
每个定义了虚函数的类,都对应一个自己的虚函数表,虚函数表中指向虚函数的指针指向对应的虚函数(虚函数的实现)。如果某个虚函数被重写,那么基类和派生类对应的虚函数表中指向该虚函数的指针指向不同的虚函数的实现,通过基类的对象和派生类的对象调用该虚函数时,虚函数的实现不同,这就实现了多态性。如果某个类定义了纯虚函数(只有定义而没有实现的虚函数),那么它无法确定对应的虚函数表,因为虚函数表中无法确定指向纯虚函数的指针,那么这个类就是抽象类(Abstract Class),抽象类不能实例化,但是可以定义指向抽象类的对象的指针(但是抽象类的对象并没有意义)。
2.2.接口(Interface)
C++中虽然没有明确地提出接口的概念,不过一般认为如果在基类中定义的虚函数(或者纯虚函数)在派生类中被重写,用指向基类的对象的指针指向派生类的对象,再调用基类中定义的虚函数,那么调用的是派生类中重写的虚函数,相当于虚函数在基类中定义,而在派生类中实现,那么基类就相当于接口了。
实际上,无论通过指向基类对象的指针还是指向派生类对象的指针(空指针都可以)指向派生类的对象,都可以定位到指向虚函数表的指针(对象的起始处),进而定位到虚函数表,那么就可以完成调用虚函数了,本来虚函数表就是派生类对应的虚函数表,自然调用的是派生类中重写的虚函数。
在通过指向基类对象的指针指向派生类的对象,调用派生类中重写的虚函数的情况下,基类的作用只是在编译时,根据基类的定义,编译后生成在虚函数表中获取虚函数对应的指向虚函数的指针(在虚函数表中定位虚函数对应的项,获取指向虚函数的指针)的二进制代码,也就是说在编译时,还是可以根据基类的定义,确定虚函数在虚函数表中对应的项,编译后生成在虚函数表中定位虚函数对应的项,获取指向虚函数的指针的二进制代码,也可以说该工作也可以是编译时绑定的(但是虚函数的调用是运行时绑定的)。这里的基类只是使用基类的定义,基类可以是抽象类。
在虚函数表中定位虚函数对应的项,获取指向虚函数的指针的工作并不一定要通过基类的定义在编译时确定,编译后生成的二进制代码完成,完全可以用其它的方法[例如C/C++的结构(Structure)]定义虚函数表和指向虚函数表的指针,再在编译时绑定,甚至可以让该工作和虚函数的调用一样完全在运行时绑定。
在通过指向基类对象的指针指向派生类的对象,调用派生类中重写的虚函数的情况下,指向基类对象的指针的作用只是定位到指向虚函数表的指针(对象的起始处),因为指向虚函数表的指针正好位于对象的起始处,所以指向基类的指针指向派生类的对象,就相当于指向指向虚函数表的指针(VPTR),也就是说在这种情况下,指向基类对象的指针指向派生类的对象,实际上是指向指向虚函数表的指针。
接口一般可以认为是抽象类,接口中只定义了纯虚函数(称为接口的方法),没有定义成员变量,接口不能实例化,但是可以定义指向接口对象的指针(实际上接口对象没有意义),称为指向接口的指针(接口指针)。从接口派生的类(非接口类)应该重写接口中定义的纯虚函数,称为实现接口的类和类实现的接口的方法。通过指向接口的指针指向实现接口的类的对象,实际上也是指向指向虚函数表的指针。指向接口的指针并非一定要用指向抽象类对象的指针实现,完全可以用其它的方法(例如C/C++的结构)定义虚函数表和指向虚函数表的指针,那么指向指向虚函数表的指针的指针就相当于指向接口的指针。通过指向接口的指针指向实现接口的类的对象,调用类实现的接口的方法,也时运行时绑定的。
笔者编写了一个C++实例程序定义接口和实现指向接口的指针。程序工程是Win32控制台(Console)工程,在命令行(MS—DOS方式)下执行,主程序源程序如下(ShenZhou_Interface.cpp):
// ShenZhou_Interface.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
class IShenZhou
{
public:
virtual void __stdcall Fly()=0; file://模拟飞船飞行
virtual int __stdcall GetPosition()=0; file://模拟航天测控(获取飞船位置)
};
struct CShenZhouVTBL;
file://模拟飞船类的对象中的指向虚函数表的指针结构
struct CShenZhouVPTR
{
CShenZhouVTBL* vptr;
};
file://模拟飞船类对应的虚函数表结构
struct CShenZhouVTBL
{
void (__stdcall *Fly)(CShenZhouVPTR* This);
int (__stdcall *GetPosition)(CShenZhouVPTR* This);
};
file://模拟“神舟”飞船类,实现IShenZhou 接口。
class CShenZhou:public IShenZhou
{
public:
int m_position;
CShenZhou();
virtual void __stdcall Fly();
virtual int __stdcall GetPosition();
};
CShenZhou::CShenZhou()
{
m_position=100;
}
void CShenZhou::Fly()
{
_tprintf(_T("Fly!\n"));
}
int CShenZhou::GetPosition()
{
return m_position;
}
int _tmain(int argc, _TCHAR* argv[])
{
CShenZhou* pShenZhou1; file://指向模拟飞船类的对象的指针
IShenZhou* pIShenZhou1; file://指向IShenZhou 接口的指针
CShenZhouVPTR* pCShenZhouVPTR1; file://指向模拟飞船类的对象中的指向虚函数表的指针的指针
pShenZhou1=new CShenZhou; file://创建模拟飞船对象
file://通过指向IShenZhou 接口的指针调用虚函数
pIShenZhou1=pShenZhou1; file://pIShenZhou1 指针指向模拟飞船对象
pIShenZhou1->Fly();
_tprintf(_T("The position of the ShenZhou 1 spaceship: %d\n"),pIShenZhou1->GetPosition());
file://通过指向模拟飞船类的对象中的指向虚函数表的指针的指针调用虚函数
pCShenZhouVPTR1=(CShenZhouVPTR*)(void*)pShenZhou1; file://pCShenZhouVPTR1 指针指向模拟飞船对象(指向模拟飞船对象中的指向虚函数表的指针)
pCShenZhouVPTR1->vptr->Fly(pCShenZhouVPTR1);
_tprintf(_T("The position of the ShenZhou 1 spaceship: %d\n"),pCShenZhouVPTR1->vptr->GetPosition(pCShenZhouVPTR1));
delete pShenZhou1; file://删除模拟飞船对象
return 0;
}
程序运行结果如下:
Fly!
The position of the ShenZhou 1 spaceship: 100
Fly!
The position of the ShenZhou 1 spaceship: 100
程序中通过C/C++的结构定义模拟飞船类的对象中的指向虚函数表的指针和模拟飞船类对应的虚函数表,使用指向IShenZhou 接口的指针和指向模拟飞船类的对象中的指向虚函数表的指针的指针分别指向同一模拟飞船对象,作为指向接口的指针的两种实现,再分别使用指向接口的指针的这两种实现,调用模拟飞船类实现的IShenZhou接口的方法,调用结果完全一致。这种调用是运行时绑定的,并不时编译时绑定的,调用不需要模拟飞船类的定义。
以后为了简单起见,按照习惯,有时称接口为对象接口;称通过指向接口的指针指向实现接口的类的对象为对象引出指向接口的指针;称通过指向接口的指针指向实现接口的类的对象,调用类实现的接口的方法为通过(对象引出的)指向接口的指针调用对象的接口的方法(或者调用对象)。
2.3.代理接口(Proxy Interface)
从指向接口的指针本身来看,指向接口的指针是指向指向虚函数表的指针的指针,而指向虚函数表的指针指向虚函数表,所以可以认为指向接口的指针间接指向虚函数表。从逻辑上看,可以认为接口(逻辑接口,不是抽象类)就是一组指向方法的指针的集合,以实现调用的运行时绑定。
对象引出指向接口的指针,从逻辑上看,可以认为是对象引出接口。任何应用程序,只要实现虚函数表数据结构和指向虚函数表的指针数据结构,同时实现虚函数表中的每一项(指向虚函数的指针)指向的虚函数,就都可以让指向接口的指针指向指向虚函数表的指针,也就是说都可以引出指向接口的指针,不一定只有对象才能引出指向接口的指针,从逻辑上看,可以认为任何应用程序都可以引出接口。
如果对象引出指向接口的指针,但是对象和调用者不在同一位置(例如不在同一地址空间,甚至不在同一计算机上),那么指向接口的指针和调用者就也不在同一位置,那么如何让调用者通过对象引出的指向接口的指针调用对象的接口的方法呢?
可以使用一个和调用者在同一位置,引出指向相同接口的指针的应用程序,该应用程序实现的接口的方法,完成通过通道(Channel),越过不同位置的边界,通过对象引出的指向接口的指针调用对象的接口的方法,这个应用程序称为代理(Proxy),引出的指向相同接口的指针称为指向代理接口(Proxy Interface)的指针,从逻辑上看,可以认为是代理引出代理接口。调用者可以使用相同的方法,通过代理引出的指向代理接口的指针调用对象的接口的方法。
调用者使用代理引出的指向代理接口的指针代替对象引出的指向接口的指针,相当于对象引出的指向接口的指针被调整(Marshal)成了代理引出的指向代理接口的指针。对象引出的指向接口的指针可以认为是对象的引用(直接引用),那么代理引出的指向代理接口的指针也可以认为是对象的引用(间接引用)。
调用者使用通过对象引出的指向接口的指针调用对象的接口的方法,从逻辑上看,可以认为是调用者使用接口。使用代理和指向代理接口的指针,可以保证无论对象和调用者是否在同一位置,通过指向接口的指针调用对象的接口的方法的方法都一致,这称为位置透明性(Location Transparency)或者位置无关性,从逻辑上看,可以认为位置透明性相当于调用者使用的接口和对象可以不在同一位置,无论对象位于什么位置,调用者使用的接口(或者代理接口)的位置都相同,调用者使用接口(或者代理接口)的方法也都相同。
第3章 类对象
[注:类对象不是类的对象]
3.1.类对象(Class Object)
如果类定义了虚函数或者实现接口,那么在运行时通过类的对象本身(包括虚函数表)或者通过对象引出的指向接口的指针就可以调用虚函数或者对象的接口的方法,这种调用是运行时绑定的,不需要类的定义或者接口的定义,这样就相当于解决了语言(源程序)无关的对象的方法或者对象的接口的方法的调用问题。
但是类(类的定义)是一个抽象的,源程序级的概念,完成面向对象程序设计中类的功能——创建对象(实例化)还是必须在源程序级使用类,或者说以硬编码的形式使用类,做不到语言(源程序)无关的创建对象工作。
类是对象的抽象,但是类本身能否看成对象呢?试把类想象成模子,对象想象成模子制造出的模型,模型固然是对象,但是针对模子来说,模子是不是也可以看成对象呢?回答是肯定的,类本身完全可以看成对象,这个对象至少有一个方法(支持一个方法)——创建对象(实例化),还可以有属性[至少包含类的运行时类型信息,C++中的运行时类型信息(RTTI)实际上就是类本身的属性之一]。
对应一个类,定义和实现一个特殊的类,这个特殊的类至少定义和实现一个方法——创建对应的类的对象,这个特殊的类称为类对象类,它的对象就称为类对象(也称为类信息、类信息对象、运行时类信息等)。用类对象代替对应的类,通过类对象调用创建对应的类的对象的方法,创建对应的类的对象,代替在源程序级使用类创建类的对象,这个过程称为类的实体化(Objectize,这个词是笔者造的)。
对于不同的类,类对象类的定义是相同的,只是实现的创建对应的类的对象的方法不同,可以定义接口(称为类对象接口),将创建对应的类的对象的方法定义成接口方法,类对象类再实现该接口,那么不同的类对象类都实现相同的类对象接口,不同的类对象都引出指向相同的类对象接口的指针,通过指向接口的指针就可以调用类对象的类对象接口的方法。通过对象引出的指向接口的指针调用对象的接口的方法是运行时绑定的,不需要类的定义或者接口的定义,这样就相当于解决了语言(源程序)无关的创建对象工作问题,也就是说通过实体化的类对象引出的指向类对象接口的指针调用类对象的类对象接口的方法,解决了语言(源程序)无关的创建对象工作问题。
典型的类对象的例子就是MFC中的运行时类信息(Runtime Class),通过给一个CObject类的派生类定义运行时类信息,就可以使用RUNTIME_CLASS宏直接通过类名获取运行时类信息,实际上就相当于将类实体化成类对象,MFC应用程序可以直接在源程序级通过类名创建对象,也可以通过在二进制代码级调用运行时类信息(类对象)的方法(CreateObject方法)创建对象,因为对于不同的类,运行时类信息类是相同的,这样解决了一个C++中没有解决的问题:类还没有定义之前,可以编写出创建类的对象的代码,该代码的具体功能(创建何种类的对象)是在二进制代码级决定的(或者说是运行时的或者动态的),并非在源程序级通过具体类名决定(或者说是编译时的、静态的或者硬编码),所以也称为“动态创建对象(这是某些VC/MFC书上的说法)”。
第2篇 现代软件组件系统
第4章 COM的本质
[注:本章内容中的“组件(Component)”一词和“接口(Interface)”一词,未经特殊说明,均指COM组件(COM Component)和COM接口(COM Interface)]
4.1.COM的特性
COM起初是作为OLE(OLE 2)/ActiveX的底层技术推出的,COM是一种组件系统,COM组件是一种软件模块,COM有许多重要的特性,COM的基本特性有:
⑴语言无关性:COM组件是语言(源程序)无关的,COM组件的调用协议是二进制代码级的协议(语言无关的协议)。
⑵位置透明性:调用者对COM组件的调用,与COM组件的位置无关,无论COM组件位于什么位置,调用着调用COM组件的方法都相同。
⑶面向对象的特性:COM是一种面向对象的组件系统,COM组件相当于类,COM组件实例化成COM组件对象,COM组件对象相当于对象,调用者调用COM组件对象。
COM的基本特性,也就是设计COM时所要达到的目标。
COM是一种面向对象的组件系统,这里用组件类(Component Class)或者组件(Component)对应面向对象系统中的类,用组件对象(Component Object)对应面向对象系统中的对象。
COM中,完整的COM组件由组件本身和组件对应的类厂组件构成,实现完整COM组件的DLL或者EXE称为组件服务器(Component Server)。因为调用者/组件结构也是一种客户/服务器(C/S)结构,所以有时也不严格区分组件和组件服务器。
4.2.接口
从调用者与组件通信的角度看,接口是一种调用协议。调用者和组件正是遵循同样的接口才能完成通信,这也是COM能够实现复杂应用(例如OLE/ActiveX)的基础,因为在复杂应用中,调用者和组件之间应该有调用协议。
COM组件实现接口,COM组件对象引出指向接口的指针,COM接口的本质与第2章中讲述的面向对象系统中的接口相同,接口解决了语言(源程序)无关的组件对象的接口的方法的调用(或者组件对象的调用)问题和位置透明性问题(通过代理)。
所有的接口都从IUnknown接口派生(标准接口),这意味着所有的接口都定义了IUnknown接口中定义的方法,指向任意接口的指针都可以转换成指向IUnknown接口的指针,所有的组件都实现IUnknown接口,所有的组件对象都引出指向IUnknown接口的指针,也可以说所有的组件对象都可以用指向IUnknown接口的指针引用,不同的组件对象有统一的方法引用。
接口使用接口GUID(IID)标识,通过组件对象引出的指向任意一个接口的指针,可以调用组件对象的IUnknown接口的QueryInterface方法查询得到该组件对象的任意一个其他接口指针,这意味着只要有指向任意一个接口的指针引用组件对象,就可以调用组件对象的任意接口的任意方法(组件实现的任意接口的任意方法)。
4.3.引用计数(Reference Count)
因为COM的位置透明性,调用者和组件对象可能不在同一位置,当调用者不再使用组件对象时,释放组件对象的内存(删除组件对象)的工作应该调用组件对象完成,也就是说由组件对象自身来完成(自管理)。
组件对象的引用计数是实现组件对象自管理的方法,创建组件对象时组件对象的引用计数为0;当使用一个指向接口的指针,添加组件对象的一个引用时,应该调用组件对象的IUnknown接口的AddRef方法将引用计数加1;当不再使用一个指向接口的指针,减少组件对象的一个引用时,应该调用组件对象的IUnknown接口的Release方法将引用计数减1,引用计数减到0时,方法释放组件对象自身的内存(自删除)。
所有的接口都从IUnknown接口派生,这意味着通过组件对象引出的任意一个指向接口的指针,都可以调用组件对象的IUnknown接口的AddRef方法和Release方法完成组件对象的引用计数。
4.4.类厂(Class Factory)
类厂就是组件的类对象,类厂的本质与第3章中讲述的面向对象系统中的类对象相同,类厂组件就是类对象类,类厂组件实现的IClassFactory接口就是类对象类实现的类对象接口,通过类厂引出的指向IClassFactory接口的指针调用类厂的IClassFactory接口的CreateInstance方法创建类厂对应的组件的组件对象,解决了语言(源程序)无关的创建对象工作问题。
COM是语言(源程序)无关的面向对象系统,不可能有源程序形式的组件类(组件类的定义),可以认为类厂就是组件类(CoClass)。
组件服务器必须创建类厂(类厂组件对象),然后将类厂引出的指向IClassFactory接口的指针动态注册到COM环境中(或者暴露给COM环境),这样调用者才能通过类厂引出的指向IClassFactory接口的指针调用类厂的IClassFactory接口的CreateInstance方法创建类厂对应的组件的组件对象。例如DLL形式的组件服务器通过引出DllGetClassObject函数将类厂引出的指向IClassFactory接口的指针暴露给COM环境。
组件类或者类厂使用组件类GUID(CLSID)标识,不同组件服务器中的不同组件类的组件类GUID都被静态注册到COM环境(注册表)中,这样调用者能使用组件类GUID,通过COM环境定位到要调用的组件所在的组件服务器,进而启动组件服务器(例如加载DLL),获取动态注册到COM环境中的类厂引出的指向IClassFactory接口的指针,进而创建类厂对应的组件的组件对象。
类厂和类厂对应的组件的组件对象都是由组件服务器创建的,所以可以说COM组件对象是自创建的。
4.5.自动化(Automation)
自动化(以前称为OLE自动化)原泛指使用解释型语言(例如Word Basic、VBA、VBScript等)调用应用程序,使得应用程序自动工作(例如使用VBA调用Word或者Excel,使得Word完成自动排版或者Excel完成自动计算)。解释性语言调用的应用程序称为自动化服务器(或者OLE自动化服务器),自动化服务器是一种COM组件服务器。后来自动化就泛指使用解释型语言调用COM组件(组件对象),能适合解释型语言调用的组件称为自动化组件。
指向接口的指针是指向指向虚函数表的指针的指针,而指向虚函数表的指针指向虚函数表。使用编译型语言调用组件对象,通过组件对象引出的指向接口的指针调用组件对象的接口的方法,接口的作用是在编译时,根据接口的定义,确定虚函数(接口的方法)在虚函数表中对应的项,编译后生成在虚函数表中定位虚函数对应的项,获取指向虚函数的指针的二进制代码,也就是说在编译时,根据接口的定义,编译后生成在虚函数表中获取虚函数(接口的方法)对应的指向虚函数的指针(在虚函数表中定位虚函数对应的项,获取指向虚函数的指针)的二进制代码是编译时绑定的(但是虚函数的调用是运行时绑定的),从逻辑上看,可以认为调用者到接口的绑定是编译时绑定的。
使用解释型语言调用组件对象,在虚函数表中获取虚函数(接口的方法)对应的指向虚函数的指针的工作只能在运行时绑定。解释型语言无法直接根据接口的定义,确定虚函数(接口的方法)在虚函数表中对应的项,需要更直接的方法通过组件对象引出的指向接口的指针调用组件对象的接口的方法。
IDispatch接口的方法可以接受字符串形式的方法名作为参数,如果组件实现IDispatch接口,那么通过组件对象引出的指向IDispatch接口的指针调用组件对象的IDispatch接口的方法,传递字符串形式的方法名,就可以调用组件对象的其它方法了,也就是说组件实现IDispatch接口可以实现在运行时通过字符串形式的方法名调用组件对象的方法,从逻辑上看,可以认为调用者到组件对象的方法的绑定是运行时绑定的。实现IDispatch接口的组件就称为自动化组件,通过组件对象引出的指向IDispatch接口的指针调用组件对象的IDispatch接口的方法,传递字符串形式的方法名调用的组件对象的其它方法,称为自动化方法,这些方法的集合也认为是一个接口(逻辑接口),称为调度接口(Dispatch Interface/Dispinterface),从逻辑上看,可以认为调用者到调度接口的绑定是运行时绑定的。
如果一个接口从IDispatch接口派生,组件实现这个接口,组件对象引出的指向这个接口的指针可以转换成指向IDispatch接口的指针,那么通过组件对象引出的指向这个接口的指针,可以调用组件对象的这个接口的方法,也可以调用组件对象的调度接口的方法,这个接口就称为双重接口(Dual Interface),相对于双重接口和调度接口,普通接口称为定制接口(Custom Interface)。
自动化提供了更加统一的调用组件对象的方法。
4.6.COM组件
综上所述,从COM组件本身的角度看,完整的COM组件相当于完全二进制代码级的组件(类),包括组件本身和组件对应的类厂组件,相当于类和类对象类,类厂相当于类对象。
从程序设计的角度看,完整的COM组件也可以看成是支持组件对象自创建、支持组件对象自管理和实现标准接口的组件(类)。
笔者用ATL 7.0(VC 7.0/ATL 7.0)开发了两个简单的COM组件,VC 7.0支持基于特性的程序设计(Attributed-based Programming)[目前部分VC 7.0的文档(包括微软的中文文档)将“Attribute”一词译作“属性”,但是笔者为了避免与“属性(Property)”一词相混淆,将“Attribute”一词译作“特性”],特性是VC 7.0的一种新增功能(扩展)。特性提供一种有效而快捷的方法来简化使用VC 7.0的COM程序设计。特性与C++关键字一样,在源文件中使用,并由编译器进行解释。特性可以修改现有代码的行为,甚至可以插入附加框架代码来完成基本任务,例如实现ActiveX控件、创建类工厂或者设置数据库命令的格式。几乎可以将特性应用到任何C++对象(如类、数据成员以及成员函数)上,还可以将特性作为独立的语句插入到源代码中。两个COM组件分别是“HelloWorld”组件(组件工程名是HelloWorld_ATL7)和模拟“神舟”飞船组件(组件工程名是ShenZhou_ATL7),组件源程序如下:
“HelloWorld”组件源程序头文件(HelloWorld.h):
// HelloWorld.h : CHelloWorld 的声明
#pragma once
#include "resource.h" // 主符号
// IHelloWorld
[
object,
uuid("CD122771-36FA-427A-A13D-0D5D951636EE"),
dual, helpstring("IHelloWorld 接口"),
pointer_default(unique)
]
__interface IHelloWorld : IDispatch
{
[id(1), helpstring("方法HelloWorld")] HRESULT HelloWorld([in] BSTR bstrInput, [out,retval] BSTR* bstrOutput);
};
// CHelloWorld
[
coclass,
threading("apartment"),
vi_progid("HelloWorld_ATL7.HelloWorld"),
progid("HelloWorld_ATL7.HelloWorld.1"),
version(1.0),
uuid("ACC82BB4-ED5C-4008-990B-D247E9EAB466"),
helpstring("HelloWorld Class")
]
class ATL_NO_VTABLE CHelloWorld :
public IHelloWorld
{
public:
CHelloWorld()
{
}
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct()
{
return S_OK;
}
void FinalRelease()
{
}
public:
STDMETHOD(HelloWorld)(BSTR bstrInput, BSTR* bstrOutput);
};
“HelloWorld”组件源程序(HelloWorld.cpp):
// HelloWorld.cpp : CHelloWorld 的实现
#include "stdafx.h"
#include "HelloWorld.h"
// CHelloWorld
STDMETHODIMP CHelloWorld::HelloWorld(BSTR bstrInput, BSTR* bstrOutput)
{
// TODO: 在此添加实现代码
CComBSTR bstrOut(L"Hello ");
bstrOut+=bstrInput;
bstrOut+=L"!";
*bstrOutput=bstrOut.Detach();
return S_OK;
}
模拟“神舟”飞船组件源程序头文件(ShenZhou.h):
// ShenZhou.h : CShenZhou 的声明
#pragma once
#include "resource.h" // 主符号
// IShenZhou
[
object,
uuid("5E55F10A-D29B-4BFF-B136-8085A3F1CD1A"),
dual, helpstring("IShenZhou 接口"),
pointer_default(unique)
]
__interface IShenZhou : IDispatch
{
[id(1), helpstring("方法Fly")] HRESULT Fly(void);
[id(2), helpstring("方法GetPosition")] HRESULT GetPosition([out,retval] LONG* Position);
};
// CShenZhou
[
coclass,
threading("apartment"),
vi_progid("ShenZhou_ATL7.ShenZhou"),
progid("ShenZhou_ATL7.ShenZhou.1"),
version(1.0),
uuid("4C6F2F88-50F9-41AB-81B6-EA1D72108242"),
helpstring("ShenZhou Class")
]
class ATL_NO_VTABLE CShenZhou :
public IShenZhou
{
public:
CShenZhou()
: m_position(100)
{
}
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct()
{
return S_OK;
}
void FinalRelease()
{
}
public:
private:
int m_position;
public:
STDMETHOD(Fly)(void);
STDMETHOD(GetPosition)(LONG* Position);
};
模拟“神舟”飞船组件源程序(ShenZhou.cpp):
// ShenZhou.cpp : CShenZhou 的实现
#include "stdafx.h"
#include "ShenZhou.h"
// CShenZhou
STDMETHODIMP CShenZhou::Fly(void)
{
// TODO: 在此添加实现代码
MessageBox(NULL,_T("Fly!"),_T("ShenZhou"),MB_ICONINFORMATION|MB_OK);
return S_OK;
}
STDMETHODIMP CShenZhou::GetPosition(LONG* Position)
{
// TODO: 在此添加实现代码
int Position1;
Position1=m_position;
*Position=Position1;
return S_OK;
}
要获得有关组件源程序和VC 7.0更多的信息和帮助,可以参考VC 7.0的文档。
第5章 现代软件组件系统
5.1.现代软件组件系统的特点
对于客户/服务器(C/S)结构(或者调用者/组件结构)的现代软件组件系统,应该有以下特点(或者功能):
⑴语言无关性:组件是语言(源程序)无关的,组件的调用协议是语言无关的协议。
⑵位置透明性:调用者对组件的调用,与组件的位置无关,无论组件位于什么位置,调用着调用组件(包括定位组件)的方法都相同。
⑶自描述性:组件应该是自描述的,调用者应该能够在调用组件之前,通过组件获取调用组件需要的组件信息(自描述),组件信息也是语言无关的。
⑷可复用性:组件应该能够在二进制代码级方便地被复用。
⑸安全性:组件应该是安全的,不应该允许任何形式的未授权使用(调用)。
5.2.几种现代软件组件系统的比较
几种典型的现代软件组件(系统)的比较如下:
COM:
语言无关性:通过COM接口、引用计数和类厂实现语言无关性。
位置透明性:通过对COM组件对象引出的指向接口的指针的调整实现位置透明性。
自描述性:通过类型库(Type Library)实现自描述性。
可复用性:可以二进制代码级复用。
安全性:DCOM/MTS/COM+ 1.0有安全性引擎,可以实现安全性。
Microsoft .NET(COM+ 2.0):
语言无关性:通过.NET Framework实现语言无关性。
位置透明性:通过远程(Remoting)技术实现位置透明性。
自描述性:通过元数据(Metadata)和清单(Manifest)实现自描述性。
可复用性:可以二进制代码级复用。
安全性:可以实现安全性。
Web Service:
语言无关性:通过SOAP协议实现语言无关性。
位置透明性:通过Web Service的URL和HTTP协议/SOAP协议/XML实现位置透明性。
自描述性:通过WSDL实现自描述性。
可复用性:可以二进制代码级复用。
安全性:可以实现安全性。
5.3.面向对象的现代软件组件系统和非面向对象的现代软件组件系统
现代软件组件系统可以分为面向对象的现代软件组件系统和非面向对象的现代软件组件系统。
面向对象的现代软件组件系统中,组件相当于类,组件实例化成组件对象,组件对象相当于对象,调用者调用组件对象。
非面向对象的现代软件组件系统中,组件本身相当于对象,调用者直接调用组件本身。
COM/Microsoft .NET(COM+ 2.0)是典型的面向对象的现代软件组件系统,Web Service是典型的非面向对象的现代软件组件系统。