用C实现继承和多态_第1页
用C实现继承和多态_第2页
用C实现继承和多态_第3页
用C实现继承和多态_第4页
用C实现继承和多态_第5页
已阅读5页,还剩4页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、用C实现继承和多态MIRO SAMEK著 陈希明译虽然面向对象的设计并不会在很大程度上依赖于某种语言,但现代著作中提 及面向对象的实现一般都认为是C+, Smalltalk,或者Java。本文从较底层的 视角用面向过程的语言(比如C)对面向对象予以实现,这对于一些想运用面向 对象思想但又不想切换到面向对象语言的嵌入式开发者会起一定的指导作用。有没有可能用像C语言这种非面向对象(non-OO)的语言写出面向对象(00)的程序? 在一个很小的,没有C+编译器可用的嵌入式系统中怎样进行面向对象的程序设计?怎样改 进C语言开发的编程模式?怎样提高C语言代码的可重用性、模块化功能以及健壮性?继 承和多态

2、究竟是怎么实现的?你的系统能接受额外开销么?在用C (而不是面向对象语言) 进行面向对象的设计时,你应该怎样折衷地考虑便利性和可读性?在这篇文章中,我将使用 下面这些面向对象的概念演示一个轻型的、高效的C语言程序,从而对这些问题展开讨论。封装一把数据和函数打包进“类”,也就是信息隐藏和模块化。继承-基于已经存在的类来定义新的类和方法的能力,这是为了代码的重用性和 组织结构。多态一同样的消息发送给不同的对象,而产生的行为应该依赖于接收到消息的对 象的属性。我采用Java语言来说明继承和多态1。继承类(或者继承的实现)仅仅是一个继承的 模型,它是由位于类层次体系根节点的抽象类提供的。反过来,这种实

3、现允许多继承,允许 类实现许多个Java-style接口。尽管面向对象语言具有很多无可置疑的优点,C语言依然是最具知名度和使用最广泛的 源代码语言。现在很多嵌入式系统并不提供其它语言的选择性,所以,大多数开发者依然仅 仅使用面向过程的编程技术,而且很多人根本没有意识到在C语言中直接实现基本的面向 对象概念是很容易的。建立这种意识很可能是重要的,原因有两个:首先是面向对象技术的推动。大多数面向对象设计都能在C中实现,但很多开发者并 不这样认为,因为C不具有面向对象语言的可行性。其实这种非必要性限制了面向对象技 术的应用。第二点是从面向过程到面向对象这种思维的平滑过渡。向面向对象技术的转移需要思维

4、 方式的跳跃。用你目前正在使用的而且很熟悉的语言来实现面向对象概念,这会给你一个立 即完美地去接触全新编程模式的机会,而且不需要大的投入。封装在C中你可以通过这种方式将数据和函数打包:在C的struct中为每个类的属性(变 量实例)定义成一个结构体成员变量。通过C函数实现类的方法,这些函数将指向这个结 构体类型的指针(this指针)作为第一个参数。更进一步地,你可以通过对类的方法实行一 致的命名约定,来加强类属性和类方法之间的关联。我采用的最流行的约定是,将结构体名 字(类名)和方法的名字连接在一起。函数名字的变更是“名字修饰”(也称命名管理)的 一部分,这在多数C+编译器中隐含地作了处理。由

5、于名字修饰消除了不同类之间的方法名 (译者注:函数名)冲突,它有效地将平面式的C函数名字空间划分成了包含在类中的、 嵌套的、独立的名字空间。接下来,我会从另一方面通过命名约定的方式说明权限控制。在C中,你只能在权限 允许范围内,通过对某个特定的属性或者方法的引用来表明你的访问目的。通过属性或者方 法的名字来表明你的访问目的,要好于在声明的地方加一行注释的方式。通过这种更合理的 命名方式,在代码中的任何部分,只要有对类的成员的“无意”的访问,就可以很容易地检 测到。大多数面向对象设计区分以下三种保护机制:Private -只有在当前类中可访问Protected -在当前类及其子类中可访问Publ

