Effective C++ 第六章--繼承與物件導向設計筆記
條款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”的兩相結合。
相關文章
- 《Effective C++》第三版-6. 繼承與物件導向設計(Inheritance and Object-Oriented Design)C++繼承物件Object
- Javascript物件導向與繼承JavaScript物件繼承
- JS物件導向程式設計(四):繼承JS物件程式設計繼承
- java-物件導向程式設計--繼承Java物件程式設計繼承
- 物件導向--繼承物件繼承
- 物件導向:繼承物件繼承
- 物件導向-繼承物件繼承
- ~~核心程式設計(五):物件導向——多繼承~~程式設計物件繼承
- Golang物件導向_繼承Golang物件繼承
- 物件導向之繼承物件繼承
- java物件導向繼承Java物件繼承
- 理解Js中物件導向程式設計的繼承JS物件程式設計繼承
- 【JavaScript筆記 · 基礎篇(十)】物件導向程式設計之三:繼承機制JavaScript筆記物件程式設計繼承
- Kotlin 物件導向程式設計 (OOP) 基礎:類、物件與繼承詳解Kotlin物件程式設計OOP繼承
- 物件導向之_繼承概念物件繼承
- Python - 物件導向程式設計 - 三大特性之繼承Python物件程式設計繼承
- 21. 物件導向之繼承物件繼承
- Javascript實現物件導向繼承JavaScript物件繼承
- [筆記]物件導向的程式設計筆記物件程式設計
- 說清楚javascript物件導向、原型、繼承JavaScript物件原型繼承
- JavaScript物件導向 ~ 原型和繼承(1)JavaScript物件原型繼承
- 《JavaScript物件導向精要》之五:繼承JavaScript物件繼承
- 物件導向 -- 三大特性之繼承物件繼承
- JavaScript物件導向那些東西-繼承JavaScript物件繼承
- JAVA物件導向高階一:繼承Java物件繼承
- 5-Java物件導向-繼承(下)Java物件繼承
- JavaScript物件導向—繼承的實現JavaScript物件繼承
- java學習——物件導向之繼承Java物件繼承
- Cris 的 Scala 筆記整理(八):物件導向中級-繼承和多型筆記物件繼承多型
- 物件導向程式設計C++物件程式設計C++
- Golang物件導向程式設計之繼承&虛基類【組合&介面】Golang物件程式設計繼承
- JS物件導向:JS繼承方法總結JS物件繼承
- python物件導向的繼承-組合-02Python物件繼承
- go物件導向思想:封裝、繼承、多肽Go物件封裝繼承
- C++ 物件導向高階設計C++物件
- 前端筆記之JavaScript物件導向(二)內建建構函式&相關方法|屬性|運算子&繼承&物件導向前端筆記JavaScript物件函式繼承
- 物件導向筆記物件筆記
- C++學習筆記——C++ 繼承C++筆記繼承
- JS的物件導向(理解物件,原型,原型鏈,繼承,類)JS物件原型繼承