敬业的IT人 >> 编程开发 >> C/C++ >> C++接口概念“方法学”

C++接口概念“方法学”

敬业的IT人 互联网 佚名 2008-1-3 19:14:04

讨论组:http://group.qqread.com

简介
接口(Interface),作为一种比类更强大的语言特性,已出现在了Java、C#及其他语言中,但C++中却没有。本文中将要演示的,是一种C++接口概念“方法学”上的实现;且从Visual Studio.NET 2002开始,微软也以一种扩展的方法来走这同一条路,其允许编译器来实现接口中的大多数特性,当然了,C++托管扩展也支持 .NET接口的定义与实现。不管怎样,在这些实现机制之间,还是有一些微妙差别的,都需要你给予充分的重视。
一个接口在没有特定实现之前,其描述了类的行为或功能,代表了提供者与使用者都必须遵守的约定,它定义各个实现者的所需完成的功能,而不管它们怎样具体去做。

第一个版本
首先,在头文件中定义了一些宏,你可在程序的预编译头文件中包含它:
//// CppInterfaces.h// #define Interface class#define DeclareInterface(name) Interface name { \ public: \virtual ~name() {}#define DeclareBasedInterface(name, base) class name : public base { \public: \virtual ~name() {}#define EndInterface }; #define implements public 
使用这些宏,能以下面这种方式声明一个接口:
//// IBar.h// DeclareInterface(IBar)virtual int GetBarData() const = 0;virtual void SetBarData(int nData) = 0;EndInterface 
接下来,可像下面这样声明一个实现了这个接口的类:

//// Foo.h// #include "BasicFoo.h"#include "IBar.h"class Foo : public BasicFoo, implements IBar{//构造及析构函数 public:Foo(int x) : BasicFoo(x){}~Foo();// IBar的实现 public:virtual int GetBarData() const{//函数体 }virtual void SetBarData(int nData){//函数体 }}; 
很简单吧,无须太费力,现在就能在C++中使用接口了,但是,因为它们不是直接被语言支持的,所以要遵循以下这些在编译时不能自动应用的规则,毕竟,在所有编译器的“眼中”,这些都是多重继承和抽象基类。

² 当声明一个类时,要使用基类来搭建“结构性”的继承(成为“is a 是一个”的关系),例如:CFrameWnd继承自CWnd,CBitmapButton继承自CButton,xxDialog继承自CDialog。尤其当在使用MFC时,这点非常重要,以避免破坏MFC的运行时类机制。
² 为实现接口使用额外的基类,有多少个接口,就用多少个基类。例如:lass Foo : public BasicFoo, implements IBar, implements IOther, implements IWhatever
² 不要在接口中声明任何变量。接口是用来表示行为,而不是数据,除此以外,这样做还可以在使用多重继承并继承自同一个接口不止一次时,避免某些错误。
² 在接口中把所有的成员函数声明为纯虚函数(即带上“=0”)。这可确保声明实现接口的每个实例化的类都实现其自已的函数;也可在抽象类中部分实现一个接口,只要在继承来的类中实现了余下的函数。另外,接口不提供“基本”实现,因为必须保证每个得到接口指针的调用者,都可以调用它的任意成员;把所有的接口成员声明为纯虚,可在编译期间强制应用这条规则。
² 接口只能从接口继承,而从其他任何类型继承都不行,可使用DeclareBasedInterface()来达到此目的。正常的类能选择实现基接口或继承来的接口,而后者也意味着实现了前两者。

把一个实现了某些接口的类的指针,赋值给这些接口的指针,并不需要进行转换,因为实际上是在把它转换为一个基类;但反过来,把一个接口指针,赋给一个实现它的类的指针,就需要显式转换了,因为是在把它转换为一个继承类。由于我们实际上是在使用多重继承——也可把它看作单重继承加上接口实现,且它们可能需要不同的内存值,所以“老式”的转换方法就行不通了;然而,打开RTTI(Run-Time Type Information运行时类型信息)/GR编译选项,并使用dynamic_cast,就完全没有问题了,且在任何情况下都是安全的。进一步来说,使用dynamic_cast还可以查询任意对象及接口,是否实现了某个特定的接口。

另外,还必须小心避免在不同接口中的函数名冲突。进入讨论组讨论。

讨论组:http://group.qqread.com

