用C实现继承和多态

上传人:ldj****22 文档编号:35889318 上传时间:2018-03-22 格式:PDF 页数:9 大小:347.69KB
返回 下载 相关 举报
用C实现继承和多态_第1页
第1页 / 共9页
用C实现继承和多态_第2页
第2页 / 共9页
用C实现继承和多态_第3页
第3页 / 共9页
用C实现继承和多态_第4页
第4页 / 共9页
用C实现继承和多态_第5页
第5页 / 共9页
点击查看更多>>
资源描述

《用C实现继承和多态》由会员分享,可在线阅读,更多相关《用C实现继承和多态(9页珍藏版)》请在金锄头文库上搜索。

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

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

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

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

5、在一起。 函数名字的变更是 名字修饰 (也称命名管理)的一 部分,这在多数 C+编译器中隐含地作了处理。由于名字修饰消除了不同类之间的方法名 (译者注:函数名)冲突,它有效地将平面式的 C 函数名字空间划分成了包含在类中的、 嵌套的、独立的名字空间。 接下来,我会从另一方面通过命名约定的方式说明权限控制。在 C 中,你只能在权限 允许范围内,通过对某个特定的属性或者方法的引用来表明你的访问目的。 通过属性或者方 法的名字来表明你的访问目的,要好于在声明的地方加一行注释的方式。 通过这种更合理的 “”命名方式,在代码中的任何部分,只要有对类的成员的 无意 的访问,就可以很容易地检 测到。大多数面

6、向对象设计区分以下三种保护机制: Private - 只有在当前类中可访问 Protected - 在当前类及其子类中可访问 Public - 任何地方都可访问(这在C 中是默认的)我使用双下划线前缀(_foo)来表示一个私有属性(private)。注意,一般说来,没 有必要把私有方法(译者注:私有成员函数)暴露在类的声明文件中(.H 文件)。而且, 你应该在你的源文件中将它们完全隐藏起来(在.C 文件中声明为 static)。对于保护属性 (protected)成员,我使用单下划线(_foo, String_foo)。在声明公有成员(public members) 时要杜绝使用下划线(foo

7、, StringFoo)。 这样,按照这种命名约定,一个名字中的下划线的 “”状态就成为了访问权限的 信号 ,而这个访问权限正是需要根据代码上下文作出检查的。 由 于公有成员可以不受限制的使用,所以他们不需要特别的修饰。 每一个类必须提供至少一个构造函数方法,用以实现结构体属性的初始化。 调用构造函 数应该成为初始化的唯一方法。 否则,这个对象的内部结构就必须被暴露在外,这就跟封装 的思想冲突了。 一个类可以提供一个析构器,但这不是强制性的。 它是一个函数方法,负责释放一个对 象在它的生命周期中分配的资源。 虽然实例化一个类可能有多种方法(不同的构造函数使用 不同的参数),但一个对象的析构应该

8、只有一个方法。 由于构造函数和析构函数的特殊角色,我又要提议一个一致的命名模式。 我使用一个基 “本名称 Con”(FooCon1,FooCon2)“和 Des”(FooDes)来分别表示构造函数和析构函数。我建议, 一个构造函数返回一个指向已经正确初始化过的结构体的指针,或者在初始化失败时返回 NULL“。析构函数只使用 this”指针作为形参,并返回NULL。 对象可以静态分配、动态分配(使用堆)、或者自动分配(使用栈)。由于C 语法的限 制,你一般不能在定义一个对象时通过调用构造函数对它进行初始化。 对于静态对象,你甚 至不能调用它的构造函数,因为函数调用不允许在静态初始化程序中发生。

9、自动对象必须在 一个函数体的开头定义,在这个时候,你经常不具备足够的初始化信息来调用合适的构造 函数。 因此,你不得不经常要把分配对象和初始化对象分离开来。 你应该像看待所有其它的 C 变量一样来看待对象,因为你永远不能在初始化它们之前使用它们。 一般的,一旦初始化信息都可用了,你就可以对对象进行初始化了。 有些对象可能需要析构函数,对所有对象来说,在它们被废弃或者生命周期结束时调 用它们的析构函数是个很好的编程习惯。 在后面我将演示一个对所有类都适用的虚析构函数。继承继承是根据已存在的类来定义新类和更加定制化的类的一种机制。当一个子类 (subclass)从它的父类(superclass)继

10、承而来时,这个子类包含了父类所有属性和方法的定 义。 通常地,子类会通过增加自己的属性和方法来扩展父类。 子类的实例化对象包含了子类 和父类定义的所有的数据。它们可以运行子类和父类的所有的方法。 这种类的关系在 C 中可以通过把父类的属性结构体嵌入到子类结构体中并作为第一个 成员。如图1 所示。按这样的结构方式可以实现这种属性组合:一个指向子类的指针总是可以被安全的转 换成指向父类的指针(向上追溯)。特别的,如果有 C 函数需要一个父类的指针,那么这 个指向子类的指针总是可以作为参数传递过去(由于 C 中的强制类型匹配,你应该显式地 对这个指针作类型转换)。这就意味着所有父类的方法在子类中都是

