(摘要:本文主要是介绍C++语言对于软件复用层次的支持程度在面向组件开发模式下的不足,并且结合和具体实例分析了出现这些问题的具体原因。)
缘起
3年前,就职于微软公司的Anders Hejlsberg在该年度的TechDays演讲中提到当今程序设计语言的发展趋势时,指出了为了提高软件产能效率,程序设计语言及相配套的函数库渐渐地从非托管语言(如C/C++语言和标准模板库STL)发展到托管框架类库(如C#+FCL或者Java),抽象层次的提高更有利于上层软件的开发,屏蔽下层系统具体实现的复杂度。的确,微软作为一个操作系统厂商,在Windows平台下提供一套简单易用,准入门槛较低的开发平台是符合软件发展趋势的。因此从90年代初开始,微软推出的DDE、OLEDB、ActiveX、COM以及MTS等一系列的新技术虽然一度使人眼花缭乱,但终于在2003年推出.NET框架才最终形成一个统一的框架,一直沿用至今。
而在微软公司在建立这Windows生态系统的过程中,组件对象模型(以下用COM称呼)是组成Windows系统关键技术,值得我们这些21世纪计算机科学从业人员的探究。
背景
COM是一种编程模式,致力于进一步提高软件的复用性,在面向对象编程已提供的源代码级复用程度上加强代码的二进制复用级别,这样的二进制代码就形成组件。
简单而言,同一份代码被不同软件的开发商利用,这就是源代码级别的复用(如MFC),这一份代码会与可执行程序一起发布给用户,但如果这一份代码在后期发现了一个漏洞,则需要重新编译这份代码,这样用户需重新下载该程序(MFC6.DLL)。否则就使用老版的程序(MFC42.DLL),还是使用着有漏洞的API,不能进行覆盖操作,这也有一定的历史原因,在Windows操作系统发布之前,Unix操作系统的应用软件就是这样进行升级,这也就是C++的一大特点。最终会造成软件版本的混乱,无法正常使用。
COM技术就是致力于解决这样的问题。
实例
如果某库开发者发现一恶较快的字符串算法,自定义了一种代替std::string类型的新型字符串类型(FastString),代码如下
1 //FastString.h 2 class __declspec(dllexport) FastString 3 { 4 public: 5 FastString(const char*); 6 ~FastString(); 7 int GetLength() const; 8 int Find(char*) const; 9 private: 10 char* m_psz; 11 };
1 //FastString.cpp 2 #include “FastString.h” 3 FastString::FastString(const char* psz) : m_psz(new char[strlen(psz) + 1]) 4 { 5 strcpy(psz,m_psz); 6 } 7 FastString::~FastString() 8 { 9 delete[] m_psz; 10 } 11 int FastString::GetLength() const 12 { 13 return strlen(m_psz); 14 } 15 int FastString::Find() const 16 { 17 int index = -1 18 //自定义查找字符串算法 19 return index; 20 }
编译成动态链接库FastString.dll后,随FastString.Lib和FastString.h发布给客户,用户就可以使用了。同时也杜绝了算法泄露的可能,但这里会带来两个问题。
首先我们假设下用户模型,如图所示
问题1:将类中内部数据成员结构暴露给应用开发者
发布给应用开发者的头文件中包含m_psz的定义,虽然头文件将此数据成员设置为private访问级别,由于头文件在开发者手中,开发者完全可以将其更改为public访问级别,这样会破坏类的内部结构,可能导致类无法工作。
按照程序库的要求,应该只讲公共函数接口暴露给客户,作为程序库的开发者,不能排除恶意用户利用以上问题进行破坏,这样也违背了库开发者的本意。
问题2:C++编译模型不支持程序库的以二进制形式覆盖升级
假设该开发者在发布了第一个版本后发现在该程序中犯了一个小错误,即在获得字符串长度时候(GetLength()),不必每次都重新计算字符串长度,可以作为成员变量在对象产生的时候就产生长度,因为字符串是不可变的。因此,他对程序进行如下更改:
1 //FastString.h 2 class __declspec(dllexport) FastString 3 { 4 public: 5 fastString(const char*); 6 ~FastString(); 7 int GetLength() const; 8 int Find(char*) const; 9 private: 10 char* m_psz; 11 int m_length; 12 };
//FastString.cpp #include “FastString.h” FastString::FastString(const char* psz) : m_length(strlen(psz)),m_psz(new char[m_length+ 1]) { strcpy(psz,m_psz); } int FastString::GetLength() const { return m_length; } //……其它代码
同样,编译生成FastString.dll并通知用户下载升级,覆盖原有的文件,库开发者认为这样会正常工作,实际上由于增加了一个类成员变量,并且更改了构造函数初始化内容,而根据C++内存对象模型,具体编译器已经安排好每个对象所需要的内存,即在product.exe中已经向栈或者堆中申请了一个FastString对象,占4个字节,如图所示
而在修改后的FastString.dll中预计该对象为8个字节,当product.exe调用新版本FastString.dll中构造函数时,由于新的构造函数需要初始化0xFF000004单元,即新版本的函数认为该单元是属于此对象内存空间,但是由于product.exe中只为该对象保留了0xFF000000单元,下一个单元是保存着其它数据,因此,一旦调用该构造函数即会对内存布局产生破坏,C++运行时即会捕捉到这样的错误。
总结
对于源代码级别的复用层级,C++语言的面相对象机制提供了很好的支持,但更多的情况下可能需要二进制代码层级的支持,COM技术即为了解决以上提到的两个问题,关于COM如何解决上述的问题,请关注《组件对象模型(COM)初探(二)》
Somnus.V
本作品采用知识共享署名-相同方式共享 2.5 中国大陆许可协议进行许可。