使用Ansi-C进行面向对象编程

harmonyxu

贡献于2011-10-27

字数:0 关键词: C/C++开发

使使使用用用ANSI-C进进进行行行面面面向向向对对对象象象编编编程程程 原著:阿塞尔-托彼亚 斯莱内尔 翻译:OOC翻译组 编辑:ruanbeihong at gmail.com July 29, 2009 2 Object-oriented Programming with ANSI-C Axel-Tobias Schreiner RIT ( Rochester Institute of Technology ) Rochester NY 14623 USA 前前前言言言 Translator: tom_xx_hu at yahoo.ca 没有能解决一切问题的编程技术 没有只产生正确结果的编程语言 没有每个项目都从头写的程序员 面世十多年后,面向对象编程现在是灵丹妙药。本质上,除了应用20多年来一贯推行的良好编程原则 之外,面向对象编程并没有更多新东西。C++ (Eiffel、Oberon-2、SmallTalk……随便你举哪个来说) 之所以是“新编程语言”是因为它是面向对象的。当然,如果你不想或不知道怎么做,你不必非得用它 去面向对象不可。即使要面向对象,普通ANSI-C同样可以做。虽然子程序的概念与计算机一样久远,优 秀的程序员也总是随身带着他们的工具和程序库,但是只有面向对象的思想,才能真正使得代码可以在 其它项目里可再用。 本书既不推崇面向对象的编程也不谴责传统方式的编程。我们将仅仅发掘如何用ANSI-C实现面向对 象的编程,要用到哪些技术,为什么这些技术帮助我们解决更大的问题,以及如何制约过度的通用性并 及早捕获出错。本书中,你会接触到所有这些术语——类、继承、实例、连接、方法、对象、多态以及 其它——不过我们剥去其神奇外衣,看看它们如何转化为我们一直以来熟悉的、做过的事情。 发现ANSI-C是一门完全面向对象的语言对于我是很有趣的事情。要分享我这份乐趣,你起码得 对ANSI-C足够熟练——至少对结构、指针、原型、函数指针不陌生。随着本书的进展,你将遇到一个“ 新语言”——按照“新语言”(new speak)在Orwell和韦氏词典的解释,“新语言”“设计目的就是缩 减思维的广度”——而我会尽力证明,它仅仅汇合了所有的,那些你总想运用到一致的途径中去的良好 的编程原则。结果,你可以成为一个更专业的ANSI-C程序员。 前六章建立用ANSI-C做面向对象编程的基础。我们从一个谨慎的抽象数据类型的信息隐藏技术开 始,然后加入基于动态连接的通用抽象函数,再通过明智而谨慎地扩充结构来继承代码。最后,我们将 所有上述发展放进一个类树中,使代码的维护容易得多。 编程需要规范。良好的编程需要实行严格的规范、遵守众多原则标准以及确保正确无误的防范措施。 程序员使用工具,而优秀的程序员制作工具来一劳永逸地处理那些重复的日常事务。用ANSI-C的面向对 象的编程需要相当大量的有“免疫”功能的编码——名称可能变化但结构不变。因此,在第7章里我们搭 建一个小小的预处理器,用来创建所需要的模板。这模板很像是另一个方言式面向对象的语言(也许就 叫yanhoodl?)。不过它不是方言,它剔除“方言”中枯燥无味的东西,让我们专注于用更好的技术解 决问题的创新。OOC(请原谅这个命名1)是坚韧的:我们创造了它,了解它,能够改变它,而且它也 如我们所愿使用ANSI-C。 余下章节继续深入讨论我们的技术。第8章加入动态类型检测来实现错误的早期捕获。第9章讲自动初 始化防止另一类软件缺陷。第10章引入委托代理,说明类和回调函数如何协作,共同完成(比如)简化 标准主程序的生成这样的常规任务。其他章节专注于用类方法堵塞内存泄漏,用一致的策略存储和加载 结构数据,用嵌套的异常处理系统实行规范的纠错。 在最后一章,我们搁置ANSI-C,做了一个期望已久的鼠标操作的计算器——先是针对curses然后是 针对X Window。这个例子清晰地表明:即使被迫适应外部库和类树的混杂,有了对象和类,我们可以 多么精致地设计和实现一个应用。 1译者注:OOC(out of cash)在英文中又有缺现钱的意思,故作者有此调侃 3 4 每一章都有总结。我试图在每章总结中给随意浏览的读者一个概述的梗概以及它对此后章节的重要 性。各章多数都备有习题,不过不必当成正式作业来完成,因为我坚定的相信读者应当自己实践。由于 我们是从从无到有建立新的技术——尽管有些个例应该能够从中获益——我一直避免制作和使用庞大的 类库。如果你真要理解面向对象的编程,更重要的是首先掌握面向对象的技术并且在代码设计范围内运 用这些技术;至于开发中依赖使用他人的库,应当放到后面。 本书的一个重要部分是所附源码软盘,其上有一个DOS文件系统,包括一个shell脚本来生成所有按 章排序的源码。还有一个ReadMe文件——在你执行make命令前要先看看这个文件。使用diff类的工具追 踪根类和ooc报告在后续章节的演化也是非常有帮助的。 这里呈上的技术起源于我对C++的失望。当时我需要面向对象技术实现一个交互式编程语言,但我 失望地意识到没法用C++“敲打”出一个可移植的东西来。我转向我所了解的,ANSI-C,然后我完全 能够做到我本来要做的事情。我将这个经历告诉课程上的几个人,然后他们用同样的方法完成了他们 的工作。如果不是布赖恩·克尼翰(Brian Kernighan)以及我的出版商翰斯·尼科拉斯(Hans-Joachim Niclas)、约翰·维特(JohnWait)鼓励我出版,这个故事可能就止于是我一时冲动的脚注(当然在以 后上课要用时再从头来过一遍)。我感谢他们和所有帮助本书的进展并且与此同甘共苦的人。最后也是 最重要的,感谢我的家庭——面向对象绝不会代替餐桌上的面包。 1993年10月于Hollage 阿塞尔-托彼亚 斯莱内尔 (Axel-Tobias Schreiner) Contents 1 抽抽抽象象象数数数据据据类类类型型型——————信信信息息息隐隐隐藏藏藏 7 1.1 数据类型 .............................................7 1.2 抽象数据类型 ...........................................7 1.3 一个例子——Set.........................................8 1.4 内存管理 .............................................8 1.5 对象 ................................................9 1.6 一个应用实例 ...........................................9 1.7 一个实现——Set......................................... 10 1.8 另一个实现——Bag....................................... 12 1.9 总结 ................................................ 14 1.10 练习 ................................................ 14 2 动动动态态态连连连接接接——————通通通用用用函函函数数数 15 2.1 数据类型 ............................................. 15 2.2 方法、消息、类与对象 ...................................... 16 2.3 选择器、动态连接与多态 .................................... 17 2.4 一个应用 ............................................. 19 2.5 一个实现——String....................................... 20 5 6 CONTENTS Chapter 1 抽抽抽象象象数数数据据据类类类型型型——————信信信息息息隐隐隐藏藏藏 1.1 数数数据据据类类类型型型 Translator: tom_xx_hu at yahoo.ca 数据类型是每种编程语言不可分割的一部分。ANSI-C就有int,double和char这几种。程序员们极少满足 于现成可用的类型,而编程语言通常提供了从预定义的类型构建新的数据类型的机制。其中一种简单的 方法是构造集合体,例如数组(array),结构体(structure)和联合体(union)。被C. A. R. Hoare称 为“我们永远无法收回的一步”的指针,允许我们表示和操作具有本质上无限复杂性的数据。 到底什么是数据类型?我们可以从不同的角度来看这个问题。一种数据类型是一个值(value)的集 合——char就有256种不同的值,int就更多了;两者都是间隔均匀的,或多或少表现得像是数学里的自 然数或整数。double类型的值就更多了,但是它们表现得却不像数学里的实数。 一种替代的方式是,我们可以定义一种数据类型,它包含了一组值的集合以及作用于该集合之上的 一组操作。典型地来说,这些值是计算机能够表示的,而这些操作多少反映了可用的硬件指令。在这方 面ANSI-C中的int就做得不太好:值的集合会随着机器的不同而变化,并且有些操作比如说算数右移也 会表现得有所不同。 更加复杂的例子也没有太大的意义。做为例子我们可以用结构体来定义线性表的的一个元素。 typedef struct node { struct node * next; information ... } node; 至于这些操作我们指定像这样的函数头部: node * head (node * elt, const node * tail); 但是,这种办法显得非常松散。好的编程原则要求我们封装数据项的表示并且只声明合适的操作。 1.2 抽抽抽象象象数数数据据据类类类型型型 Translator: yfwill at gmail.com 7 8 CHAPTER 1. 抽抽抽象数据类型——信息隐藏 如果我们不把一种数据类型的具体表示展现给用户,那么我们称它是“抽象的”。在理论的层面这要求 我们通过数学公理和适当的操作来指定这种数据类型的属性。例如,向队列中加入元素后才能删除,从 队列中取出的元素的顺序和放入时的顺序相同。 抽象数据类型给了程序员巨大的灵活性。由于表示不是定义的一部分,我们可以自由选择最容易的或 者是效率最高的表示方法来实现定义。如果我们能正确的发布必要的信息,那么如何使用该数据类型和 如何实现它就是完全独立的了。 抽象数据类型满足了良好的编程原则对于信息隐藏以及“分而治之”方式的需求。信息(例如数据项 的表示)只给予需要知道的人:实现者而不是使用者。通过抽象数据类型我们干净地把实现和使用两种 编程任务分开:我们在把一个大系统分成小模块的道路上顺利前行。 1.3 一一一个个个例例例子子子——————Set Translator: yfwill at gmail.com 那么我们如何实现一种抽象数据类型呢?比如,我们考虑一个元素的集合。该集合上可以进 行add,find和drop1操作。它们作用于一个集合和一个元素上,并返回一个元素,刚加入集合的 (add),从集合中找到的(find)或者从集合中删除掉的(drop)元素。find可以用来实现一个条件 判断contains,它可以告诉我们集合中是否已经包含某元素。 从这个角度来看,集合是一个抽象数据类型。为了声明我们能对一个集合做什么,我们创建了一个头 文件Set.h: #ifndef SET_H #define SET_H extern const void * Set; void * add (void * set, const void * element); void * find (const void * set, const void * element); void * drop (void * set, const void * element); int contains (const void * set, const void * element); #endif 其中的预处理语句是为了保护下面的声明:无论我们多少次包含Set.h,C编译器只会看到一次声明。 这种保护头文件的技术非常的标准,GNU C预处理器能够识别它,并且当保护符号已经定义的时候不会 访问这个文件。 Set.h是完整的,但它有用吗?我们几乎不能透露或者呈现更少的内容了:Set头文件必须在某种程度 上表示出我们正在进行操作的是集合这个事实。add()接收一个元素,把它加到集合中,并且返回该元素 ——无论它是新加入的或者已经存在于集合中;find()在集合中查找一个元素,返回该元素中或者空指 针;drop()定位元素,把它从集合中删除,并且返回被删除的内容;contains()把find()的结果转换成一 个真值。 我们自始至终的使用了泛型指针void *。一方面它使得了解集合到底是什么变得不可能,但另一方 面,它允许我们向add()和其他几个函数传递任何东西。并不是任何东西会表现得像一个集合或者一个元 素一样——为了信息隐藏我们牺牲了类型安全。然而,在第8章中我们将会看到这种方法可以安全的实 现。 1.4 内内内存存存管管管理理理 Translator: yfwill at gmail.com 1不幸的是,remove是一个ANSI-C库函数,用来删除一个文件。如果我们用remove来命名一个set的函数,我们就不能再包 含stdio.h这个文件了。 1.5. 对象 9 我们可能忽略了一些事情:如何获取一个集合?Set是一个指针,而不是一个用typedef定义的类型, 所以我们不能够定义一个类型是Set的局部或者全局变量。作为替代,我们用指针来引用集合和元素,并 且在new.h中声明所有数据项的产生和删除方法: void * new (const void * type, ...); void delete (void * item); 就像Set.h一样,这个文件受到了预处理符号NEW_H的保护。本书只是展示了每个新的文件中有意思的 部分,示例的完整代码包含在源代码磁盘中。 new()接收像Set这样的描述符以及更多可能的初始化参数,并且返回一个指针,该指针指向新产生的 符合描述参数的数据项。delete()接收一个由new()产生的指针,回收其相关的资源。 new()和delete()大致上是ANSI-C函数calloc()和free()的一个前端。如果它们是的话,描述符至少需要 指明内存大小。 1.5 对对对象象象 Translator: yfwill at gmail.com 如果我们需要了解集合中的任何情况,就需要头文件Object.h中描述的另一个抽象数据类型Object: extern const void * Object; /* new(Object); */ int differ (const void * a, const void * b); differ()能够比较对象:如果它们不相等就返回true,否则返回false。这样的描述为strcmp()的应用留 下了空间:对于某些对象的配对(pair),我们也许会选择返回负值或正值来指明两者的次序。 真实生活中的对象需要有更多的功能来完成一些有实际意义的事。现在,我们把自己局限于单纯的成 为集合的成员的需求。如果我们构建一个更大的类库,就会发现一个集合——或是别的东西——也是一 个对象。在这一点上,大量的功能性函数多多少少给了我们自由。 1.6 一一一个个个应应应用用用实实实例例例 Translator: yfwill at gmail.com 有了这些头文件,也就是这些抽象数据类型的定义,我们就可以写一个程序main.c: #include #include "new.h" #include "Object.h" #include "Set.h" int main () { void * s = new(Set); void * a = add(s, new(Object)); void * b = add(s, new(Object)); void * c = new(Object); if (contains(s, a) && contains(s, b)) puts("ok"); if (contains(s, c)) puts("contains?"); if (differ(a, add(s, a))) 10 CHAPTER 1. 抽象数据类型——信息隐藏 puts("differ?"); if (contains(s, drop(s, a))) puts("drop?"); delete(drop(s, b)); delete(drop(s, c)); return 0; } 我们建立了一个集合并且加入了两个新的对象。如果一切正常,我们能在集合中找到这两个对象,而 且不会存在任何新的对象了。所以这个程序只是简单的打印出ok。 对于differ()的调用展示了一个语义:一个数学集合只能包含一份a对象的拷贝;如果想要再加入一 个a对象的话就会返回原始的对象并且differ()应该返回false。与此类似,一旦我们删除了一个对象,它就 不应该还在这个set中了。 删除一个不在集合中的元素会导致一个空指针被传递给delete()。目前来说,我们坚持free()的语义并 且要求它是可以被接受的。 1.7 一一一个个个实实实现现现——————Set Translator: yfwill at gmail.com main.c可以正确的编译,但是在我们链接并且执行该程序之前,我们必须实现该抽象数据结构并完成 存储管理的部分。假定对象里不存放任何信息并且每个对象至多属于一个集合,则可以把每个对象和集 合看作heap[]数组的索引——都是一个个不重复的小正数。如果某对象是集合的元素,那么该对象的数 组元素中存储着表示其所在的集合的整数索引值。因此,对象指向包含它的集合。 方案一是如此的简单,我们可以把所有的模块集成到一个文件:Set.c中。集合和对象的表示是一样 的,所以new()不再考虑参数所给的类型描述。new() 返回heap[]数组中值为0的一个索引作为新元素: { #if ! defined MANY || MANY < 1 #define MANY 10 #endif static int heap [MANY]; void * new (const void * type, ...) { int * p; /* & heap[1..] */ for (p = heap + 1; p < heap + MANY; ++ p) if (! * p) break; assert(p < heap + MANY); * p = MANY; return p; } } 我们用0值来标识heap[]数组中可用的成员;所以不能返回heap[0]的引用——因为如果它是一个集 合,那么它所包含的元素可能包括索引值0。 在向集合中增加对象之前,将一个不可能取到的索引值MANY赋给该元素对应的数组成员,这 样new()就不可能再次将其分配为新元素,我们也不会将其误认为某个集合的元素。 new()会用完内存。这是很多“不会发生”的错误的第一个。我们简单的使用ANSI-C的assert()宏来 标记这个错误。更为可行的方法应该至少打印出适当的错误信息或者使用用户可以重写的通用错误处理 1.7. 一个实现——SET 11 函数。然而,就增进编程技术而言,我们更倾向于保持代码的干净整洁。十三章中我们会介绍普适的方 法来处理异常。 delete()必须小心处理空指针。当heap[]数组中的成员被设为0时即完成了对其的回收。 void delete (void * _item) { int * item = _item; if (item) { assert(item > heap && item < heap + MANY); * item = 0; } } 我们需要一个统一的方式来处理这些通用指针,故我们用名字前面加下划线的方法来表示这些通用指 针,并且只用他们来初始化需要的特定类型和名称的局部变量。 集合是由其对象表示出来的:每个集合中的元素都指向集合。如果一个元素对应的数组成员值 为MANY,那么该元素就可以被加入到集合中,不然的话它一定已经在集合中了,因为我们不允许一个 对象属于多个集合。 void * add (void * _set, const void * _element) { int * set = _set; const int * element = _element; assert(set > heap && set < heap + MANY); assert(* set == MANY); assert(element > heap && element < heap + MANY); if (* element == MANY) * (int *) element = set - heap; else assert(* element == set - heap); return (void *) element; } assert()宏保证了我们不会处理指向heap[]数组之外的元素,并保证集合不能再属于别的集合,也就是 集合对应的数组成员的值应为MANY。 其余的函数也很简单。find()用来检验其element参数对应的heap[]中的数组成员值是不是等于集合对 应的数组序号值。 void * find (const void * _set, const void * _element) { const int * set = _set; const int * element = _element; assert(set > heap && set < heap + MANY); assert(* set == MANY); assert(element > heap && element < heap + MANY); assert(* element); return * element == set - heap ? (void *) element : 0; } contains()函数将find()的结果转化为布尔值: int contains (const void * _set, const void * _element) { return find(_set, _element) != 0; } 12 CHAPTER 1. 抽象数据类型——信息隐藏 drop()利用find()函数来检查要被drop的元素是否真的属于该集合。如果是,我们恢复它的状态,标 记为MANY,并返回它。 void * drop (void * _set, const void * _element) { int * element = find(_set, _element); if (element) * element = MANY; return element; } 如果我们挑剔一点,我们还得保证要被除去的元素也不能属于别的集合。如果这样我们拷贝一 些find()中的代码来实现drop()。 我们的实现方法很不寻常。事实上在集合操作中我们甚至不需要differ()函数。但是我们依旧要写出 它,因为我们应用程序的需要。 int differ (const void * a, const void * b) { return a != b; } 元素对象不同也就是他们在数组中对应的序号不同,也就是仅仅比较元素对应的指针就足够了。 对于这个方案来说,我们已经完成所有的工作了。至于Set和Object描述符其实并不需要,我们这里 加上只是使C编译器高兴: const void * Set; const void * Object; 在main.c中我们就用这两个指针来创造新的集合和元素。 1.8 另另另一一一个个个实实实现现现——————Bag Translator: yfwill at gmail.com 不需要修改Set.h中提供的接口我们便可以更改其实现方法。这里我们用动态内存的方法,并且把集 合和元素看作结构体: struct Set { unsigned count; }; struct Object { unsigned count; struct Set * in; }; count用来记录集合中元素的个数,而对于每个元素来说,count记录该元素已经被加入某集合多少次 了。如果用drop()函数从集合中删除该元素的时候我们仅仅对count减一,直到count值为0时才真正删除 该元素,此时的数据结构就叫做Bag,也就是有引用次数记录的元素的集合。 因为我们要用动态存储的方法来表示集合和元素,就需要初始化描述符Set和Object,这样new()才知 道需要预留多少内存。 static const size_t _Set = sizeof(struct Set); static const size_t _Object = sizeof(struct Object); const void * Set = & _Set; const void * Object = & _Object; new()函数变得简单多了: 1.8. 另一个实现——BAG 13 void * new (const void * type, ...) { const size_t size = * (const size_t *) type; void * p = calloc(1, size); assert(p); return p; } delete()函数也可以将其参数直接传给free()函数——在ANSI-C中空指针也可以传给free()函数。 add()必须更多的信任其指针参数了。其将元素引用计数与集合元素数都加一。 void * add (void * _set, const void * _element) { struct Set * set = _set; struct Object * element = (void *) _element; assert(set); assert(element); if (! element -> in) element -> in = set; else assert(element -> in == set); ++ element -> count, ++ set -> count; return element; } find()函数依旧要检查要查找的元素是不是指向着合适的集合: void * find (const void * _set, const void * _element) { const struct Object * element = _element; assert(_set); assert(element); return element -> in == _set ? (void *) element : 0; } contains()函数基于find()所以没有任何变化。 如果drop()函数在集合中发现了其参数所指向的元素,便将该元素的引用计数和集合元素数减一。如 果该元素的引用计数变为0,则从集合中删除该元素。 void * drop (void * _set, const void * _element) { struct Set * set = _set; struct Object * element = find(set, _element); if (element) { if (-- element -> count == 0) element -> in = 0; -- set -> count; } return element; } 现在又新加入一个函数count(),用来计算集合中元素的个数: unsigned count (const void * _set) { const struct Set * set = _set; 14 CHAPTER 1. 抽象数据类型——信息隐藏 assert(set); return set -> count; } 当然了,如果直接让程序去读取结构体的count成员会简单很多,但是我们坚持不应泄漏集合的表示 方法。与让程序能够直接修改程序关键数值的危险相比,多调用几次函数的代价真不算什么。 包和集合不同:在包中,任何一个元素可以被多次加入;只有被扔掉的次数和放入包中的次数想 同时,它才会从包中消失。在1.6的程序中,我们将一个元素a两次加入集合,在将其从包中删除一次 后,contains()依旧能够从包中找到该元素。程序会有如下输出: ok drop? 1.9 总总总结结结 Translator: yfwill at gmail.com 对于一个抽象数据结构来说,我们完全隐藏了其所有的实现细节,比如数据项的表示方法等等。 程序代码只能访问头文件,该文件仅仅声明数据类型的描述符指针以及作用于该数据类型的操作。 描述符指针传给通用函数new()来获得一个指向数据项的指针。这个指针传给另一个通用函数delete()来 回收其相关的资源。 一般来说,每个抽象数据类型都在一个单独的代码文件中实现。理想情况下,它也不能看到别的数据 类型的具体实现。描述符指针应该至少指向一个常数值size t用来指明数据项需要的空间。 1.10 练练练习习习 Translator: yfwill at gmail.com 如果一个元素可以同时属于多个集合,集合的实现就必须改变了。如果我们继续用一些惟一的小整数 来表示元素,并且限定了可用元素的上限,那么就可以把一个集合表示成一个位图,并以一个长字符串 的形式存储。其中某一位如果是选中的就表示其对应的元素在该集合中,否则则不在。 更通用更常见的方法是用线性链表的节点存放集合中元素的地址。这种方法对元素没有任何限制,并 且允许在不了解元素的实现方法的情况下实现集合。 能查看单个元素对于调试是很有帮助的。一个合理的通用解决方法是定义两个函数: int store (const void * object, FILE * fp); int storev (const void * object, va_list ap); store() 向文件指针fp所指文件内写入对元素的描述。storev()则使用va arg()取得ap所指之参数表中 的文件指针,然后向该文件指针写入对元素的描述。两个函数都返回写入的字符数。在下列的集合函数 中storev()更实用一些: int apply ( const void * set, int (* action) (void * object, va_list ap), ...); apply() 对set中的每一个元素调用action(),并将参数表中其余的参数传递给action()。action() 不可以改 变set,但可以通过返回0而提早终止apply()。若所有元素都得到处理则apply()返回true。 Chapter 2 动动动态态态连连连接接接——————通通通用用用函函函数数数 2.1 数数数据据据类类类型型型 Translator: ruanbeihong at gmail.com 让我们先实现一个简单的字符串数据类型,在后面的章节里,我们会把它放入一个集合中。在创建一 个新的字符串时,我们分配一块动态的缓冲区来保存它所包含的文本。在删除该字符串时,我们需要回 收那块缓冲区。 new() 负责创建一个对象,而delete() 必须回收该对象所占用的资源。new() 知道它要创建的对象是 什么类型的,因为它的第一个参数为该对象的描述符。依据该参数,我们可以用一系列if语句来分别处 理每一种数据类型的对象的创建。这种做法的的缺点是,对我们所要支持的每一种数据类型,new()中都 要显式地包含特定于该数据类型的代码1。 delete() 所要解决的问题更为棘手。它也必须随着被删除对象的类型的不同而作出不同的动作:若是 一个String 对象,则必须释放它的文本缓冲区;若是在第一章中用过的那种Object 对象,则只需回收该 对象自身;而若是一个Set 对象,则需要考虑它可能已经请求了很多内存块用来储存其元素的引用。 我们可以给delete() 添加一个参数:类型描述符或者做清理工作的函数,但这种方式不仅笨拙,而且 容易出错。有一种更为通用更为优雅的方式,即保证每个对象都知道如何去销毁它所占有的资源。可以 让每个对象都存有一个指针域,用它可以定位到一个清理函数。我们称这种函数为该对象的析构函数。 现在new() 有一个问题。它负责创建对象并返回一个能传递给delete() 的指针,就是说,new() 必须 配置每个对象中的析构函数信息。很容易想到的办法,是让指向析构函数的指针成为传递给new() 的类 型描述符的一部分。到目前为止,我们需要的东西类似如下声明: struct type { size_t size; /* size of an object */ void (* dtor) (void *); /* destructor */ }; struct String { char * text; /* dynamic string */ const void * destroy; /* locate destructor */ }; struct Set { ... information ... const void * destroy; }; 1译者注:即,需要将数据类型的信息硬编码到new()中 15 16 CHAPTER 2. 动态连接——通用函数 看起来我们有了另一个问题:需要有人把析构函数的指针dtor 从类型描述符中拷贝到新对象 的destory 域,并且该副本在每一类对象中的位置可能还不尽相同。 初始化是new() 工作的一部分,不同的类型有不同的事情要做——new() 甚至需要为不同的类型而配 备不同的参数列表: new(Set); /* make a set */ new(String, "text"); /* make a string */ 对于初始化,我们使用另一种特定于类型(译者住:与类型有绑定性质)的函数,我们称之为构造函 数。由于构造函数和析构函数都是特定于类型的,不会改变,我们把他们两个都作为类型描述的一部分 传递给new() 。 要注意的是,构造函数和析构函数不负责请求和释放该对象自身所需的内存,这是new() 和delete()的 工作。构造函数由new() 调用,只负责初始化new() 分配的内存区域。对于一个字符串来说,构造函数 做初始化工作时确实需要申请一块内存来存放文本,但struct String 自身所占空间是由new() 分配的。 这块空间最后会被delete() 释放。而首先要做的是,delete() 调用析构函数,做与构造函数的初始化相逆 的工作,然后才是delete() 回收new() 所分配的内存区域。 2.2 方方方法法法、、、消消消息息息、、、类类类与与与对对对象象象 Translator: cuikai1981@gmail.com delete()必须能够在不知所给对象类型的情况下定位到析构函数。因此,需要修订第2.1中的声明,对于 所有传入delete()的对象,强调用于定位析构函数的指针必须位于这个对象的头部,而不管这些对象具体 是什么类型。 这个指针又应该指向什么呢?如果我们有的只是一个对象的地址,那么这个指针可让我们访问这个对 象的类型信息,诸如析构函数。这样看起来我们同样也将很快建立一个其他的类型信息函数,诸如显示 对象的函数,或者比较函数differ(),又或者可以创建本对象完整拷贝的clone()函数。因此我将让这个指 针指向一个函数指针表。 如此看来,我们认识到这个指针索引表必须是类型描述的一部分,并且传给new(),并且显而易见的 解决方式便是把整个的类型描述作为一个对象: struct Class { size_t size; void * (* ctor) (void * self, va_list * app); void * (* dtor) (void * self); void * (* clone) (const void * self); int (* differ) (const void * self, const void * b); }; struct String { const void * class; /* must be first */ char * text; }; struct Set { const void * class; /* must be first */ ... }; 2.3. 选择器、动态连接与多态 17 我们的每一个对象开始于一个指向它自身所拥有的类型描述的指针,并且通过这个类型描述,我 们能定位这个对象类型描述信息:.size是通过new()分配的这个对象的长度;.ctor指针指向被new()函数 调用的构造函数,这个构造函数接受被申请的区域和在初始时传递给new()的其余的参数列表;.dtor指 向被delete()调用的析构函数,用来销毁接受到的对象;.clone指向一个拷贝函数,用来拷贝接受到的对 象;.differ指针指向一个用来将这个对象于其他对象进行比较的函数。 大体上看看上面这个函数列表就能发现每个函数都是通过对象来选择作用于不同的对象的。只有构 造函数要处理那些部分初始化的存储区域。我们称这些函数叫做这些对象的方法。调用一个方法就叫做 一次消息,我们已经用self作为函数参数来标记接收该消息的对象。当然这里我们用的是纯C函数,所 以self不一定得是函数的头一个参数。 一些对象将共享同样类型描述,就是说,他们需要同样数量的内存和提供同样的方法供使用。我们称 所有拥有同样类型描述的对象为一个类;单独的一个对象称作为这个类的实例。到现在为止,一个类、 一个抽象数据类型、一些可能的值及其的操作(就是说一个的数据类型)几乎是一样的。 一个对象是一个类的实例,也就是说,在通过new()为它分配了内存后,它就有了一个状态,并且这 个状态可以通过它所属类的方法进行操作。按惯例,一个对象是一个特定数据类型的一个值。 2.3 选选选择择择器器器、、、动动动态态态连连连接接接与与与多多多态态态 Translator: cuikai1981@gmail.com 谁来传递消息?构造函数被new()调用来处理几乎没初始化的内存区。 void * new (const void * _class, ...) { const struct Class * class = _class; void * p = calloc(1, class -> size); assert(p); * (const struct Class **) p = class; if (class -> ctor) { va_list ap; va_start(ap, _class); p = class -> ctor(p, & ap); va_end(ap); } return p; } 把struct Class指针放在一个对象的开始是非常重要的,这也是为什么初始化这个已经在new()中的指 针。 18 CHAPTER 2. 动态连接——通用函数 上图右方的描述数据类型的类是在编译的时候初始化的,对象则是在运行时被创建,此时才将指针安 入到其中。在以下的赋值中 * (const struct Class **) p = class; p指向该对象的内存区的开始处。我们对p强制类型转换,使之作为struct Class的指针,并且把参 数class设置为这个指针的值。 随后,如果在这个类型描述里有构造函数,我们调用它并以它的返回值作为new()的返回值,即作 为new出的对象。??节会说明一个聪明的构造函数能够对自身的内存进行管理。 要注意的是,只有像new()那样明确可见的函数能有变参数列表。用文件stdarg.h里的宏va start()对va list类 型的变量ap初始化后,就可以通过变量ap来访问这个变参列表。new()只能把整个列表传递给构造函 数。因此,.ctor声明使用一个va list参数,而不是它自己的变参列表。由于我们可能在以后想要在几个 函数间共享这些原始参数,因此我把ap的地址传递给构造函数——当它返回后ap将指向变参列表中第一 个没有被构造函数使用的参数。 delete()假定每个对象,即每个非空指针,指向一个类型描述对象。这是为了在有析构函数时方便调 用它。在这里,参数self扮演着前面图中的指针p的角色。我们利用本地变量cp进行强制类型转换,并且 非常小心的从self找到它的类型描述: void delete (void * self) { const struct Class ** cp = self; if (self && * cp && (* cp) -> dtor) self = (* cp) -> dtor(self); free(self); } 析构函数通过被delete()调用也获得一个机会来换它自身的指针并传给free()。如果在开始时构造函数 想要欺骗,析构函数因而也有机会去更正这个问题,见??节。如果一个对象不想被删除,它的析构函数 可以返回一个空指针。 所有其他存储在类型描述中的方法都以类似的方式被调用。每个方法都有一个参数self用来接受对 象,并且需要通过它的描述符调用方法。 int differ (const void * self, const void * b) { const struct Class * const * cp = self; assert(self && * cp && (* cp) -> differ); return (* cp) -> differ(self, b); } 同样核心部分当然是假定我们能直接通过指针self找到一个类型描述指针*self。至少现在我们会提防 空指针。我们可以是指定一个“幻数”(magic number)于每个类型描述的开始,或者甚至用*self与所有 已知的类型描述的地址或地址范围进行比较。第8章,我们将做更多的严肃的检测。 无论如何,differ()的示例解释为什么这个调用函数的技术被称为动态联接或晚绑定(late binding):我 们能为任意对象调用differ()只要他们有一个适当的类型描述指针作为开始,真正工作的函数是尽可能晚 得被决定——只有在真正调用时。 我们称differ()为一个选择函数。这是一个多态函数的例子,即一个函数能接受不同类型的参数,并 且根据他们类型表现出不同的行为。一旦我们实现类型描述符中含有.differ的更多的类,differ()便成为 一个能适用于任何对象的通用函数。 我们可以认为选择函数是虽然自身不是动态链接的但仍表现出类似多态函数的方法,这是因为他们让 动态链接能作真正的工作。 2.4. 一个应用 19 多态函数通常编译在很多程序语言之中,例如在Pascal中write()函数处理不同的参数类型,C中 的+操作符能使用在整型、指针或浮点指针上。这叫做重载:参数类型和操作符名共同决定会执行什么 样的操作;同样的操作符名能同不同参数类型产生不同的操作效果。 这里没有完全清晰的差别:由于动态链接,至少对于内置的数据类型,differ()的行为如同一个重载 函数,并且C编译器能产生如同+产生的那样的多态函数。但是C编译器能根据不同的+操作符返回创建 的不同的返回类型,但函数differ()必须总是有独立于传入参数的返回类型。 方法可以在没有进行动态链接的情况下成为多态的。看如下例子,构造一个函数sizeOf()返回任何一 个对象的大小: size_t sizeOf (const void * self) { const struct Class * const * cp = self; assert(self && * cp); return (* cp) -> size; } 所有的对象都有他们的描述符,并且我们可以从描述符中获得size的大小,注意他们的区别: void * s = new(String, "text"); assert(sizeof s != sizeOf(s)); sizeof 是一个C的操作符,它被用来在编译时计算并返回他的参数的字节个数。sizeOf()是我们多态函 数,它在运行时返回参数所指对象的字节个数。 2.4 一一一个个个应应应用用用 Translator: ruanbeihong at gmail.com 尽管还没有实现字符串,我们仍然能写个简单的测试程序: extern const void * String; 我们所有的方法对所有的对象都通用;因此,我们把它们的声明放到1.4节介绍的内存管理头文 件new.h中: void * clone (const void * self); int differ (const void * self, const void * b); size_t sizeOf (const void * self); 前两个原型声明选择函数,它们从struct Class中对应的元素中通过简单地从声明函数中去掉一次间 接而派生出。以下是应用: #include "String.h" #include "new.h" int main () { void * a = new(String, "a"), * aa = clone(a); void * b = new(String, "b"); printf("sizeOf(a) == %u\n", sizeOf(a)); if (differ(a, b)) puts("ok"); if (differ(a, aa)) puts("differ?"); 20 CHAPTER 2. 动态连接——通用函数 if (a == aa) puts("clone?"); delete(a), delete(aa), delete(b); return 0; } 我们建了两个字符串并复制了其中一个,我们显示了String对象的大小——不是被对象控制的文 本2的大小——并且我们确认了两个不同的文本是两个不同的字符串。最后,我们确认了复制的字符串于 原来的相等却不相同,然后我们又把它们删掉了。如果一切正常,该程序会打印出: sizeOf(a) == 8 ok 2.5 一一一个个个实实实现现现——————String Translator: 05281253@bjtu.edu.cn 我们用编写需要被加入到String这个类型描述中去的方法来实现字符串。动态连接有助于明确指定需 要编写哪些函数来实现一个新的数据类型。 构造函数的文本从传向new()的文本中得到,并保存一份动态拷贝于由new() 分配的String结构体中。 struct String { const void * class; /* must be first */ char * text; }; static void * String_ctor (void * _self, va_list * app) { struct String * self = _self; const char * text = va_arg(* app, const char *); self -> text = malloc(strlen(text) + 1); assert(self -> text); strcpy(self -> text, text); return self; } 在构造函数中我们只需要初始化.text,因为.class已经由new()设置。 析构函数释放由字符串控制的动态内存。由于只有在self非空的情况下delete() 才会调用析构函数, 因此在这里我们不需要检查其他事情: static void * String_dtor (void * _self) { struct String * self = _self; free(self -> text), self -> text = 0; return self; } String clone()复制一份字符串。由于之后初始的和复制的字符串都将被传送到delete(),所以我们必 须产生一份新的字符串的动态拷贝。这只要简单的调用new() 即可。 2译者注:此处的文本指的是C字符串,而字符串指的是作者正在实现的对象 2.5. 一个实现——STRING 21 static int String_differ (const void * _self, const void * _b) { const struct String * self = _self; const struct String * b = _b; if (self == b) return 0; if (! b || b -> class != String) return 1; return strcmp(self -> text, b -> text); } 类型描述符是唯一的--在这里我们用这个事实来验证第二个参数是否真的是一个字符串。 这些方法都被设置成静态是因为他们需要被new(),delete()或者其他的选择函数调用。这些方法通过 类型描述符使得他们可以被选择函数调用。 #include "new.r" static const struct Class _String = { sizeof(struct String), String_ctor, String_dtor, String_clone, String_differ }; const void * String = & _String; String.c包含了String.h和new.h中的公共声明。为了能够合适的初始化类型描述符,它也包含了私有 头文件new.r,new.r中包括了在2.2节中说明的struct Class的定义。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 16 金币 [ 分享文档获得金币 ] 7 人已下载

下载文档

相关文档