OOC:InheritanceCode Reuse and Refinement

来自 ChinaUnix Wiki

=继承 代码重用和改进=

目录

A Superclass — Point

父类——点类

In this chapter we will start a rudimentary drawing program. Here is a quick test for one of the classes we would like to have:

本章我们要实现一个基本的绘图程序。这里就有一个简单的测试,用来测试我们可能会用到的类:

#include "Point.h"
#include "new.h"
int main (int argc, char ** argv)
{   void * p;
    while (* ++ argv)
    {   switch (** argv) {
        case ’p’:
            p = new(Point, 1, 2);
            break;
        default:
            continue;
        }
        draw(p);
        move(p, 10, 20);
        draw(p);
        delete(p);
    }
    return 0;
}

For each command argument starting with the letter p we get a new point which is drawn, moved somewhere, drawn again, and deleted. ANSI-C does not include standard functions for graphics output; however, if we insist on producing a picture we can emit text which Kernighan’s pic [Ker82] can understand:

对每一个以字母P开头的命令参数,我们进行如下操作:画一个新的点,移动到某地,再画一个点,然后删除掉该点。ANSI-C没有标准的图像输出函数;但是如果我们一定要输出一个图像,我们可以输出一些Kernighan's Pic[Ker82]能够读懂的文字:

$ points p
"." at 1,2
"." at 11,22

The coordinates do not matter for the test — paraphrasing a commercial and OOspeak: ‘‘the point is the message.’’

坐标对于该测试没有多大的关系——只是表示“文字信息中包含坐标信息”

What can we do with a point? new() will produce a point and the constructor expects initial coordinates as further arguments to new(). As usual, delete() will recycle our point and by convention we will allow for a destructor.

我们能对坐标点做哪些操作?new()会新建一个坐标点,函数中的构造器将new()中的参数视为初始化的坐标位置。与以前一样delete()也是使用析构器释放坐标点的空间。

draw() arranges for the point to be displayed. Since we expect to work with other graphical objects — hence the switch in the test program — we will provide dynamic linkage for draw().

draw()函数把要显示的点排列出来。因为我们可能希望处理多种图像元素——所以switch语句出现在测试程序中——我们将会为draw()函数使用动态链接技术。

move() changes the coordinates of a point by the amounts given as arguments. If we implement each graphical object relative to its own reference point, we will be able to move it simply by applying move() to this point. Therefore, we should be able to do without dynamic linkage for move().

move()根据所给参数改变点的坐标位置。如果我们对每一个图像元素都关联到其自身的引用坐标点上,我们就能简单的通过对坐标点调用move()函数来移动它们。所以,对于move()函数我们不需要使用动态链接。

Superclass Implementation — Point

父类的实现——Point类

The abstract data type interface in Point.h contains the following:

头文件Point.h中包含下面的抽象数据类型接口:

extern const void * Point;          /* new(Point, x, y); */
void move (void * point, int dx, int dy);

We can recycle the new.? files from chapter 2 except that we remove most methods and add draw() to new.h:

我们可以重新利用第二章的new.?,不过要移除掉其中大部分的方法,并向new.h中增加draw()方法的声明。

void * new (const void * class, ...);
void delete (void * item);
void draw (const void * self);

The type description struct Class in new.r should correspond to the method declarations in new.h:

而new.r中的类型描述结构体struct Class中应对应new.h中声明的方法:

struct Class {
    size_t size;
    void * (* ctor) (void * self, va_list * app);
    void * (* dtor) (void * self);
    void (* draw) (const void * self);
};

The selector draw() is implemented in new.c. It replaces selectors such as differ() introduced in section 2.3 and is coded in the same style:

选择子draw()要在new.c中实现。它代替了2.3节中介绍的像differ()这样的选择子,编码风格也和它们一样:

void draw (const void * self)
{   const struct Class * const * cp = self;
    assert(self && * cp && (* cp) —> draw);
    (* cp) —> draw(self);
}

After these preliminaries we can turn to the real work of writing Point.c, the implementation of points. Once again, object-orientation has helped us to identify precisely what we need to do: we have to decide on a representation and implement a constructor, a destructor, the dynamically linked method draw() and the statically linked method move(), which is just a plain function. If we stick with two-dimensional, Cartesian coordinates, we choose the obvious representation:

有了上面的准备我们就可以着手完成一些实际的工作了——完成Point.c,也就是实现坐标点。再一次,面向对象的方法帮助我们准确的识别哪些工作是需要完成的:我们要选择如何实现构造器,析构器,动态链接方法draw()和静态链接方法move()。如果我们坚持使用二位笛卡尔坐标系,就可以选用下面简单的表示方法来表示点类的结构:

struct Point {
    const void * class;
    int x, y;           /* coordinates */
};

The constructor has to initialize the coordinates .x and .y — by now absolutely routine:

