1. 程式人生 > 實用技巧 >Effective C++ - 繼承與面向物件設計

Effective C++ - 繼承與面向物件設計


author: lunar
date: Wed 16 Sep 2020 03:14:38 PM CST

6. 繼承和麵向物件程式設計

條款32: Make sure public inheritance models "is-a".

如果你令class D以public形式繼承class B, 你便是告訴編譯器每一個型別為D的物件同時也是一個型別為B的物件.

"public繼承"意味著is-a, 適用於base class的每一件事也要適用於derived class身上.

條款33: Avoid hiding inherited names.

這是一個關於作用域的問題. 當編譯器處理一個變數時, 它會現在local作用域內查詢該變數.

對於繼承了base class和derived class, 其作用域巢狀在base class內. 對於在derived class的函式mf4中呼叫的一個函式mf2, 編譯器會首先在mf4的作用域內查詢. 然後在derived class內查詢, 然後去base class內查詢. 然後在包含base class的namespace內查詢. 最後在global作用域內查詢.

當子類需要過載父類的成員函式時, 則需要過載所有的同名函式. 如果只是過載一個, 則其它同名函式將會被覆蓋, 無法被子類繼承. 即使引數不同也一樣.

這類問題的解決辦法可以藉助using關鍵字來完成, 例如下面的程式碼, 子類就可以正常使用父類的方法.

#include <iostream>
using namespace std;

class Base {
public:
    void func(int x) {
        cout << "base func" << endl;
    }
};

class Derived: public Base {
public:
    using Base::func;
    void func() {
        cout << "derived func" << endl;
    }
};

int main() {
    Derived d;
    d.func(1);
    return 0;
}

條款34: Differentiate between inheritance of interface and inheritance of implementation.

對於base class的成員函式的宣告和定義, 我們希望有多種不同的繼承方式:

  1. 只繼承base class的函式宣告: 將相應介面設為純虛擬函式可以實現.
  2. 同時繼承函式宣告和定義: 使用非純虛擬函式可以實現

條款35:Consider alternatives to virtual functions.

藉由 Non-Virtual Interface 手法實現 Template Method 模式

有這樣一種場景:對於所有動物的父類Animal,所有動物都要定義“吃”這個動作,但是每個子類的實現方式又有不同。我們需要在每次呼叫“吃”的API時都要列印一次日誌,這個日誌對於所有不同實現的子類都是相同的。

我們可以這樣實現:將具體吃的實現函式定義為private virtual型別的函式,而日誌列印的函式放在一個wrapper函式裡面,這個函式是non-virtual的,在這個函式裡可以呼叫具體的實現函式。

這種手法稱為non-virtual interface (NVI)

藉由 Function Pointers 實現 Strategy 模式

利用函式指標將進行實際操作,在建構函式中將這個函式指標傳入。

藉由 tr1::function 完成 Strategy 模式

std::tr1::function 是一種通用、多型的函式封裝,tr1::function的例項可以對任何可呼叫的物件進行儲存、複製和呼叫操作。

在例項化時,其接受一個函式的型別作為模板引數。比如對於接受一個const int,返回int的函式來說:

std::tr1::function<int (const int)> instance;

可以將這個instance指向任何可呼叫物件的地址,然後就可以像普通函式一樣進行呼叫。

這個條款看不下去了,看得人頭大。

條款36:Never define an inherited non-virtual function.

這個是很顯而易見的,如果你想要過載父類的函式的話,最好還是使用virtual函式。因為non-virtual函式都是靜態繫結的,你用哪種型別的變數呼叫成員函式,就只會呼叫該種類型的成員函式,不會管你物件具體是哪種型別的。

條款37:Never redefine a function's inherited default parameter value.

由於條款36,只需要考慮要不要重定義繼承而來的帶有預設引數值的virtual函式。

這種情況下,本條款成立的理由是:virtual函式系動態繫結,而預設引數值是靜態繫結的。

也就是說,對於Base* b = new Derived()類似形式定義的變數,當b呼叫函式時,函式的具體實現雖然會使用Derived類裡面定義的,但是預設引數卻會使用Base中定義的。

如果實在想要為函式帶一個預設引數,可以考慮採用條款35所提到的NVI 風格的寫法。

條款38:Model "has-a" or "is-implemented-in-terms-of" through composition.

中文意思是:通過複合塑模出 has-a 或 “根據某物實現出”。

複合是型別之間的一種關係,當某種型別的物件內含其它型別的物件,便是這種關係。

程式中的物件其實相當於你所塑造的世界中的某些事物,例如人、汽車、一幀幀視訊畫面等等,這樣的物件屬於應用域(application domain)。另一些物件則完全是實現細節上的人工製品,比如互斥鎖,二叉樹,緩衝區等等,這類物件屬於實現域(implementation domain)。

應用域的物件之間的關係則是has-a,而實現域之間的關係則是“根據某物實現出”。例如,你要實現一個自己版本的set,你想通過STL的list 來實現,但是你不能直接繼承list,因為前面的條款講過:直接public繼承會導致兩個物件之間的關係變為is-a。所以只能在底層用list來裝載資料,就如同list底層通過陣列來裝載資料。這樣兩者之間的關係就是“根據某物實現出”。

條款39: Use private inheritance judiciously.

private繼承意味著兩個主要的規則:

  1. 編譯器不會自動將derived class物件轉換為base class物件;
  2. 由private class繼承而來的所有成員,在derived class中都會變成private屬性,即使它們在base class中屬於protected甚至public屬性。

private繼承意味著 implemented-in-terms-of 的關係。如果你讓class D private繼承class B,你的用意是為了採用class B內已經備妥的特性,而不是B物件和D物件存在任何觀念上的關係。

複合的意義也是這樣,儘可能使用複合,只有當protected成員或者virtual函式牽扯進來的時候才使用private繼承。

給個示例:

一個Widget型別在實現中需要使用到Timer的某個函式並重寫一次,想要重寫就只能繼承。重寫時放在private領域內,因為這是內部實現時要用到的介面,不是可以對外開放的介面。

還有一種寫法,在Widget內宣告一個巢狀式private class WidgetTimer,後者以public的形式繼承Timer

有兩點理由證明下面這種寫法更好:

  1. Widget類可能會被別的類繼續繼承,這樣這個virtual函式又可能會被重寫。而下面的寫法就不存在這種問題,因為private領域內定義的類是不對子類可見的。
  2. 編譯依存性的問題,如果Widget繼承自Timer,則勢必會和Timer產生編譯依存性。而後者可以選擇將WidgetTimer移出,然後帶一個簡單的WidgetTimer宣告式。

條款40:Use multiple inheritance judiciously.

C++其實並不建議使用多重繼承。

如果你繼承的類也繼承了其它的類的話,這時來自於基類的資料會同時存在你的父類和本類中,造成了空間的浪費。這時可以使用虛繼承。

對於虛繼承的建議:

  1. 非必要不適用;
  2. 如果要使用virtual base classes,極可能避免在其中放置資料。