Effective C++ 第六章--繼承與物件導向設計筆記

FreeeLinux發表於2017-01-19

條款32:確定你的public繼承塑模處is-a關係

  • “public繼承”意味著is-a。適用於每個基類身上的每一件事一定也適用於派生類身上,因為每一個派生類物件也都是一個基類物件

條款33:避免遮掩繼承而來的名稱

派生類繼承自基類,會產生命名作用域巢狀。派生類內部自成一個作用域,如果派生類使用某個名字,首先會在派生類命名作用域查詢。如果派生類內部沒有該名字,才會在基類的作用域類查詢該名字。如果基類也沒有,再去namespace,甚至全域性作用域。

派生類繼承自基類後,如果定義一個函式,函式名名與基類中函式名相同,那麼基類中的所有同名函式會被覆蓋。無論引數是否一致。因為這是命名規則。

如果派生類繼承基類,基類有同名且過載多次的函式,派生類希望重新定義或者覆蓋其中一部分,使用using。使用using可以使基類一部分和派生類過載函式同時存在,而不致使基類所有同名函式全部被覆蓋。

如:

class base {
public:
    void fun(int i) { std::cout<<"base"<<std::endl; }
};

class derived : public base{
public:
    using base::fun;
    void fun() { std::cout<<"derived"<<std::endl; }
};

如上用法,則不會產生全部覆蓋。派生類可以根據不同引數選擇呼叫基類或是自己的函式。

有時候派生類不想繼承基類的全部函式,這時候,不能用公有繼承,因為公有繼承是is-a。此時使用using不可行,因為using會使基類中所有同名函式都被派生類可見。我們需要使用private繼承加轉交函式來實現。

class derived : private base {  //私有繼承,只繼承實現
public:
    virtual void fun() { base::fun(); }
};

上面程式碼僅為示意,以這種形式書寫,就可呼叫基類某個函式。

  • 派生類內的名稱會遮掩基類內的名稱。在公有繼承下從來沒有人希望如此。
  • 為了讓遮掩的名稱再見天日,可使用using宣告式或轉交函式(forwarding functions)。

條款34:區分介面繼承和實現繼承

  • 介面繼承和實現繼承不同。在公有繼承下,派生類總是繼承基類的介面。
  • 純虛擬函式只具體指定介面繼承
  • 虛擬函式(非純)具體指定介面繼承及預設實現繼承。(意思是虛擬函式自身定義了一份預設實現,如果子類不重寫的話,那使用的就會是這份基類的實現)。
  • 普通函式具體指定介面繼承以及強制性實現繼承。(公有繼承下普通函式是不能被重寫的,雖然沒有語法錯誤,但不符合is-a,所以是強制性實現)。

條款35:考慮virtual函式以外的其他選擇

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

大概意思就是virtual函式要作為private函式,使用一個non-virtual函式呼叫private virtual函式。

比如:

class game_character {
public:
    int health_value() const {
        ...           //做一些前期工作
        int ret = do_health_value();   //做真正的工作
        ...           //做一些後期處理
        return ret; 
    }
private:
    virtual int do_health_value() const {  //派生類可重新定義它,只不過不能使用
        ...
    }
};

這一設計,又稱non-virtual interface(NVI)手法。它是所謂Template Method設計模式的一種形式。上面的non-virtual函式可以稱作wrapper。

NVI手法的一個優點在於可以做事前,事後的工作。比如事前加鎖,事後解鎖,assert驗證等。

藉由Funciton Pointers實現Strategy(策略)模式

實際上就是針對不同需求使用不同函式指標進行回撥,不贅述。

藉由tr1::function完成Strategy模式

這個實際上是std::function,或者說boost::function,這本書比較老,所以當時還沒有。

std::function相對函式指標的優勢在於全能!不僅可使用函式,也可食用函式物件,甚至成員函式。不過要注意與std::bind的配合,繫結成員函式需要改該型物件指標。

古典的Strategy模式

利用純程式碼實現策略模式:

class game_character; //前向生命

class health_calc_func {
public:
    virtual int calc(const game_character& gc) const 
    { ... }
};
health_calc_func default_health_calc;