11、适用的-换句话说, 它们被继承下来了。 这种简单的方法只能处理简单的继承(只有一个父类),因为一个具体多个父类的子 类无法将所有父类的属性成员都作出正确的属性组合。 这里我将被继承的类命名为 super,这可以使各个类之间的继承关系更加明显,而且跟 Java 更加相似。Super 类提供了一个方法来访问 super 类的属性成员,比如,一个孙类对象 可以通过这个方式来访问祖先类的保护属性成员_foo:this- super.super._foo。 继承增加了类构造函数和析构函数的责任。 由于每个子类对象都包含一个嵌入的父类对 象,所以这个子类的构造函数必须要考虑到父类需要初始化的部分。 为了避

12、免任何潜在的依 赖关系,在子类进行初始化属性成员之前应该首先调用父类的构造函数。 而对于析构函数, 恰恰跟这个顺序相反,继承下来的那一部分应该在最后一步销毁。 我还在我的实现中借鉴了 Java 只有一个抽象基类 Object 的概念,这意味着没有类可以 被单独定义,而是必须以 Object 类作为类层次体系的根节点,从别的类扩展得到。这种设图 1 子类结构体中嵌入父类并将其作为第一个成员(图 a);内存布局图(图 b)置方式在混杂的编程实践中(对一个面向过程的语言添加面向对象属性)显得尤为方便, 因为每一个对象最终都可以看做是 Object 类的一个实例,而且 Object 类已经跟所有别的类

13、 显式的区分开了。 这跟C+有所不同,在 C+中,每一个结构体都等同于一个类。 正如我将 要演示的,我给 Object 类添加了重要的行为,这个行为接下来被所有其它类所继承,进而 实现了多态。表1 演示了一个String 基础类的声明,它扩展了 Object 类。 这个类封装了一个字符缓冲 区(_buffer),提供了两个构造函数(StringCon1,StringCon2)、一个析构函数和一个对字符 缓冲区进行只读访问的方法 StringToChar。这个类是通过预定义的宏(CLASS, VTABLE, METHODS, END_CLASS)来声明的,这些宏在 object.h 中进行了定义

14、和说明,你可以从 下载获得。多态一个扩展的类经常对继承而来的一个或多个方法进行重新实现,以此对它的祖先类的 行为进行覆写。比如,Object 类定义了析构函数 Object_Des,从Object 类扩展而来的 String 类(参考表1)用它自己的析构函数 StringDes 覆写了这个行为。 我们假设在代码中的某个地方 销毁了一个混杂的含有多个 Object 通用类型指针的数据容器,因为 Stirng 类(或者其它的 类)是从 Object 类继承来的,在这个数据容器中的一些指针可能实际上是指向 String 对象, 如果这时你的代码正确地调用并执行了 StringDes 来销毁这些 S

15、tring 对象(如果是别的类对象,那对应的就是别的析构函数),那么你的代码就是多态的。 多态行为需要进行函数方法 解析,而方法解析依赖的是动态运行时的类(String),而不是指针类型的类(Object)。 这又被称为动态绑定。 在函数方法解析时添加一个额外的间接调用可以在 C 中有效的实现动态绑定。不同于 直接调用一个方法(C 函数),你可以调用一个由函数指针指向的函数体,这个函数指针 在每个对象所引用的描述符类2中定义。这个描述符类(有时被称为虚拟列表或者VTABLE)实际上是一系列跟虚函数对应的函数指针-换句话说,是留给以后的子类进行 覆写的方法。 在前面的例子中,Object 类对虚

16、析构函数的实现看上去好像是下面这个样子的。Object 类在它的描述符类中声明了一个函数指针 Des:每一个 Object 类的实例对象维护了一个指向这个描述符类的指针(称为虚指针或者 VPTR-请参考 Eckel,1995):那么虚析构函数的动态绑定就是:在这里obj 指向一个 Object 结构体。 注意,obj 在这里用到了两次:一次是为了函数方法解析,一次是作为 this 指针参数。 动态绑定需要至少两次的内存访问和至少一次的区别于直接函数调用的额外调用 (Rumbaugh,1991)。动态绑定所消耗的内存中保存了每一个对象中的虚指针(从Object 继 承而来的),再加上为每个类存储VTABLE 所消耗的内存。 描述符类可以把自己作为一个 VTABLE 类的唯一实例(一个由 VTABLE 对象表示的 类)。所以你可以通过对 VTABLEs 运用嵌套的技术来实现虚函数的继承。这已经在宏 VTABLE(参考表 1)中封装好了。 所有的描述符

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 行业资料 > 其它行业文档

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号