构造器必须要负责初始化.x和.y——通过下面的构造函数完整地实现:

static void * Point_ctor (void * _self, va_list * app)
{   struct Point * self = _self;
    self —> x = va_arg(* app, int);
    self —> y = va_arg(* app, int);
    return self;
}

It turns out that we do not need a destructor because we have no resources to reclaim before delete() does away with struct Point itself. In Point_draw() we print the current coordinates in a way which pic can understand:

事实上我们并不需要一个析构函数,因为在delete()函数删除struct Point之前,没有什么资源需要释放。在 Point_draw()函数中我们将点的坐标以pic程序可读的形式打印出来:

static void Point_draw (const void * _self)
{   const struct Point * self = _self;
    printf("\".\" at %d,%d\n", self —> x, self —> y);
}

This takes care of all the dynamically linked methods and we can define the type descriptor, where a null pointer represents the non-existing destructor:

这样做能够很好的处理所有的动态链接的方法,然后我们可以定义类型描述符,其中的空指针表示析构函数不存在:

static const struct Class _Point = {
    sizeof(struct Point), Point_ctor, 0, Point_draw
};
const void * Point = & _Point;

move() is not dynamically linked, so we omit static to export it from Point.c and we do not prefix its name with the class name Point:

move()函数不是动态链接的,所以我们将其从Point.c中导出,并且忽略其static属性。也是由于上述原因,我们也不在其名字前加上前缀Point了。

void move (void * _self, int dx, int dy)
{   struct Point * self = _self;
    self —> x += dx, self —> y += dy;
}

This concludes the implementation of points in Point.? together with the support for dynamic linkage in new.?.

上面总结了坐标点结构在point.?中的实现,并且介绍了如何在new.?中实现动态链接。

Inheritance — Circle

继承——Circle(圆)

A circle is just a big point: in addition to the center coordinates it needs a radius. Drawing happens a bit differently, but moving only requires that we change the coordinates of the center.

圆本身就是一个很大的点:除了一个中心点坐标,其还需要一个半径来表示。画圆和画点有点不同,但是移动却是相同的,因为圆的移动就是中心点的移动。

This is where we would normally crank up our text editor and perform source code reuse. We make a copy of the implementation of points and change those parts where a circle is different from a point. struct Circle gets an additional com- ponent:

这里我们就可以准备好我们的编辑器——复制,粘帖——代码重用。我们把坐标点的实现复制下来,然后改变那些圆不同于点的地方就可以了。struct Circle多了一个组成:

int rad;

This component is initialized in the constructor

该成员一样在构造函数中被初始化:

self —> rad = va_arg(* app, int);

and used in Circle_draw():

并且在成员函数Circle_draw()中使用:

printf("circle at %d,%d rad %d\n",
    self —> x, self —> y, self —> rad);

We get a bit stuck in move(). The necessary actions are identical for a point and a circle: we need to add the displacement arguments to the coordinate com- ponents. However, in one case, move() works on a struct Point, and in the other case, it works on a struct Circle. If move() were dynamically linked, we could pro- vide two different functions to do the same thing, but there is a much better way.

然而在move()中我们却遇到了一些问题。对于点和圆来说,移动的动作是完全一样的:我们需要向坐标点施以需要移动的参数。然而,我们要求move()既可以作用于struct Point,也可以作用于struct Circle。所以如果我们用动态链接的方法实现move()函数,我们就会有两个不同的函数来实现完全相同的功能,所以这里介绍一个更好的方法。

Consider the layout of the representations of points and circles: (一张图片)

考虑计算机中点(point)和圆(circle)表示的结构: 图...

The picture shows that every circle begins with a point. If we derive struct Circle by adding to the end of struct Point, we can pass a circle to move() because the initial part of its representation looks just like the point which move() expects to receive and which is the only thing that move() can change. Here is a sound way to make sure the initial part of a circle always looks like a point:

上图显示,所有的圆都是以一个点开始的。如果我们向struct Point的末端添加一个成员,并以此来形成struct Circle的话,我们就能对圆也调用move()函数了,因为圆的初始部分跟点的结构完全相同,而move()函数刚好希望能以点为参数,并且可以改变点结构中的数据。下面的方法能够完美的确保圆的初始结构和点完全相同:

struct Circle { const struct Point _; int rad; };

We let the derived structure start with a copy of the base structure that we are extending. Information hiding demands that we should never reach into the base structure directly; therefore, we use an almost invisible underscore as its name and we declare it to be const to ward off careless assignments.

我们让构造出来的结构体开始于一个我们要扩展的基础结构体。信息隐藏原则要求我们永远不能直接访问基础机构体;所以我们使用一个几乎看不到的下划线来表示它的名字,并且声明其为const类型以防止不小心导致对其赋值。

This is all there is to simple inheritance: a subclass is derived from a superclass (or base class) merely by lengthening the structure that represents an object of the superclass.

