C++類別別的複合和繼承關係(C++繼承類和封閉類的關係)

2020-07-16 10:04:25
在 C++ 中,類和類之間有兩種基本關係:複合關係和繼承關係。

複合關係也稱為“has a”關係或“有”的關係,表現為封閉類,即一個類以另一個類的物件作為成員變數。如上節中 CStudent 類的例子,每個 CStudent 物件都“有”一個 string 類的成員變數 name,代表姓名。

繼承關係也稱為“is a”關係或“是”的關係,即派生類物件也是一個基礎類別物件。如在上節的程式中,CUndergraduateStudent 類(代表本科生)繼承了 CStudent 類(代表學生)。因為本科生也是學生,因此可以說,每一個 CUndergraduateStudent 類的物件也是一個 CStudent 類的物件。

在設計兩個有關係的類時要注意,並非兩個類有共同點,就可以讓它們成為繼承關係。讓類 B 繼承類 A,必須滿足“類 B 所代表的事物也是類 A 所代表的事物”這個命題從邏輯上是成立的。例如,寫一個平面上的點類 CPoint::
class CPoint{
    double x, y;  //點的坐標
};
又要寫一個圓類 CCircle。CCircle 類有圓心,圓心也是平面上的一點,因而 CCircle 類和 CPoint 類似乎有相同的成員變數。如果因此就讓 CCircle 類從 CPoint 類派生而來,即採用如下寫法:
class CCircle: public CPoint{
    double radius;  //半徑
};
是不正確的。因為,“圓也是點”這個命題是不成立的。這個錯誤不但初學者常犯,甚至很多知名教材也以此作為繼承的例子。正確的做法是使用“has a”關係,即在 CCircle 類中引入 CPoint 成員變數,代表圓心:
class CCircle
{
    CPoint center;  //圓心
    double radius;  //半徑
}
這樣,從邏輯上來說,每一個“圓”物件都包含(有)一個“點”物件,這個“點”物件就是圓心——這非常合理。

如果寫了一個 CMan 類代表男人,後來發現又需要一個 CWoman 類代表女人,僅僅因為 CWoman 類和 CMan 類有共同之處,就讓 CWoman 類從 CMan 類派生而來,同樣也是不合理的。因為“一個女人也是一個男人”從邏輯上不成立。

但是讓 CWoman 類包含 CMan 類成員物件就更不合適了。

此時正確的做法應該是概括男人和女人的共同特點,編寫一個 CHuman 類,代表“人”,然後 CMan 類和 CWoman 類都從 CHuman 類派生。

有時,複合關係也不一定都是通過封閉類實現的,尤其當類 A 中有類 B,類 B 中又有類 A 的情況。

假設要編寫一個小區養狗管理程式,該程式需要一個“主人”類,還需要一個“狗”類。狗是有主人的,主人也有狗。假定狗只有一個主人,但一個主人可以有最多 10 條狗。該如何處理“主人”類和“狗”類的關係呢?下面是一種直觀的寫法:
class CDog;
class CMaster  //主人
{
    CDog dogs[10];
    int dogNum;  //狗的數量
};
class CDog
{
    CMaster m;
};
這種寫法是無法通過編譯的。因為儘管提前對 CDog 類進行了宣告,但編譯到第 4 行時,編譯器還是不知道 CDog 類的物件是什麼樣的,所以無法編譯定義 dog 物件的語句。而且這種“人中有狗,狗中有人”的做法導致了回圈定義。

避免迴圈定義的方法是在一個類中使用另一個類的指標,而不是物件作為成員變數。例如下面的寫法:
class CDog;
class CMaster
{
    CDog* dogs[10];
    int dogNum;  //狗的數量
};
class CDog
{
    CMaster m;
};
上面這種寫法在第 4 行定義了一個 CDog 類的指標陣列作為 CMaster 類的成員物件。指標就是地址,大小固定為 4 個位元組,所以編譯器編譯到此時不需要知道 CDog 類是什麼樣子。

這種寫法的思想是:當一個 CMaster 物件養了一條狗時,就用 new 運算子動態分配一個 CDog 類的物件,然後在 dogs 陣列中找一個元素,讓它指向動態分配的 CDog 物件。

這種寫法還是不夠好。問題出在 CDog 物件中包含了 CMaster 物件。在多條狗的主人相同的情況下,多個 CDog 物件中的 CMaster 物件都代表同一個主人,這造成了沒有必要的冗餘:一個主人用一個 CMaster 物件表示足矣,沒有必要對應於多個 CMaster 物件。

而且,在一對多這種情況下,當主人的個人資訊發生變化時,就需要將與其對應的、位於多個 CDog 物件中的 CMaster 成員變數 m 都找出來修改,這毫無必要,而且非常麻煩。

正確的寫法應該是為“狗”類設一個“主人”類的指標成員變數,為“主人”類設一個“狗”類的物件陣列。如下所示:
class CMaster;
classCDog
{
    CMaster* pm;
};
class CMaster
{
    CDog dogs[10];
    int dogNum;
};
這樣,主人相同的多個 CDog 物件,其 pm 指標都指向同一個 CMaster 物件。

實際上,每個主人未必都養 10 條狗,因此出於節省空間的目的,在 CMaster 類中設定 CDog 類物件的指標陣列,而不是物件陣列,也是一種好的寫法。如下所示:
class CMaster
{
    CDog* dogs[10];
    int dogNum;
};
有的教材將類 A 的成員變數是類 B 的指標這種情況稱為“類 A 知道類 B”,兩個類之間是“知道”關係。