class game_character {
public:
    explicit game_character(health_calc_func* phcf = &default_health_cal) : health_calc_(phcf)
    {}
    int health_value() const {
        return health_calc_->calc(*this); 
    }
private:
    health_calc_func* health_calc_;
};

上述就是策略模式(策略模式真平易近人),用上述方法只要為health_calc_func繼承體系納入一個派生類,就可以新增一個新的演算法了。

條款36:絕不重新定義繼承來的non-virtual函式

這個不必說,因為要符合is-a。

條款37:絕不重新定義繼承而來的預設引數值

程式碼驗證:

class base {
public:
    virtual void fun(int i = 3) { std::cout<<i<<std::endl; }
};

class derived : public base {
public:
    virtual void fun(int i = 2)  {  std::cout<<"derived"<<std::endl; std::cout<<i<<std::endl; }
};

int main()
{
    base *b = new derived;
    b->fun();
    return 0;
}

上述程式碼列印的結果是:

derived  3

這是完全錯誤的結果,呼叫了派生類函式,卻列印出基類的預設值。

  • 絕對不要重新定義一個繼承而來的預設引數值,因為預設引數值都是靜態繫結,而virtual函式——你唯一應該覆寫的東西——卻是動態繫結。

條款38:通過複合塑模處has-a或”根據某物實現出”

複合有has-a和”is-implement-in-terms-of”兩種。has-a就是某物有某個成員,比如教室,成員變數就是桌子等,這個好理解。主要來說後者,意思是根據某物實現出。比如自己實現一個資料結構集合set,我們使用連結串列list來做它的底層實現,那麼不應該用繼承自list(這會構成is-a,明顯不符合),應該把list作為set的成員變數。成員函式在連結串列上進行相應的簡單操作就可以實現set的功能,所以說set是根據list實現出。

  • 複合(composition)的意義和public繼承完全不同。
  • 在應用域(application domain),複合意味has-a(有一個)。在實現域(implementation domain),複合一位is-implemented-in-terms-of(根據某物實現出)。

條款39:明智而審慎的使用private繼承

  • private繼承意味著”根據某物實現出”。它通常比複合(composition)的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函式時,這麼設計是合理的。
  • 和複合(composition)不同,private繼承可以造成empty base最優化。這對致力於”物件尺寸最小化”的程式庫開發者而言,可能很重要。(這意思是,基類如果是空類,非空派生類繼承基類,基類的那一個char佔位位元組會被去掉。實際上,這本書太老了,即便不使用private繼承,目前所有編譯器都會針對這種情況優化,公有繼承也同樣。所以這裡是錯的。

我們可以使用複合來代替私有繼承,如下:

class base {
private:
    virtual void timer() { std::cout<<"base"<<std::endl; }
};

class derived {
public:
    void call() { timer_.timer(); }
private:
    class inside_class : public base {
    public:
        virtual void timer() { std::cout<<"inside_timer"<<std::endl; }
    };  
    inside_class timer_;
    //如果derived繼承自base,此處可以重寫:
    //virtual void timer() { std::cout<<"derived"<<std::endl; }   
    //注意本段程式碼derived並沒有繼承base,上面一行程式碼僅為說明:如果繼承,基類私有虛擬函式雖然能重寫,但是是不可呼叫的。   
};

int main()
{
    derived d;
    d.call();
    return 0;
}

並且利用上述技巧我們可以實現不能被派生類定義的虛擬函式(注意去掉註釋):
如果要類A繼承某個類,要實現它的虛擬函式,我們使用一個成員類公有繼承它,幷包含一個該成員類的物件。我們就可以在成員類中重寫改虛擬函式,呼叫該虛擬函式通過成員類物件即可。那麼,往後如果某個類B繼承類A,那麼它是不可以定義上述虛擬函式的,因為那是類A私有物件的函式。

條款40:明智而審慎的使用多重繼承

  • 多重繼承比單一繼承複雜。它可能導致新的歧義性,以及對virtual繼承的需要。
  • virtual繼承會增加大小、速度、初始化(及賦值)複雜度等等成本。如果virtual base classes不帶任何資料,將是最具實用價值的情況。
  • 多重繼承的確有正當用途。其中一個情節設計”public繼承某個Interface class”和”private 繼承某個協助實現的class”的兩相結合。

相關文章