这就是一个简单继承的全部:子类由父类(或基类)继承而来,方法就是丰富表示父类的数据结构。

Since representation of the subclass object (a circle) starts out like the representation of a superclass object (a point), the circle can always pretend to be a point — at the initial address of the circle’s representation there really is a point’s representation.

因为子类(circle类)的表示开始于一个非常像其父类(point类)的结构,所以圆(circle)可以把自己伪装成一个点(point)——圆的物理表示的地址的前面部分就是一个点的表示。

It is perfectly sound to pass a circle to move(): the subclass inherits the methods of the superclass because these methods only operate on that part of the subclass’ representation that is identical to the superclass’ representation for which the methods were originally written. Passing a circle as a point means converting from a struct Circle * to a struct Point *. We will refer to this as an up-cast from a subclass to a superclass — in ANSI-C it can only be accomplished with an explicit conversion operator or through intermediate void * values.

对圆使用move()函数是完美的:子类继承了其父类的方法,因为这些方法只作用于子类的某一部分,该部分与其父类的表示完全相同,而函数本来就是为其父类所写,所以其可以完美的作用于子类上。把圆作为点传给函数就意味着把struct Circle *强制转化为struct Point *。我们把这个转化成做由子类到父类的向上类型转化——在ANSI-C中只有通过外部的强制类型转换运算符或者通过一个中间的void *值来实现。

It is usually unsound, however, to pass a point to a function intended for circles such as Circle_draw(): converting from a struct Point * to a struct Circle * is only permissible if the point originally was a circle. We will refer to this as a down-cast from a superclass to a subclass — this requires explicit conversions or void * values, too, and it can only be done to pointers to objects that were in the subclass to begin with.

然而把一个点假装为圆传给函数,比如 Circle_draw()却不能那么完美:只有一个点本来就是一个由圆强制转化来的点的时候才能把struct Point *强制转化为struct Circle *。我们称这个过程为由父类到子类的向下类型转化——这也需要外部的强制类型转化或者void *来实现,而且这个过程只能把一个指针转化为一个以此指针的结构开始的子类的对象。

Linkage and Inheritance

链接和继承

move() is not dynamically linked and does not use a dynamically linked method to do its work. While we can pass points as well as circles to move(), it is not really a polymorphic function: move() does not act differently for different kinds of objects, it always adds arguments to coordinates, regardless of what else might be attached to the coordinates.

函数move()不是动态链接的,也不采用动态链接的思想来实现其功能。虽然我们可以把点和圆两种数据结构都传给move(),但是它却不是多态函数:move()函数对于不同的对象操作却是相同的,仅仅是向坐标增加参数,不论可能会增加什么参数。

The situation is different for a dynamically linked method like draw(). Let us look at the previous picture again, this time with the type descriptions shown expli- citly:

这与采用动态链接的方法,比如draw()不同。让我们再回头看看刚才那个图片,这次把类型描述结构也画出来:

(图片) When we up-cast from a circle to a point, we do not change the state of the circle, i.e., even though we look at the circle’s struct Circle representation as if it were a struct Point, we do not change its contents. Consequently, the circle viewed as a point still has Circle as a type description because the pointer in its .class com- ponent has not changed. draw() is a selector function, i.e., it will take whatever argument is passed as self, proceed to the type description indicated by .class, and call the draw method stored there.

当我们从圆到点进行向上类型转换的时候,我们不会改变圆的状态,也就是说,虽然我们把一个圆的struct Circle结构看作是一个struct Point,我们却不改变其中的内容。这个被看作是点的圆依旧是Circle类型的,因为其类型描述结构中的.class指针成员没有变化。draw()是选择子函数,也就是说,它能接受任何以self参数传来的值,找到由.class指向的类型描述符,并且调用存储在那里的方法。

A subclass inherits the statically linked methods of its superclass — those methods operate on the part of the subclass object which is already present in the superclass object. A subclass can choose to supply its own methods in place of the dynamically linked methods of its superclass. If inherited, i.e., if not overwrit- ten, the superclass’ dynamically linked methods will function just like statically linked methods and modify the superclass part of a subclass object. If overwritten, the subclass’ own version of a dynamically linked method has access to the full representation of a subclass object, i.e., for a circle draw() will invoke Circle_draw() which can consider the radius when drawing the circle.

子类继承了父类的静态链接的方法——这些方法作用于子类对象的某一部分,而该部分一定已经存在于其父类的对象中。子类可以选择使用自己的方法而不是其父类的动态链接的方法。也就是说,如果继承下来而没有被重写,父类的动态链接方法就像静态链接方法一样,可以用来改变子类的来自父类的那部分结构。如果被重写的话,子类的动态链接方法就可以操作整个子类的所有数据。一句话,对于圆来说,draw()函数会调用Circle_draw(),而该函数会在画圆的时候考虑半径。

