2021年4月18日 星期日

[Effective C++] [閱讀心得]: public 繼承代表 is-a 的關係

Photo by Hamza NOUASRIA on Unsplash

所謂 is-a 代表能施加在父類別的每一件事情也要能夠施加在子類別身上,Scott Meyers 在書中舉了一個鮮明的例子

class Bird {
public:
    virtaul void fly();
    ...
};

class Penguin : public Bird {
    ...
};

在這個例子中, Penguin 是一種 Bird,但是 Penguin 並不會飛,這就違反了 is-a 的關係。因此,這樣的塑模方式並不正確。比較理想的方式是


class
Bird { public: ... }; class FlyingBird : public Bird { public: virtual void fly(); ... }; class Penguin : public Bird { ... };

這裡新增了一個 FlyingBird 的類別,並將原本 Bird 中的 fly() 移動到 FlyingBird 中。如此一來,像 Penguin、Chicken 這種不會飛的鳥會繼承 Bird,而像 Eagle、Owl 這些會飛的鳥則繼承  FlyingBird。這樣的塑模方式才比較符合 is-a 的語意。

而為了符合 is-a 的語意,又會衍生出一些其他的議題。

1. 避免遮掩繼承而來的名稱

這邊直接用例子來說明

class Base {
public:
    void mf();
    void mf(double);
};

class Derived : public Base {
public:
    void mf();
};

這個例子中, Derived 類別 public 繼承了 Base 類別,並覆寫了其中不帶參數的 mf()。這會出現一個問題: Base 類別的 mf(double) 會被遮掩掉。也就是,下面這段程式碼將無法通過編譯

Derived d;
double x;
...
d.mf(x); // 錯誤!!! mf(double)被遮掩掉

這就表示,Derived 類別不再是某一種 Base 類別,因為他無法呼叫 Base 的 mf(double) 函式。

要解決這個問題的方法就是利用 using。如下所示

class Base {
public:
    void mf();
    void mf(double);
};

class Derived : public Base {
public:
    using Base::mf; // 加入此行
    void mf();
};

如此一來,在 Derived 類別中,Base 的 mf 就不再被遮掩掉,因此 Derived 的物件便可以呼叫到 double 版本的 mf 函式,is-a 的關係也就重新被建立起來了。

2. 不重新定義 non-virtual 函式

一般而言,父類別的 non-virtual 函式通常具有"不變性",也就是說設計者通常會希望這樣的函式在子類別中擁有與父類別相同的行為。因此,如果我們在子類別中重新定義了 non-virtual 函式,這樣即有可能會破壞 is-a 的關係。

而另一個不要重新定義 non-virtual 函式的原因是: non-virtual 函式是一種靜態綁定。直接用一個例子說明

class Base {
public:
    void mf();
};

class Derived : public Base {
public:
    void mf();
};

Base *b = new Derived();
b->mf(); // Base 版本的mf()

在這個例子中,Derived 類別重新定義了 mf() 函式,很明顯地,設計者希望 Derived 的物件會呼叫到它自己的 mf()。然而,因為 b 被宣告成 Base 的指標,而 mf() 是靜態綁定,因此實際的情況是: 呼叫 mf() 時會呼叫到 Base 版本的 mf(),而不是 Derived 版本的 mf()。

沒有留言:

張貼留言