进一步评估
也许在你看了上述文字之后,会有一些想法:为什么要为宏费心呢?而它们并没有用到什么强制性的规则,也没有提高老式#define begin {、#define end }语法的可读性啊。
但如果你仔细观察DeclareInterface和DeclareBasedInterface宏,你会注意到还是有一些强制性规则存在的:实现了一个接口的类都有一个虚拟析构函数;也许你认为它并不重要,但是如果缺少虚拟析构函数,会导致某些问题,请看下面的代码:
DeclareInterface(IBar)virtual LPCTSTR GetName() const = 0;virtual void SetName(LPCTSTR name) = 0;EndInterfaceclass Foo : implements IBar{//内部数据 private:char* m_pName;//构造与析构函数 public:Foo(){m_pName = NULL;}~Foo(){ReleaseName();}protected:void ReleaseName(){if (m_pName != NULL)free(m_pName);}// IBar的实现 public:virtual const char* GetName() const{return m_pName}virtual void SetName(const char* name){ReleaseName();m_pName = _strdup(name);}};class BarFactory{public:enum BarType {Faa, Fee, Fii, Foo, Fuu};static IBar CreateNewBar(BarType barType){switch (barType){default:case Faa:return new Faa;case Fee:return new Fee;case Fii:return new Fii;case Foo:return new Foo;case Fuu:return new Fuu;}}}; 
如上所示,这里有一个类工厂(factory),可以基于BarType参数,请求它来创建IBar的某个实现;在用完之后,往往会想到删除这个对象,目前为止,一切都正常,但如果用在某些程序的main函数中呢:

int main(){IBar* pBar = BarFactory::CreateBar(Foo);pBar->SetName("MyFooBar");//尽可能地多使用pBar// ...//在不需要时删除它 delete pBar; //啊呀! } 

在执行到delete pBar这一行时会发生什么,完全依赖于对象的类是否有一个虚拟析构函数。如果Foo没有一个虚拟析构函数,编译器只会生成一个对IBar隐式空析构函数的调用,而Foo的构析函数并没有被调用,因此会造成内存漏泄。在接口声明宏中的虚拟析构函数就是为了要防止这种情况发生的,它们保证了每个实现了一个接口的类,都会有一个虚拟析构函数。
既然我们使用了DeclareInterface,那么用EndInterface来进行配对还是说得过去的,至少好过用一个花括号来匹配吧。至于Interface与implements,它们各自会解析为class与public,使用它们看上去似乎有点多余,但它们能更好地表达代码所代表的意图,比如说从Foo : public IBar中,就只能看出继承关系,如果写成Foo implements IBar,就可看出实际的用意了。

 

进入讨论组讨论。

讨论组:http://group.qqread.com

Visual Studio.NET 中对C++接口的支持
微软从Visual Studio.NET 2002开始,引入了一种新的关键字:__interface,它是一种针对C++编译器的微软扩展,在相应文档中,对一个Visual C++接口进行了如下说明:

² 能从零个或多个基接口继承。
² 不能继承自一个基类。
² 只能包含公有及纯虚方法。
² 不能包含构造函数、析构函数、操作符函数。
² 不能包含静态方法。
² 不能包含数据成员;但允许有属性。

文档还注明:“一个C++类或结构可用以上规则来实现,但必须加上__interface。”因此,如果不在意可移植性的话,是否可用__interface这个扩展让编译器来完成剩下的工作呢?错了。
还记得虚拟析构函数吗?__interface并不会为实现一个类而添加虚拟析构函数,如果你仔细看过文档,就会发现它们实际上是由COM接口实现的,所以你从不会用到delete,因为它们是引用计数的对象,当计数为零时,会对自身调用delete。
那么在DeclareInterface 宏定义中使用__interface,这不就两全其美了吗?再看一下前面的规则:“不能包含构造函数、析构函数及操作符函数”。这可能让人觉得有点不可能,甚至会想到为什么要在COM接口中使用__interface呢,况且也不适合这里所说到的通用接口啊。那么,请再往下看。

两全其美的方法
这儿有一个非常简单的解决方案:接口的名字实际上用于声明一个类,而这个类包含了一个虚拟析构函数且继承自包含了必要方法的一个__interface。现在宏定义如下:
//// CppInterfaces2.h// #define Interface class#define implements public #define DeclareInterface(name) __interface actual_##name { #define DeclareBasedInterface(name, base) __interface actual_##name \: public actual_##base {#define EndInterface(name) }; \ Interface name : public actual_##name { \public: \virtual ~name() {} \}; 
使用上面定义的宏来声明一个接口:
//// IBar2.h// DeclareInterface(IBar)int GetBarData() const;void SetBarData(int nData);EndInterface(IBar) 
也许你已注意到了,新的宏定义用到了两次接口名(IBar),第一次是DeclareInterface(),而第二次是EndInterface()。另一方面,倘若你不怎么在意移植性问题,那么这个宏比微软编译器所提供的扩展有利得多,因为接口(仅指提供了实现类的纯虚方法、虚拟析构函数且没有数据成员的接口)的方方面面,都自动完成了,甚至不必显式声明接口方法为virtual或纯虚(=0)——虽然声明了也没什么关系。进入讨论组讨论。
粤ICP备06119539号
Copyright CiscoSky.Org,Some Rights Reserved.
Email:me1228#tom.com