Static and Dynamic Linkage

静态和动态链接

A subclass inherits the statically linked methods of its superclass and it can choose to inherit or overwrite the dynamically linked methods. Consider the declarations for move() and draw():

子类能够继承其父类的静态链接方法,也可以选择继承或者重写其父函数的动态链接方法。请看下面的move()和draw()的声明:

void move (void * point, int dx, int dy);
void draw (const void * self);

We cannot discover the linkage from the two declarations, although the implemen- tation of move() does its work directly, while draw() is only the selector function which traces the dynamic linkage at runtime. The only difference is that we declare a statically linked method like move() as part of the abstract data type interface in Point.h, and we declare a dynamically linked method like draw() with the memory management interface in new.h, because we have thus far decided to implement the selector function in new.c.

仅仅从声明中看不出这些函数和它们的链接方法有什么联系,然而move()的实现直接完成其工作,draw()函数却仅仅是一个选择子,在运行时选择合适的动态链接函数。唯一的区别只是,我们把像move()这样的静态链接函数作为抽象数据类型接口声明到头文件Point.h中,而把像draw()这样的动态链接函数和内存管理部分声明在new.h中,因为我们早就决定在new.c中实现选择子函数。

Static linkage is more efficient because the C compiler can code a subroutine call with a direct address, but a function like move() cannot be overwritten for a subclass. Dynamic linkage is more flexible at the expense of an indirect call — we have decided on the overhead of calling a selector function like draw(), checking the arguments, and locating and calling the appropriate method. We could forgo the checking and reduce the overhead with a macro* like

静态链接带来更高的效率,因为C编译器能够将一个子过程调编码成一个可直接调用的地址,但是像move()这样的静态链接函数不能被子类重写覆盖。采用动态链接能带来更大的灵活性,但是代价是使用间接调用带来的性能损失——调用一个像draw()这样的选择子函数,我们必须检查其参数,定位并且调用合适的方法。我们可以放弃检查以把其简化成一个宏:

#define draw(self) \
    ((* (struct Class **) self) —> draw (self))

but macros cause problems if their arguments have side effects and there is no clean technique for manipulating variable argument lists with macros. Additionally, the macro needs the declaration of struct Class which we have thus far made avail- able only to class implementations and not to the entire application.

但是使用宏也有缺点:比如参数可能有side affects(应该怎么翻译)而且宏没有一种清楚的策略来处理可变的参数列表。更进一步的说,该宏需要struct Class的声明,而该声明仅仅对于该类的实现是可见的,对于整个程序并不是可见的。

Unfortunately, we pretty much decide things when we design the superclass. While the function calls to the methods do not change, it takes a lot of text editing, possibly in a lot of classes, to switch a function definition from static to dynamic linkage and vice versa. Beginning in chapter 7 we will use a simple preprocessor to simplify coding, but even then linkage switching is error-prone.

不幸的是,在设计基类的时候我们就需要决定太多了。要保证调用类方法的函数不变,实现把函数定义由静态链接转化为动态链接需要很多的编码工作,甚至带来很多的类,反之也一样。从第7章开始,我们会介绍一个简单的预处理程序用来简化编码,然而即便如此,链接的改变一样是易错的。

In case of doubt it is probably better to decide on dynamic rather than static linkage even if it is less efficient. Generic functions can provide a useful concep- tional abstraction and they tend to reduce the number of function names which we need to remember in the course of a project. If, after implementing all required classes, we discover that a dynamically linked method was never overwritten, it is a lot less trouble to replace its selector by its single implementation, and even waste its slot in struct Class, than to extend the type description and correct all the initiali- zations.

有的时候,动态链接还是优于静态链接的,虽然效率低于后者。泛型函数提供了很好的概念抽象并且有利于减少我们在工程进行过程中需要记忆的函数名。如果完成工程中所有的类之后,我们发现有一个动态链接的方法从没有被重写,我们可以将该方法的所有选择子改为同一个实现,虽然这样做浪费了struct Class中的空间,也比扩展类型描述结构,修正所有的初始化要省事些。

Visibility and Access Functions

可视化和访问函数

We can now attempt to implement Circle_draw(). Information hiding dictates that we use three files for each class based on a ‘‘need to know’’ principle. Circle.h contains the abstract data type interface; for a subclass it includes the interface file of the superclass to make declarations for the inherited methods available:

现在我们可以尝试来实现Circle_draw()了。信息隐藏要求我们对于每一个类要使用三个文件来实现——这基于一个“须知”的原则。Circle.h包括抽象类型接口;作为子类,它必须包括其父类的接口以保证继承来的方法是可用的:

#include "Point.h"
extern const void * Circle; /* new(Circle, x, y, rad) */

The interface file Circle.h is included by the application code and for the implemen- tation of the class; it is protected from multiple inclusion.

上面的Circle.h文件被所有的程序代码所包含用于类的实现;当然它也被防止多次包含的宏保护起来的。