6、ic -任何地方都可访问(这在C中是默认的)我使用双下划线前缀(foo)来表示一个私有属性(private)0注意,一般说来,没有 必要把私有方法(译者注:私有成员函数)暴露在类的声明文件中(.H文件)。而且,你应 该在你的源文件中将它们完全隐藏起来(在.C文件中声明为static)。对于保护属性(protected) 成员,我使用单下划线(_foo, String_foo)。在声明公有成员(public members)时要杜绝使用 下划线(foo, StringFoo)o这样,按照这种命名约定,一个名字中的下划线的状态就成为了 访问权限的“信号”,而这个访问权限正是需要根据代码上下文作出检

7、查的。由于公有成员 可以不受限制的使用,所以他们不需要特别的修饰。每一个类必须提供至少一个构造函数方法,用以实现结构体属性的初始化。调用构造函 数应该成为初始化的唯一方法。否则,这个对象的内部结构就必须被暴露在外,这就跟封装 的思想冲突了。一个类可以提供一个析构器,但这不是强制性的。它是一个函数方法,负责释放一个对 象在它的生命周期中分配的资源。虽然实例化一个类可能有多种方法(不同的构造函数使用 不同的参数),但一个对象的析构应该只有一个方法。由于构造函数和析构函数的特殊角色,我又要提议一个一致的命名模式。我使用一个基 本名称“Con”(FooCon1,FooCon2)和“Des”(FooDe

8、s)来分别表示构造函数和析构函数。我 建议,一个构造函数返回一个指向已经正确初始化过的结构体的指针,或者在初始化失败时 返回NULL。析构函数只使用“this”指针作为形参,并返回NULLo对象可以静态分配、动态分配(使用堆)、或者自动分配(使用栈)。由于C语法的限 制,你一般不能在定义一个对象时通过调用构造函数对它进行初始化。对于静态对象,你甚 至不能调用它的构造函数,因为函数调用不允许在静态初始化程序中发生。自动对象必须在 一个函数体的开头定义,在这个时候,你经常不具备足够的初始化信息来调用合适的构造函 数。因此,你不得不经常要把分配对象和初始化对象分离开来。你应该像看待所有其它的C 变量

9、一样来看待对象,因为你永远不能在初始化它们之前使用它们。一般的,一旦初始化信 息都可用了,你就可以对对象进行初始化了。有些对象可能需要析构函数,对所有对象来说,在它们被废弃或者生命周期结束时调用 它们的析构函数是个很好的编程习惯。在后面我将演示一个对所有类都适用的虚析构函数。继承继承是根据已存在的类来定义新类和更加定制化的类的一种机制。当一个子类 (subclass )从它的父类(superclass)继承而来时,这个子类包含了父类所有属性和方法的定 义。通常地,子类会通过增加自己的属性和方法来扩展父类。子类的实例化对象包含了子类 和父类定义的所有的数据。它们可以运行子类和父类的所有的方法。这

10、种类的关系在C中可以通过把父类的属性结构体嵌入到子类结构体中并作为第一个 成员。如图1所示。图1子类结构体中嵌入父类并将其作为第一个成员(图a);内存布局图(图b)按这样的结构方式可以实现这种属性组合:一个指向子类的指针总是可以被安全的转换 成指向父类的指针(向上追溯)。特别的,如果有C函数需要一个父类的指针,那么这个指 向子类的指针总是可以作为参数传递过去(由于C中的强制类型匹配,你应该显式地对这 个指针作类型转换)。这就意味着所有父类的方法在子类中都是适用的-换句话说,它们被 继承下来了。这种简单的方法只能处理简单的继承(只有一个父类),因为一个具体多个父类的子类 无法将所有父类的属性成员

