# 第四章 繼承——代碼重用和改進
> 來源:http://blog.csdn.net/besidemyself/article/details/6423491
> 譯者:[besidemyself](http://my.csdn.net/besidemyself)
## 4.1 一個超級類——點
我們將在這章以一個基本的畫圖程序作為開始。這里是是我們樂意擁有的其中一個類的快速測試如下:
```c
#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;
}
```
對于每一個命令參數以字符 `p` 開始,我們獲得一個新的繪圖的點,移動這個點到某處,從新繪制,并且刪除。標準化C語言不包含圖形化輸出標準的函數:然而,如果我們堅持產生一幅圖片,我們能夠發表文本,對于這個文本Kernighan 的圖片 [Ker82] 能夠理解:
```
$ points p
"." at 1,2
"." at 11,22
```
坐標對于測試是無關緊要的——從商業和面向對象的說法解釋:“點就是一則消息。”
我們用這個點能做些什么呢?`new()` 將產生一個點,并且構造器期望著初始化坐標作為進一步的參數傳進 `new()` 。通常,`delete()` 將回收我們的點并且按照慣例調用析構器。
`draw()` 安排點被顯示出來。由于我們希望與其他圖形對象協同工作——因此在測試程序中會有`switch`——對于`draw()` 我們將提供動態連接。
`move()` 通過傳遞一系列參數來改變點的坐標。如果我們實現每一個圖形對象,這些對象都與它涉及的點關聯,我們將能夠通過簡單的應用這個點的`move()` 方法來移動它。因此,對于`move()` 在不需要動態連接的情況下我們應該可以做。
## 4.2 超級類的實現——點
在`Point.h` 中,抽象數據類型包含如下:
```c
extern const void * Point; /* new(Point, x, y); */
void move (void * point, int dx, int dy);
```
我們能夠重復利用第二章的`new` 文件,盡管我們刪除了很多方法并且對`new.h` 文件增加了`draw()` 方法:
```c
void * new (const void * class, ...);
void delete (void * item);
void draw (const void * self);
```
在`new.r` 中類型描述 `struct Class` 應該與在`new.h` 中聲明的方法相關聯:
```c
struct Class {
size_t size;
void * (* ctor) (void * self, va_list * app);
void * (* dtor) (void * self);
void (* draw) (const void * self);
};
```
選擇器`draw()` 在`new.c` 中實現。它將代替如`differ()` 在2.3 節介紹的選擇器,并且以相同的風格編寫代碼:
```c
void draw (const void * self)
{
const struct Class * const * cp = self;
assert(self && * cp && (* cp) -> draw);
(* cp) -> draw(self);
}
```
這些預備工作完成后,我們將轉去做真正的工作去寫`Point.c`,對點的實現。在此,面向對象幫助我們精確的鑒別出我們需要做什么:我們必須對表示式做出決定并實現構造器,析構器,動態鏈接方法`draw()` 和靜態鏈接方法`move()` ,這些都是基本的函數。如果我們堅持二維,笛卡爾坐標,我們選擇如下明確的表示:
```c
struct Point {
const void * class;
int x, y; /* coordinates */
};
```
構造器必須初始化坐標 `.x` 和 `.y` ——現在一個絕對的例程如下:
```c
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;
}
```
現在的結果是我們并不需要析構器,因為在`delete()` 之前沒有資源需要回收。在`Point_draw()` 函數中,我們以一種圖片能夠識別的方式打印當前的坐標:
```c
static void Point_draw (const void * _self)
{
const struct Point * self = _self;
printf("\".\" at %d,%d\n", self -> x, self -> y);
}
```
這樣照顧到所有的動態連接方法,并且我們能夠定義類型描述符,在此一個空的指針代表一個不存在的析構器:
```c
static const struct Class _Point = {
sizeof(struct Point), Point_ctor, 0, Point_draw
};
const void * Point = & _Point;
```
`move()` 不是動態連接的,因此我們省略`static` 使得它作用域能夠超出`Point.c`并且我們不給它加類名前綴`Point` :
```c
void move (void * _self, int dx, int dy)
{
struct Point * self = _self;
self -> x += dx, self -> y += dy;
}
```
與在`new.c` 中的動態連接相結合,這就得出了`Point.c`中點的實現。
## 4.3 繼承——環
一個環形僅僅是一個大的點:此外對于中心坐標它需要一個半徑。畫法有點不同,但是移動只需要我們改變中心坐標。
這就是我們能夠正常的為我們的文本編輯器和演擇源代碼重用而做好準備的地方。我們對點的實現做一個拷貝并且改變環與點不同的地方。`Struct Circle` 獲取其他額外的組成:
```c
int rad;
```
這部分組成在構造器中初始化
```c
self->rad=va_arg(*app,int);
```
并且在`Circle_draw()` 中使用:
```c
printf("circle at %d,%d rad %d\n",
self —> x, self —> y, self —> rad);
```
我們在`move()` 中有點迷惑。對于一個點和一個環必要的動作是相同的:對于坐標部分我們需要增加轉移參數。然而,在一種情況,`move()` 工作于`struct Point` ,在另外一種情況,它工作與`struct Circle` 。如果`move()` 是動態連接的,我們需要提供兩個不同的函數去做相同的事情。但是,會有更好的方式,考慮一下點和環表示的層:
```c
struct Point struct Circle
```
圖片顯示每一個環都以一個點開始。如果我們分配一個`struct Circle` 通過增加到`struct Point`的結尾,我們可以向`move()` 函數中傳遞一個環,因為表示式的初始化部分看起來僅僅像點,而`move()` 方法期望接到收點,并且點僅僅是`move()` 方法能夠改變的。這里是一個合理的方式確保對環的初始化部分總看起來像點:
```c
struct Circle { const struct Point _; int rad; };
```
我們讓派生的結構體以一個我們要擴展的基結構體的拷貝而開始。信息隱藏要求我們決不直接的訪問基結構體;因此,我們使用幾乎不可見的下劃線作為它的名字并且把它聲明為`const`避開粗心的指派。
這就是簡單的繼承的全部:一個子類從一個超類(或者基類)繼承僅僅通過擴充表示超類的結構體。
由于子類對象(一個環)的表示就像一個超類對象(一個點)的表示一樣動身。環總能夠佯裝成一個點——在一個環的表示的初始化地址處的確是一個點的表示。
向`move()` 中傳遞一個環是完全確定的:子類繼承了超類的方法,因為這些方法僅在子類的表示上操作,這些子類的表示和超類的表示是相同的, 而這些方法原先就在超類上寫好了。傳遞一個環就像傳遞一個點意味著把`struct Circle*` 轉換成`struct Point*` 。我們將把這樣的操作看成一個從子類到超類的上拋——在標準化C語言中,它能夠使用明確的轉換操作符來實現或者通過中間的`void*` 的值。
這通常是不佳的,然而,傳遞一個點到一個函數專為環如,`Circle_draw()`: 如果一個點原先就是一個環,從`struct Point*` 轉換成`struct Circle*` 僅僅是可允許的。我們稱這樣的從超類到子類的轉換為下拋——這也要求明確的轉換或void*值,并且它僅僅對于指針,對于對象能夠使用,指針,對象在子類的開始做轉換。
對于動態連接方法如`draw()` ,這種情形是不同的。讓我們再次看先前的圖片,這次完全明確類型描述符如下 :
## 4.4 連接和繼承
`move()` 不是動態連接的并且不使用動態連接方法做工作。然而我們能夠傳遞一個指針和環到`move()` 中,它的確不是一個多肽的函數:`move()` 對于不同的對象不會做不同的處理,它總是增加參數到坐標,忽略其他與坐標相依附的。
當我們上拋從一個環到一個點時,我們沒有改變環的狀態,換句話說,即使我們把環的`struct Circle` 表示當成一個點的`struct Point` ,我們不會改變它的內容。結果,把環視為點作為一個類型描述符仍然擁有`Circle`,因為點在它的 `.class` 部分并沒有改變。`draw()`是一個選擇器函數,即,它將會使用無論傳入什么樣的參數作為自身,去處理被 `.class` 所指示的類型描述符,并且調用在這里存儲的畫圖方法。
一個子類繼承它的超類的靜態鏈接的方法——這些方法操作子類對象的部分,這些子類對象是已經在超類對象上呈現的。一個子類能夠選擇支持它自己的方法代替它的超類的動態連接方法。如果繼承,即,若沒有重寫,超類動態的連接的方法就像靜態連接的方法一樣的起作用并且修改子類對象的超類的部分內容。如果重寫,子類他自己的動態連接方法的版本訪問子類對象所有的表示,即,對于一個環,`draw()` 將會調用`Circle_draw()` 方法,此方法能夠考慮到半徑當畫環的時候。
## 4.5 靜態和動態連接
一個子類繼承了它的超類的靜態鏈接的方法并且選擇性的繼承或重寫動態連接的方法。考慮對于`move()` 和`draw()`的聲明如下:
```c
void move(void* point, int dx,int dy);
void draw(const void* self);
```
我們不能夠從這兩個聲明中發現連接,盡管對于`move()` 的實現能夠直接的工作,然而`draw()` 僅僅是一個選擇器函數在運行時跟蹤動態連接。不同點就是我們聲明一個靜態鏈接方法就像`move()` 在`Point.h` 中作為抽象數據類型接口的一部分,且我們聲明一個動態連接方法就像`draw()` 攜帶內存管理接口在`new.h` 中,因為迄今為止我們已經決定在`new.c` 中實現數據選擇器。
靜態鏈接會更加有效率因為C編譯器能夠使用直接的地址調用子程序,但是對于一個函數如`move()` 對于子類不能被重寫。動態連接在間接調用的擴展上更加便捷——我們已經對調用選擇器函數如`draw()`的額外開銷作了決定,檢查參數,定位,調用正確的方法。我們丟棄了檢查并且使用`macro*` 像如下減少了額外開銷:
```c
#define draw(self) ((*(struct Class**)self)->draw(self));
```
但是如果他們的參數有負面的影響宏會引發問題并且對于宏并沒有明確的技術用于操作可變參數列表。此外,宏需要`struct Class` 的聲明,此`struct Class` 到目前為止對于類的實現已經可用而不是對于整個程序。
不幸的是當我們設計超類時,我們還需要決定很多事情。但是函數調用方法是不會改變的,它會占用很多文本編輯,更可能的會在許多類中,把一個函數的定義從靜態轉換到動態連接,反之亦然。從第七章開始我們將使用一個簡單的預處理去簡化編碼,即使如此連接轉換也是極易出錯的。
帶著這種懷疑,與靜態鏈接相比決定動態連接可能會更好點即使它效率較低。通用函數能提供一個有用的概念性的抽象并且他們傾向于減少我們需要在項目過程中記憶的函數名的數量。如果,實現所有要求的類后,我們發現其實動態連接方法從來沒有被重寫,通過其單一的實現去替代它的選擇器并且甚至在`struct Class` 中浪費它的位置與擴展類型描述和更正所有的初始化相比麻煩會更少。
## 4.6 可見度和訪問函數
我們現在可以嘗試著實現`Circle_draw()` 。基于“need to know ”這樣的規則信息隱藏要求我們對于每個類使用3個文件。`Circle.h` 包含抽象數據類型接口;對于一個子類它包含了超類的接口文件以便于這樣的聲明使得繼承的方法可用:
```c
#include "Point.h"
extern const void* Circle; /*new(Circle,x,y,rad)*/
```
接口文件`Circle.h` 被應用程序代碼所包含并且對于類的實現;它避免了多次包含所引發的錯誤。
一個環的表示在第二個頭文件中聲明,`Circle.r` 。對于子類它包含了超類的表示文件以便于我們能夠通過擴展超類派生出子類的表示:
```c
#include "Point.r"
struct Circle{const struct Point _;int rad;};
```
子類需要超類的表示去實現繼承:`struct Circle` 包含了一個`const struct Point`。這個點確定不是只讀的——`move()` 將改變它的坐標——但是`const` 限定詞防止了意外的覆蓋它的組成部分。表示文件`Circle.r` 僅僅被類的實現所包含;仍然受到多重調用的保護。
最終,對一個環的實現對于類,對于對象管理,被在包含接口和表示文件的原文件`Circle.c` 中所定義:
```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 rad %d\n",self->_.x,self->_.y,self->rad);
}
```
在`Circle_draw()` 中,對于環我們通過子類部分使用“可見的名字”. 來讀取點部分。從信息隱藏的角度看這并不是一個好的注意。然而讀取坐標值不應該產生重大的問題,我們決不能確保在其他情形下,一個子類的實現不去直接的欺騙和修改它的父類的一部分,因此帶著其不變量去玩一場浩劫。
效率要求一個子類能直接的訪問到其超類的組成部分。信息隱藏和可維護性原則要求一個超類從它的子類上盡可能好的隱藏對它自己的表示。如果我們后面做出選擇,我們應該能夠提供對這些子類被允許查看超類所有組成部分訪問函數,并且對于這些組成部分提供更正函數,即,便要子類去做修改。
訪問和修改函數時靜態鏈接的方法。如果我們對于超類在表示文件中聲明了他們,超類僅包含在子類的實現中,我們可以使用宏,如果宏使用每個參數僅以此則副作用沒有問題。作為一個例子,在`Point.r` 中,我們定義了下面的訪問宏:
```c
#define x(p) (((const struct Point*)(p))->x)
#define y(p) (((const struct Point*)(p))->y)
```
這些宏對于任何以`struct Point` 開始對象能夠被應用于一個指針,也就是說,對于對象,從我們的點的任何子類。這項技術即為,上拋我們的點到超類并引用我們感興趣的部分。`const` 在拋得過程中對結果的分配。如果`const` 被忽略
```c
#define x(p) (((struct Point*)(p))->x)
```
一個宏調用`x(p)` 產生一個能成為分配的目標的`l-value`,一個好點的修改函數最好是一個宏的定義
```c
#define set_x(p,v) (((struct Point*)(p))->x=(v))
```
此定義產生一個分配。
在子類實現的外部對于訪問和修改函數我們僅僅使用靜態鏈接的方法。我們不能夠求助于宏,因為對于宏引用超類的內部表示是不可見的。對于包含進應用程序的信息隱藏并不提供表示文件`Point.r` 而實現。
宏定義揭示了,然而,一旦一個類的表示可用,信息隱藏能夠被很容易的擊敗。這里有一個方式更好的隱藏`struct Point` 。在超類的實現中,我們使用正常的定義:
```c
struct Point{
const void* class;
int x,y;
};
```
對于子類的實現我們提供下面的看起來不透明的版本:
```c
struct Point{
const char _[sizeof(struct {const void* class; int x,y;})];
};
```
這個結構體像先前擁有相同的大小,但是我們不能夠讀取也不能夠寫它的組成部分因為他們被隱藏在一個匿名的內部結構中。重點是這兩種聲明必須包含相同的組成部分的聲明并且這在沒有與處理器的情況下是很難維持的。
## 4.7 子類的實現——環
我們已經做好了些完整實現的準備,我們可以選擇先前部分介紹的我們最喜歡的技術。面向對象規定我們需要一個構造器,可能的話還會有一個析構器,`Circle_draw()`,和類型描述`Circle` 都綁定在一起。以便于練習我們的方法,我們包含了`Circle.h` 并增加了下面的行在4.1 部分的程序中做測試:
```c
case 'c':
p=new(Circle,1,2,3);
break;
```
現在我們能夠觀察到下面的測試程序的表現:
```
$ circles p c
"." at 1,2
"." at 11,12
circle at 1,2 rad 3
circle at 11,22 rad 3
```
環的構造函數接收3個參數:第一個參數為環的點的坐標接下來是半徑。初始化點部分是點的構造器的工作。它會處理部分`new()` 參數列表的參數。環的構造器從它的初始化半徑的地方攜帶保留的參數列表。
一個子類的構造器首先應該允許超類做部分初始化,這部分初始化把清晰地內存帶進超類對象。一旦超類構造器構造完成,子類構造器完成初始化并把超類對象帶進子類對象中。
對于環,意味著我們需要調用`Point_ctor()` 。像其他所有動態鏈接一樣,這個函數被聲明為`static` ,因此隱藏在`Point.c` 的內部。然而,我們仍然能夠通過在`Circle.c` 中可用的類型描述符來 `Point` 獲得此函數。
```c
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;
}
```
這里應該很清楚為什么我們傳遞參數的地址`app` 列表指針到每個構造器而不是`va_list` 的值本身:`new()`調用子類的構造器,此構造器調用超類的構造器,等等。最超級的構造器是第一個將去實際的作一些事情,并且會撿起傳進`new()` 的最左邊的參數列表。保留的參數對于下一個子類是可用的,等等知道最后,最右邊的參數被最終的子類所使用,也就是說,被`new()`所直接的調用的構造器所調用。
構造器以嚴格的相反的次序是最好的組織:`delete()` 調用子類的析構器。它首先應該銷毀它自己的資源接下來調用直接的超類的析構器,這個析構器可直接的銷毀下一個資源集等等。構造是先發生在子類之前的父類上的。析構則是相反,子類要先于父類,即,環部分要先于點部分。這里,然而,什么也不需要做。
我們先前已經讓`Circle_draw()` 工作了,我們使用可見部分,并且編碼表示文件`Point.r` 如下:
```c
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)
```
現在我們可以對于`Circle_draw()` 使用訪問宏:
```c
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()` 擁有靜態鏈接并且被從點的實現上繼承。我們得出結論環的實現是通過定義僅僅全局可見`Circle.c` 的部分內容:
```c
static const struct Class _Circle = {
sizeof(struct Circle), Circle_ctor, 0, Circle_draw
};
const void * Circle = & _Circle;
```
然而,在接口,表示式,實現文件之間似乎我們有一個可行的分配程序文本實現類的策略,點和環的例子還沒有顯現出一個問題:如果一個動態連接的方法如`Point_draw()` 在子類中沒有被重寫,子類的類型描述符需要指向在父類實現的函數。函數名,然而在這里被定義成`static`,因此選擇器是不能夠被規避的。我們將在第六章看到一個清晰地解決此問題的方法。作為暫時的權衡,我們在這種情況下可以避免對`static` 的使用,僅僅在子類的實現文件中聲明函數的頭,對于子類并且使用函數名去初始化類型描述。
## 4.8 總結
超類的對象和子類是相似的,但是在表現形式上并不相同。子類正常情況下會有更詳盡的陳述更多的方法——他們被超類對象的版本專用指定。
我們使用超類對象的表示的拷貝來作為子類對象表示的開始,即,子類對象通過把它的組成部分增加到超類對象的末尾被表示。
一個子類繼承了超類的方法:因為一個子類對象的起始部分看起來像超類對象,我們可以上拋并且看到一個指向子類對象的指針作為一個指向我們能夠傳遞超類方法的超類對象。為了避免顯性轉換,我們使用`void*`作為通用指針來聲明所有方法的參數。
繼承可以被看成一個多態機制的根本形式:一個超類方法接受不同類型,它自己的類和所有子類命名的對象。然而因為對象都佯裝成超類對象,方法僅僅在每個對象的超類部分起作用,并且它將,因此從不同的類對于對象不會起不同的作用。
動態鏈接方法能夠從一個超類繼承或在子類中重寫——對于子類通過無論何種函數的指針被放進類型描述符來決定。因此,對于一個對象如果動態鏈接方法被調用,我們總能夠訪問屬于對象真正的類的方法即使指針上拋到一些超類上。如果動態鏈接方法被繼承,它只能在子類對象的超類部分起作用,因為它的確不知道子類的存在。如果一個方法被重寫,子類的版本能夠訪問整個對象,他甚至可以通過顯性的超類的類型描述符的使用來調用它關聯的超類的所有方法。
特別注意,對于超類的表示,構造器首先回調超類的構造器直到最終的祖先以便于每個子類的構造器僅僅處理它自己的對類的擴展。每個超類析構器應該先刪除它的子類的資源然后調用超類的析構器等等直到最終的祖先。構造器的調用順序是從祖先到最終的子類,析構器的發生則正好是相反的順序。
我們的策略還是有點小毛病的:在通常情況下我們不應該從一個構造器中調用動態鏈接方法,因為對象也許并沒有完全被初始化好。在構造器被調用之前`new()` 把最終的類型描述符插入到一個對象中,作為一個構造器在相同的類中是沒有必要的訪問方法的。安全的技術是在相同的類中對于構造器通過內部的名字來調用方法,也就是說,對于點,我們調用`Points_draw()`而不是`draw()` 。
為了鼓勵信息隱藏,我們使用了三個文件對類的實現。接口文件包含了抽象的數據類型描述,表示文件包含了對象的結構,實現文件包含了方法和初始化類型描述的代碼。一個接口文件包含了超類接口文件并且被實現和任何應用所包含。一個表示文件包含了超類的表示文件并且僅僅被實現所包含。
超類的部分不應該直接的在子類中被引用。相反,對于每個部分我們能夠既提供靜態鏈接訪問和盡可能的修改方法,也能對于超類的表示文件增加適當的宏。函數符號使得使用文本編輯器或調試器去跟蹤可能的信息泄露或不變量的破壞更簡單。
## 4.9 是或有嗎?——繼承對集合
作為`struct Circle` 我們對環的表示包含了對點的表示:
```c
struct Circle { const struct Point _; int rad; };
```
但是,我們自然絕冬不去直接的訪問者部分。相反,當我們想要繼承我們從`Circle` 上拋到`Point` 并且在這里處理`struct Point` 的初始化。
這里有另外一個表示環的方式:它能包含一個點作為一個集合。我們能夠僅僅通過指針來處理對象;因此這樣的一個環的表示看起來就像如下所示:
```c
struct Circle2 {
struct Point * point;
int rad;
};
```
這個環一點也不像一個點,也就是說,它不能夠從`Point` 所繼承并且重用它的方法。然而,它能夠把點的方法應用到點的部分;它僅僅不能把點的方法用于它自己。
如果一種語言對于繼承有明確的符號,差異就會更加明顯,相似的表示在C++ 中會有如下的表示:
```cpp
struct Circle:Point{int rad;}; //inheritance
struct Circle2{ struct Point point;int rad}; //aggregate
```
在C++ 中作為一個指針我們是不必要訪問對象的。
繼承,即,從超類來建立子類,而集合,即,把對象的一部分作為另外一個對象的一部分,提供非常相似的功能。這些應用在特殊的設計中通常被所 is-it-or-has-it?的測試所決定:如果一個新類的一個對象僅僅像一些其他類的對象,我們應該使用繼承來實現新的類;如果一個新類有一個其他類作為它的狀態的一部分對象,我們應該建立集合。
到我們的點所關注的,一個環僅僅是一個大的點,這就是為什么我們使用繼承來做一個環的原因。一個方形是一個不明確的例子:我們能通過一個參考點和邊的長度來描述它,或我們能夠使用端點的對角線或甚至三個角來描述。僅僅帶參考點是方形的幾分花哨點;其他表示通向集合。 在我們的算術表達式中,我們已經使用了繼承從單目到雙目操作節點,但是這已經充分的違背了測試。
## 4.10 多重繼承
因為我們使用平凡的標準化C語言。我們不能夠隱藏這樣的事實——繼承意味著在另一個結構的開始包含一個結構體。利用上拋是在子類的對象上重復利用超類方法的關鍵所在。通過投擲一個結構體起始的地址完成一個從環島到點的上拋;指針的值并沒有改變。
如果我們在其他結構中包含兩個及以上的結構體,并且如果我們愿意在上拋期間做一些地址的處理,我們可以稱這樣的結果為多重繼承:一個對象能夠像它屬于幾個類一樣了表現。優點似乎是我們不必很仔細的設計繼承的關系——我們可以很快的把類仍到一起并且繼承我們希望繼承的任何東西。缺點是,顯然,在我們能夠重用方法之前我們得有地址處理機制。
事情能夠實際的很快讓我們感到迷惑。思考一個文本,一個方形,每一個都有一個繼承的引用點。我們能夠把他們一起扔到一個按鈕上——僅僅存在的問題希望這個按鈕應該繼承一個或兩個引用點。
我們使用標準化C語言擁有很大的優點:它會使這樣的事實很明顯,即,繼承——多重或其他總是伴隨著包含而進行。包含,然而也能作為集合被實現。與復雜化語言定義和增加過量實現相比多重繼承對于程序員來說要做的更多,這一點也不清晰。我們將使得事情變得簡單兵器只做簡單的繼承。第14章將首要展示多重繼承的使用,庫的合入能夠被集合和消息轉換所實現。