The representation of a circle is declared in a second header file, Circle.r. For a subclass it includes the representation file of the superclass so that we can derive the representation of the subclass by extending the superclass:

而圆的具体表示是在第二头文件:Circle.r中声明的。一个子类中一定包含其父类的表示文件,所以我们可以通过扩展父类来得到子类的表示:

#include "Point.r"
struct Circle { const struct Point _; int rad; };

The subclass needs the superclass representation to implement inheritance: struct Circle contains a const struct Point. The point is certainly not constant — move() will change its coordinates — but the const qualifier guards against accidentally overwriting the components. The representation file Circle.r is only included for the implementation of the class; it is protected from multiple inclusion.

子类需要父类的具体表示方法来实现继承:struct Circle中包含了const struct Point。点当然不是常数,不是const的——move()可以改变其坐标位置——const是用来防止不小心重写覆盖了该成员。表示文件Circle.r只有在实现该类的文件中才被包含;而且被保护起来以防止多次被包含。

Finally, the implementation of a circle is defined in the source file Circle.c which includes the interface and representation files for the class and for object manage- ment:

最后,圆类的实现在源文件Circle.c中,该文件包含了声明接口的头文件,用于表示该类和管理该类的表示文件:

#include  "Circle.h"
#include  "Circle.r"
#include  "new.h"
#include  "new.r"
static void Circle_draw (const void * _self)
{   const struct Circle * self = _self;
    printf("circle at %d,%d rad %d\n",
         self —> _.x, self —> _.y, self —> rad);
}

In Circle_draw() we have read point components for the circle by invading the sub- class part with the ‘‘invisible name’’ _. From an information hiding perspective this is not such a good idea. While reading coordinate values should not create major problems we can never be sure that in other situations a subclass implementation is not going to cheat and modify its superclass part directly, thus potentially playing havoc with its invariants.

在Circle_draw()中我们读取圆中对应的点类的成员是通过用一个“隐形名字”_来调用子类的某个成员。从信息隐藏的角度来看这不是一个好主意。读取坐标值的同时不应该带来太多的问题——比如我们可能无法确定在某些情况下,子类会不会故意欺骗父类从而实现直接对其父类部分的修改,thus potentially playing havoc with its invariants.

Efficiency dictates that a subclass reach into its superclass components directly. Information hiding and maintainability require that a superclass hide its own representation as best as possible from its subclasses. If we opt for the latter, we should provide access functions for all those components of a superclass which a subclass is allowed to look at, and modification functions for those components, if any, which the subclass may modify.

追求高效率要求我们子类能直接接触到其继承自父类的部分。信息隐藏和可操作性又要求我们父类应该向其子类尽可能的隐藏自己的实现。如果我们倾向于后者,我们应该为父类中所有子类可以查看的成员提供访问函数,而如果允许子类对某些成员进行修改,也应该在父类中为这些成员提供modify函数。

Access and modification functions are statically linked methods. If we declare them in the representation file for the superclass, which is only included in the implementations of subclasses, we can use macros, because side effects are no problem if a macro uses each argument only once. As an example, in Point.r we define the following access macros:*

Acess和modify函数都是静态链接的方法函数。如果我们在父类的表示文件中声明它们,而这些表示文件只有在子类的实现文件中才会被包含,我们就可以使用宏,因为如果宏中每一个参数只是用一次那么side effects就不是什么问题。比如,在Point.r中我们定义如下的访问宏:

#define x(p) (((const struct Point *)(p)) —> x)
#define y(p) (((const struct Point *)(p)) —> y)

These macros can be applied to a pointer to any object that starts with a struct Point, i.e., to objects from any subclass of our points. The technique is to up-cast the pointer into our superclass and reference the interesting component there. const in the cast blocks assignments to the result. If const were omitted

这些宏可以作用于任何一个指向以struct Point开始的对象的指针,也就是说,任何指向以我们的点类为开始的对象的指针。做法就是将该指针向上类型转化为父类,然后引用我们感兴趣的部分即可。const防止对结果赋值,如果没有const

#define x(p) (((struct Point *)(p)) —> x)

a macro call x(p) produces an l-value which can be the target of an assignment. A better modification function would be the macro definition

一次x(p)宏调用就可能产生一个可以被赋值的左值。下面有一个更高明的,用宏写成的修改函数:

#define set_x(p,v) (((struct Point *)(p)) —> x = (v))

which produces an assignment.

Outside the implementation of a subclass we can only use statically linked methods for access and modification functions. We cannot resort to macros because the internal representation of the superclass is not available for the macros to reference. Information hiding is accomplished by not providing the representa- tion file Point.r for inclusion into an application.

在用来实现子类的文件之外,对于访问和修改函数我们就只能采用静态链接的方法。我们无法用宏来实现,因为父类的内部具体实现对于我们要引用的宏是不可见的。信息隐藏的实现来自于我们不将表示文件Point.r包含到别的程序中去。