11、都作出正确的属性组合。这里我将被继承的类命名为super,这可以使各个类之间的继承关系更加明显,而且跟 Java更加相似。Super类提供了一个方法来访问super类的属性成员,比如,一个孙类对象 可以通过这个方式来访问祖先类的保护属性成员_foo: this- super.super._foo。继承增加了类构造函数和析构函数的责任。由于每个子类对象都包含一个嵌入的父类对 象,所以这个子类的构造函数必须要考虑到父类需要初始化的部分。为了避免任何潜在的依 赖关系,在子类进行初始化属性成员之前应该首先调用父类的构造函数。而对于析构函数, 恰恰跟这个顺序相反,继承下来的那一部分应该在最后一步销毁。我

12、还在我的实现中借鉴了 Java只有一个抽象基类Object的概念,这意味着没有类可以 被单独定义,而是必须以Object类作为类层次体系的根节点,从别的类扩展得到。这种设 置方式在混杂的编程实践中(对一个面向过程的语言添加面向对象属性)显得尤为方便,因 为每一个对象最终都可以看做是Object类的一个实例,而且Object类已经跟所有别的类显 式的区分开了。这跟C+有所不同,在C+中,每一个结构体都等同于一个类。正如我将 要演示的,我给Object类添加了重要的行为,这个行为接下来被所有其它类所继承,进而 实现了多态。#include object.h/* Character String c

13、lass */CLASS(Stringj Object)char *_buffer; /* private buffer */VTABLE(String, Object)METHODS/* public constructors */String StringConl(String this,const char *str);String StringCon2(String this, String other);/* public destructor */void StringDes(String this);/* public to char conversion */表1演示了一个St

14、ring姓础类的声明,它扩展了 Ojc)关。这个类封装了一个字符缓 冲区钮桃陞祐提供了两个构造函数(StringCon1,StringCon2)、一个析构函数和一个对字符 缓冲区进行只读访问的方法 StringToChar。这个类是通过预定义的宏(CLASS, VTABLE, METHODS, END_CLASS)来声明的,这些宏在object.h中进行了定义和说明,你可以从 HYPERLINK /code /code 下载获得。多态一个扩展的类经常对继承而来的一个或多个方法进行重新实现,以此对它的祖先类的行 为进行覆写。比如,Object类定义了析构函数Object_Des,从Object类

15、扩展而来的String 类(参考表1)用它自己的析构函数StringDes覆写了这个行为。我们假设在代码中的某个地方 销毁了一个混杂的含有多个Object通用类型指针的数据容器,因为Stirng类(或者其它的类) 是从Object类继承来的,在这个数据容器中的一些指针可能实际上是指向String对象,如 果这时你的代码正确地调用并执行了 StringDes来销毁这些String对象(如果是别的类对象, 那对应的就是别的析构函数),那么你的代码就是多态的。多态行为需要进行函数方法解析, 而方法解析依赖的是动态运行时的类(String),而不是指针类型的类(Object)。这又被称 为动态绑定。在

16、函数方法解析时添加一个额外的间接调用可以在C中有效的实现动态绑定。不同于 直接调用一个方法(C函数),你可以调用一个由函数指针指向的函数体,这个函数指针在 每个对象所引用的描述符类【2中定义。这个描述符类(有时被称为虚拟列表或者VTABLE) 实际上是一系列跟虚函数对应的函数指针-换句话说,是留给以后的子类进行覆写的方法。在前面的例子中,Object类对虚析构函数的实现看上去好像是下面这个样子的。Object 类在它的描述符类中声明了一个函数指针Des:struct ObjectClass void (*Des)(struct Object*);;每一个Object类的实例对象维护了一个指向这

17、个描述符类的指针(称为虚指针或者 VPTR-请参考 Eckel,1995):struct Object struct ObjectClass *_vptr;;那么虚析构函数的动态绑定就是:(*obj-_vptr-Des)(obj);在这里obj指向一个Object结构体。注意,obj在这里用到了两次:一次是为了函数方法解析,一次是作为this指针参数。 动态绑定需要至少两次的内存访问和至少一次的区别于直接函数调用的额外调用 (Rumbaugh,1991)。动态绑定所消耗的内存中保存了每一个对象中的虚指针(从Object继 承而来的),再加上为每个类存储VTABLE所消耗的内存。描述符类可以把自