The macro definitions demonstrate, however, that as soon as the representa- tion of a class is available, information hiding can be quite easily defeated. Here is a way to conceal struct Point much better. Inside the superclass implementation we use the normal definition:

然而,上面的宏定义说明一旦一个类的表示存在,信息隐藏就很容易被保证了。下面这种方法就能更好的隐藏struct Point。在父类的实现中我们使用一般的定义:

struct Point {
    const void * class;
    int x, y;           /* coordinates */
};

For subclass implementations we provide the following opaque version:

而对于子类的实现,我们就提供下面的“不透明”版本:

struct Point {
   const char _ [ sizeof( struct {
     const void * class;
     int x, y;               /* coordinates */
   })];
};

This structure has the same size as before, but we can neither read nor write the components because they are hidden in an anonymous interior structure. The catch is that both declarations must contain identical component declarations and this is difficult to maintain without a preprocessor.

该结构和上面的结构大小完全相同,但是我们既不能读也不能改写其中的成员,因为他们都被藏在一个匿名的内部结构中了。巧妙的地方在于上面的两种声明都必须包含完全相同的成员声明,而这在没有专门的预处理的情况下很难处理。

Subclass Implementation — Circle

子类实现——Circle

We are ready to write the complete implementation of circles, where we can choose whatever techniques of the previous sections we like best. Object- orientation prescribes that we need a constructor, possibly a destructor, Circle_draw(), and a type description Circle to tie it all together. In order to exer- cise our methods, we include Circle.h and add the following lines to the switch in the test program in section 4.1:

现在我们要具体实现完整的圆的实现,我们可以选择上面介绍的任意一个喜欢的技术。面向对象要求有一个构造器,最好还有一个析构器,Circle_draw()以及一个类型描述Circle来把上面的几个粘合在一起。为了测试我们的方法,我们包含Circle.h然后在测试程序的switch语句中增加下面几行:

case ’c’:
    p = new(Circle, 1, 2, 3);
    break;

Now we can observe the following behavior of the test program:

现在我们就可以观察到测试程序打印出下面的结果:

$ circles p c
"." at 1,2
"." at 11,22
circle at 1,2 rad 3
circle at 11,22 rad 3

The circle constructor receives three arguments: first the coordinates of the circle’s point and then the radius. Initializing the point part is the job of the point constructor. It consumes part of the argument list of new(). The circle constructor is left with the remaining argument list from which it initializes the radius.

圆的构造函数接受三个参数:先是圆心的坐标,然后是半径。初始化圆心点的坐标是点类的构造函数的工作,它使用new()函数参数列表的一部分。圆类的构造函数则根据剩下未使用的参数列表来初始化圆的半径。

A subclass constructor should first let the superclass constructor do that part of the initialization which turns plain memory into the superclass object. Once the superclass constructor is done, the subclass constructor completes initialization and turns the superclass object into a subclass object.

子类的构造函数应该首先让父类的构造函数来完成把空白内存开辟为父类对象的那部分初始化工作。一旦父类构造函数完成上述工作,子类的构造函数就进行完整的初始化,并将该父类对象转化为子类对象。

For circles this means that we need to call Point_ctor(). Like all dynamically linked methods, this function is declared static and thus hidden inside Point.c. However, we can still get to the function by means of the type descriptor Point which is available in Circle.c:

这就意味着,对于圆类,我们需要调用其父类构造函数Point_ctor()。正如所有的动态链接方法,该函数被声明为static,被隐藏在Point.c中。然而,我们还是能够通过在Circle.c中可见的类型描述数据机构Point来调用该函数。

static void * Circle_ctor (void * _self, va_list * app)
{   struct Circle * self =
        ((const struct Class *) Point) —> ctor(_self, app);
    self —> rad = va_arg(* app, int);
    return self;
}

It should now be clear why we pass the address app of the argument list pointer to each constructor and not the va_list value itself: new() calls the subclass construc- tor, which calls its superclass constructor, and so on. The supermost constructor is the first one to actually do something, and it gets first pick at the left end of the argument list passed to new(). The remaining arguments are available to the next subclass and so on until the last, rightmost arguments are consumed by the final subclass, i.e., by the constructor directly called by new().

现在就应该可以搞清楚,为什么我们是把参数列表的指针app传给每一个构造函数,而不传va_list本身了:因为new()调用子类的构造函数,而子类又调用父类的构造函数,依次继续。而最上端的构造函数是第一个真正来完成构造工作的函数,它会使用传给new()的参数列表的最左端参数。剩下的参数就传送给下一个子类,然后一直继续下去直到最右端的参数被最后一个子类使用掉,也就是被new()直接调用的那个构造函数使用掉。