18、己作为一个VTABLE类的唯一实例(一个由VTABLE对象表示的 类)。所以你可以通过对 VTABLEs运用嵌套的技术来实现虚函数的继承。这已经在宏 VTABLE (参考表1)中封装好了。所有的描述符类都直接或间接从描述符ObjectClass继承 而来,所以它们都继承了虚析构函数。继承使虚函数的调用语法稍复杂化了一些。总的来说,你必须向上追溯到对象指针(对 于Object类),向下追溯到虚指针_vptr(对于特定的描述符类)。包括对对象指针进行两次 引用的这些操作被封装进了宏VCALL和END_CALL。比如,对任意类型的对象obj,它的 虚析构函数的调用是这个形式:VCALL(obj, O

19、bject, Des) END_CALL;如果一个虚函数使用了不同于this指针的参数,那么它们应该在宏END_CALL的前面 列出来。比如,result = VCALL(obj, FooClass, Foo) ,5 ,i+j END_CALL;在这里,obj是指向FooClass或者FooClass子类的指针,虚函数Foo是在FooClass的 VTABLE中定义的。虚拟列表需要通过它们的构造函数进行初始化。这个由VTABLE构造函数进行初始化 的操作可以分解成两步:拷贝继承来的VTABLE,通过将选出来的虚函数的实现进行覆写 的方式对VTABLE进行定制。第一步是通过宏BEGIN_VTAB

20、LE自动产生的。拷贝继承来的VTABLE保证了在对祖 先类添加新的虚函数时不会破坏到任何子类。或者说,对子类没必要作手动改变(你只需要 重新编译子类代码)。除非一个类要显式地选择对祖先类的行为进行覆写,这种继承方式的 实现就已经足够了。当然,如果一个类声明了属于它自己的虚函数,这些相应的函数指针是 不会在这一步中初始化的。为了使第二步(将虚函数绑定到它们的实现)更加方便,我还提供了宏VMETHOD和 IMETHOD。如果你不能为一个给定的方法提供它的实现,你打算把他作成一个“纯虚函数” (留给它的子类实现),那么你还是应该用一个“假实现” Object_NoIm对函数指针进行初 始化。Obje

21、ct_NoIm会中断运行(通过一个失败断言),这对于运行时检测未实现的抽象方 法会有所帮助。正如我刚才所说,每一个对象都保留了一个指向它的描述符类的指针(虚指针),这个 描述符类是从object类继承来的。这个虚指针在对象初始化期间(也就是构造函数中)需要 进行正确的设置。而这个设置要在祖先类的构造函数执行之后进行,因为祖先类的构造函数 要把这个指针设置成指向祖先类的VTABLEo如果这个正在初始化的对象的VTABLE还没 有设置好,那这个VTABLE的构造函数应该被调用。这两个步骤通过对宏VHOOK的调用 来完成。注意,祖先类的构造函数对它们的VTABLE进行了同样的处理,所以整个类层次 体

22、系都得到了正确的初始化。图2演示了 String类(在表1中进行了声明)的定义。这个类的VTABLE只是对虚析构函 数的实现进行了覆写。我在把this指针传递给VMETHOD(Object, Des)之前对this指针使用 了显示的类型转换,以避免编译器警告。还要注意构造函数是怎样首先调用祖先类的构造函 数,继而把虚指针做成钩子调用,最后初始化成员属性的。图2 Foo类实现Fooable接口时的内存布局。指针foo可以访问Fooable虚拟列 表(*(*foo),也可以将obj指针重新构造成这个类的结构体属性。(obj = (Object)(char*)foo - (*foo) - _offs

23、et)接口有时候你只需要定义一些抽象的方法,没必要进行具体实现,但这些方法是对象所必须 支持的。完全根据所定义的接口进行对象操作大大简化了子系统之间的依赖关系,所以,可 复用的面向对象设计的一个主要原则是:“对接口编程,而不是对过程实现编程”囹o Java 通过对接口的支持非常完美地阐述了这个设计需求(Gosling, 1995和Arnold,1996)。在C中,我采用对VTABLE概念的一般化来实现接近Java风格的接口。如果一个类只 定义了抽象方法(纯虚函数)而没有定义任何属性成员,那它什么样子的?这样的一个类将 完全由它的VTABLE所代表。对这个类的继承只需要维护一个指向相应VTABL

24、E的虚指针。 当然,还要实现所有的抽象方法。一个对象可以很容易地维护多个这样的指针,所以基于一 些特定类(接口)的多继承将变得很容易。这并没有完全解决内存排列布局问题。在需要一个接口的地方,你不能仅仅简单地使用 一个指向对象的指针,因为没有办法找到相应的VTABLE,这是因为额外的虚指针不能跟 从Object类继承来的虚指针vptr排列在一起。如果总是碰到这种情况,你可以使用一个额外的间接调用来解决这个问题。如图2所示, 在一个实现了 Fooable接口的对象的内部有一个指向Fooable虚拟列表的指针foo,这个指针 就是一个很好的切入点。它可以用来访问Fooable虚拟列表(用于解析虚函数

25、调用),也可以 重新构造这个对象的位置(用于提供this参数)。接口跟类差别很大,接口不是从Object派生出来的(因为它们没有定义属性成员),而 且它们定义了不同的存放有offset成员的虚拟列表VTABLEso注意,由接口定义的虚函数 也必须使用this指针,但这个指针是通用的Object类型。我在下一节讨论的示例代码演示 了怎样在不做任何具体实现的情况下,使用接口编写代码。示例代码为了阐述刚才讨论的概念,我提供了一个简单类体系的实现,如图3所示(完整的列表 在 ESP 站点提供,/code)o图3 Shape和它的子类的类结构表。(粉红色框是接口)Shape类扩展了 Object类并且实

26、现了 Scalable接口。这是一个抽象类(只被用来继承), 所以它要保护它的构造函数和析构函数。Shape类把String类作为一个成员,演示了对象的 混合。Scalable接口只定义了一个抽象方法Scale()。Circle和Rect都是具体的类,它们派生 自Shape类而且都覆写了 Scale()和Area()方法。这个测试用例在栈(stack)中分配了一个Circle 对象,在堆(heap)中分配了一个存放Rect对象的数组。Shape类的测试函数和Scalable接口 分别演示了类和接口的动态绑定。作为实践,你可以修改这些代码,为Shape类(甚至Object基类)添加一些属性成员

27、或者虚函数。你可以让自己信服,完成这些改动后并不需要对子类作手动修改。我还强烈建 议你用调试器单步跟踪这些代码。#include #include #include Ostring.hO/* implementation of String class */BEGIN_VTABLE(String, Object)VMETHOD(Object, Des) = (void (*)(Object)StringDes; END_VTABLEString StringCon1(String this, const char *str) (Object_Con(&this-super); /* const

28、ruct superclass */ VHOOK(this, String); /* hook String VPTR */ /* allocate and initialize the buffer */this-_buffer = (char*)malloc(strlen(str) + 1);if (!this-_buffer)return NULL; /* failure */ strcpy(this-_buffer, str); return this;String StringCon2(String this, String other) (return StringCon1(thi

29、s, StringToChar(other);const char *StringToChar(String this) ( return this-_buffer;void StringDes(String this) (free(this-_buffer); /* release buffer */Object_Des(&this-super); /* destroy superclass */表2 String类的定义还有什么没有实现那么在用C语言而不是一个面向对象语言时,还有什么没有实现呢?用我在本文中演 示的这些技巧你没有必要再去牺牲更多的便利性和可读性,因为你已经可以很容易地把大多 数重要的面向对象概念映射到C中。你也不必费尽心思去维护,因为你可以让很多任务成 为自动的。这种实现的最重要的特种是,为祖先类添加新的属性成员和方法(包括虚函数和 接口)时不需要对子类进行任何的手动修改。真正的问题是,C在为对象进行初始化和销毁时比面向对象语言有着更加严格的编程规 则,尤其在那些垃圾回收机制方面。但这是众所周知的C语言的缺陷,不是很容易就能修 复的。(比如,C+还在大

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论