Destruction is best arranged in the exact opposite order: delete() calls the sub- class destructor. It should destroy its own resources and then call its direct super- class destructor which can destroy the next set of resources and so on. Construc- tion happens superclass before subclass, destruction happens in reverse, subclass before superclass, circle part before point part. Here, however, nothing needs to be done.

析构函数最好按照和构造函数相反地次序来安排:delete()首先调用子类的析构函数,删除它自己的资源后调用其父类的析构函数,其完成删除下一部分资源的任务,然后继续下去。构造先发生在父类然后才发生在子类,而析构则正好相反,先子类然后父类——先圆类然后点类。但是,本例中,析构函数什么都不用完成。

We have worked on Circle_draw() before. We use visible components and code the representation file Point.r as follows:

我们已经写过一部分Circle_draw()函数。我们使用可见的成员,并且按照下面的方法编码表示文件Point.r:

struct Point {
    const void * class;
    int x, y;               /* coordinates */
};
#define x(p)    (((const struct Point *)(p)) —> x)
#define y(p)    (((const struct Point *)(p)) —> y)

Now we can use the access macros for Circle_draw():

这样我们就可以在Circle_draw()函数中使用访问宏了:

static void Circle_draw (const void * _self)
{   const struct Circle * self = _self;
    printf("circle at %d,%d rad %d\n",
        x(self), y(self), self —> rad);
}

move() has static linkage and is inherited from the implementation of points. We conclude the implementation of circles by defining the type description which is the only globally visible part of Circle.c:

move()函数是静态链接的,继承自点类的实现。我们通过定义类型描述符来完成圆类的实现。而该类型描述数据结构是Circle.c中唯一全局可见的部分:

static const struct Class _Circle = {
    sizeof(struct Circle), Circle_ctor, 0, Circle_draw
};
const void * Circle = & _Circle;

While it looks like we have a viable strategy of distributing the program text implementing a class among the interface, representation, and implementation file, the example of points and circles has not exhibited one problem: if a dynamically linked method such as Point_draw() is not overwritten in the subclass, the sub- class type descriptor needs to point to the function implemented in the superclass. The function name, however, is defined static there, so that the selector cannot be circumvented. We shall see a clean solution to this problem in chapter 6. As a stopgap measure, we would avoid the use of static in this case, declare the func- tion header only in the subclass implementation file, and use the function name to initialize the type description for the subclass.

看起来,我们在如何分配程序代码到接口文件,表示文件和实现文件中的策略不是固定的,然而点和圆的这个例子却还没有展示一个可能出现的问题:如果一个动态链接的方法,比如Point_draw()没有在子类中被重写,那么子类的类型描述符结构中该成员就要指向其父类中实现的函数了。然而,函数名我们声明为static的,所以选择子就不能被circumvented。在第六章中我们能看到一个清楚的解决办法。作为一个stopgap方法,本例中我们可以不使用static,把函数头文件声明在子类的实现文件中,并且用该函数名来初始化子类的类型描述结构。

Summary

The objects of a superclass and a subclass are similar but not identical in behavior. Subclass objects normally have a more elaborate state and more methods — they are specialized versions of the superclass objects.

We start the representation of a subclass object with a copy of the representa- tion of a superclass object, i.e., a subclass object is represented by adding com- ponents to the end of a superclass object.

A subclass inherits the methods of a superclass: because the beginning of a subclass object looks just like a superclass object, we can up-cast and view a pointer to a subclass object as a pointer to a superclass object which we can pass to a superclass method. To avoid explicit conversions, we declare all method parameters with void * as generic pointers.

Inheritance can be viewed as a rudimentary form of polymorphism: a super- class method accepts objects of different types, namely objects of its own class and of all subclasses. However, because the objects all pose as superclass objects, the method only acts on the superclass part of each object, and it would, therefore, not act differently on objects from different classes.

Dynamically linked methods can be inherited from a superclass or overwritten in a subclass — this is determined for the subclass by whatever function pointers are entered into the type description. Therefore, if a dynamically linked method is called for an object, we always reach the method belonging to the object’s true class even if the pointer was up-casted to some superclass. If a dynamically linked method is inherited, it can only act on the superclass part of a subclass object, because it does not know of the existence of the subclass. If a method is overwrit- ten, the subclass version can access the entire object, and it can even call its corresponding superclass method through explicit use of the superclass type description.

In particular, constructors should call superclass constructors back to the ulti- mate ancestor so that each subclass constructor only deals with its own class’ extensions to its superclass representation. Each subclass destructor should remove the subclass’ resources and then call the superclass destructor and so on to the ultimate ancestor. Construction happens from the ancestor to the final sub- class, destruction takes place in the opposite order.

Our strategy has a glitch: in general we should not call dynamically linked methods from a constructor because the object may not be initialized completely. new() inserts the final type description into an object before the constructor is called. Therefore, if a constructor calls a dynamically linked method for an object, it will not necessarily reach the method in the same class as the constructor. The safe technique would be for the constructor to call the method by its internal name in the same class, i.e., for points to call Points_draw() rather then draw().

To encourage information hiding, we implement a class with three files. The interface file contains the abstract data type description, the representation file con- tains the structure of an object, and the implementation file contains the code of the methods and initializes the type description. An interface file includes the superclass interface file and is included for the implementation as well as any appli- cation. A representation file includes the superclass representation file and is only included for the implementation.

Components of a superclass should not be referenced directly in a subclass. Instead, we can either provide statically linked access and possibly modification methods for each component, or we can add suitable macros to the representation file of the superclass. Functional notation makes it much simpler to use a text edi- tor or a debugger to scan for possible information leakage or corruption of invari- ants.

Is It or Has It? — Inheritance vs. Aggregates

Our representation of a circle contains the representation of a point as the first component of struct Circle:

struct Circle { const struct Point _; int rad; };

However, we have voluntarily decided not to access this component directly. Instead, when we want to inherit we cast up from Circle back to Point and deal with the initial struct Point there.

There is a another way to represent a circle: it can contain a point as an aggre- gate. We can handle objects only through pointers; therefore, this representation of a circle would look about as follows:

struct Circle2 { struct Point * point; int rad; };

This circle does not look like a point anymore, i.e., it cannot inherit from Point and reuse its methods. It can, however, apply point methods to its point component; it just cannot apply point methods to itself.

If a language has explicit syntax for inheritance, the distinction becomes more apparent. Similar representations could look as follows in C++:

struct Circle : Point { int rad; }; // inheritance
struct Circle2 {
    struct Point point; int rad;
};

In C++ we do not necessarily have to access objects only as pointers.

Inheritance, i.e., making a subclass from a superclass, and aggregates, i.e., including an object as component of some other object, provide very similar func- tionality. Which approach to use in a particular design can often be decided by the is-it-or-has-it? test: if an object of a new class is just like an object of some other class, we should use inheritance to implement the new class; if an object of a new class has an object of some other class as part of its state, we should build an aggregate.

As far as our points are concerned, a circle is just a big point, which is why we used inheritance to make circles. A rectangle is an ambiguous example: we can describe it through a reference point and the side lengths, or we can use the end- points of a diagonal or even three corners. Only with a reference point is a rectan- gle some sort of fancy point; the other representations lead to aggregates. In our arithmetic expressions we could have used inheritance to get from a unary to a binary operator node, but that would substantially violate the test.

Multiple Inheritance

Because we are using plain ANSI-C, we cannot hide the fact that inheritance means including a structure at the beginning of another. Up-casting is the key to reusing a superclass method on objects of a subclass. Up-casting from a circle back to a point is done by casting the address of the beginning of the structure; the value of the address does not change.

If we include two or even more structures in some other structure, and if we are willing to do some address manipulations during up-casting, we could call the result multiple inheritance: an object can behave as if it belonged to several other classes. The advantage appears to be that we do not have to design inheritance relationships very carefully — we can quickly throw classes together and inherit whatever seems desirable. The drawback is, obviously, that there have to be address manipulations during up-casting before we can reuse methods of the superclasses.

Things can actually get quite confusing very quickly. Consider a text and a rec- tangle, each with an inherited reference point. We can throw them together into a button — the only question is if the button should inherit one or two reference points. C++ permits either approach with rather fancy footwork during construction and up-casting.

Our approach of doing everything in ANSI-C has a significant advantage: it does not obscure the fact that inheritance — multiple or otherwise — always happens by inclusion. Inclusion, however, can also be accomplished as an aggregate. It is not at all clear that multiple inheritance does more for the programmer than complicate the language definition and increase the implementation overhead. We will keep things simple and continue with simple inheritance only. Chapter 14 will show that one of the principal uses of multiple inheritance, library merging, can often be real- ized with aggregates and message forwarding.

Exercises

Graphics programming offers a lot of opportunities for inheritance: a point and a side length defines a square; a point and a pair of offsets defines a rectangle, a line segment, or an ellipse; a point and an array of offset pairs defines a polygon or even a spline. Before we proceed to all of these classes, we can make smarter points by adding a text, together with a relative position, or by introducing color or other view- ing attributes.

Giving move() dynamic linkage is difficult but perhaps interesting: locked objects could decide to keep their point of reference fixed and move only their text portion.

Inheritance can be found in many more areas: sets, bags, and other collections such as lists, stacks, queues, etc. are a family of related data types; strings, atoms, and variables with a name and a value are another family.

Superclasses can be used to package algorithms. If we assume the existence of dynamically linked methods to compare and swap elements of a collection of objects based on some positive index, we can implement a superclass containing a sorting algorithm. Subclasses need to implement comparison and swapping of their objects in some array, but they inherit the ability to be sorted.

个主工具