Effective C++ 第六章—— 繼承與面向對象設計
c++的OOP的特點:繼承可以是單一繼承或多重繼承;每一個繼承的連結可能是public、protected或private,也可是是virtual或non-virtual;成員函數可以是virtual或non-virtual的。
條款32——确定你的public繼承塑模是is-a關系
公有繼承(public inheritance)意味着一種“is-a”的關系。
在C++領域中,在公有繼承時,任何一個函數如果期望獲得一個基類實參的話,都也願意接受一個公有派生類對象。
public繼承主張,能夠施行于base class對象上的每件事情,也可以施行于derived class對象身上。
class之間的關系除了is-a、還有兩種,分别為has-a、is-implemented-in-terms-of(根據某物實作出)。
請記住:
- “public繼承”意味is-a。适用于base class上的每一件事也一定适用于derived class身上,因為每一個derived class對象都是一個base class對象。
條款33——避免遮掩繼承而來的名稱
C++的名稱遮掩規則是指局部作用局和全局作用域名稱相同時,在局部作用域内的同名變量會遮掩全局作用域的同名變量。
derived class作用域被嵌套在base class對象中。
在公有繼承時,如果base class 和derived class中有同名函數,derived class的同名函數會遮掩掉base class的同名函數,例如:
class Base{
private:
int x;
public:
virtual void mf1 () =0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};
Dervied d;
int x;
d.mf1(); //ok,調用Derived::mf1
d.mf1(x); //worry, Derived::mf1遮掩了Base::mf1
d.mf2(); //ok,調用Base::mf2
d.mf3(); //ok,調用Derived::mf3;
d.mf3(x); //worry,Derived::mf3遮掩了Base::mf3
遮掩規則不論base class和derived class函數同名且有不同參數,是不是virtual或non-virtual,都适用。
上述規則導緻沒有辦法繼承重載函數,然而有時候,我們希望繼承重載函數。可以适用using聲明式來解決:
class Base{
private:
int x;
public:
virtual void mf1 () =0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
using Base::mf1;//讓Base class中名為mf1和mf3的所有東西在Derived作用域内可見(并且public)
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
...
};
Dervied d;
int x;
d.mf1(); //ok,調用Derived::mf1
d.mf1(x); //ok,調用Base::mf1
d.mf2(); //ok,調用Base::mf2
d.mf3(); //ok,調用Derived::mf3;
d.mf3(x); //ok,調用了Base::mf3
如果你繼承并加上重載函數,而你又希望重新定義或覆寫(推翻)其中的一部分,你必須為哪些原來會被遮掩的每個名稱引入一個using聲明式,否則某些你希望繼承的名稱會被遮掩。
如果我們隻想唯一繼承上述例子中base class中的mf1的無參數版本,使用using聲明式是沒有用的,需要從新定義一個轉交函數。
class Base{
private:
int x;
public:
virtual void mf1 () =0;
virtual void mf1(int);
...
};
class Derived: public Base {
public:
virtual void mf1()//轉交函數,定義為inline
{ Base::mf1(); }
...
};
Dervied d;
int x;
d.mf1(); //ok,調用Derived::mf1
d.mf1(x); //worry,Base::mf1被Derived::mf1遮掩了
inline轉交函數的目的是将繼承而得的名稱彙入到derived class作用域内。
當繼承結合template時又會出現其他的問題。
請記住:
- derived class内的名稱會遮掩base class内的名稱。在public繼承下從沒有人希望如此。
- 為了讓被遮掩的名稱重見天日,可以使用using聲明式或者轉交函數。
條款34——區分接口和實作繼承
public繼承由兩部分組成:函數接口繼承和函數實作繼承。
一個例子用于區分繼承時繼承接口、繼承實作或兩者都繼承但希望能覆寫繼承的實作,希望同時繼承但不允許覆寫:
class Shape{
public:
virtual void draw() const =0;
virtual void error(const std::string& msg);
int objectID() const;
...
};
class Rectangle: public Shape {...};
class Ellipse: public Shape {...};
- 成員函數的接口總是會被繼承。
- 聲明一個pure virtual函數的目的是讓derived class隻繼承函數接口;pure virtual函數一般不想要逃提供實作,如果提供了,調用他的唯一途徑是明确指出其class的名稱。derived class必須提供實作。
Shape* ps = new Shape;//worry,Shape是抽象基類
Shape* ps1 = new Rectangle;//ok
ps1->draw();//Rectangle的draw
Shape* ps2 = new Ellipse;
ps1->Shape::draw();//調用Shape::draw
ps2->Shape::draw();//調用Shape::draw
- 聲明impure virtual函數(非純虛函數,但是是虛函數)的目的是讓derived class繼承該函數的接口和預設實作。可以讓不同的派生類接口一樣,但是行為不一樣。derived class會提供不同的實作覆寫他或者如果需要與基類相同的實作這需要定義一個同名函數調用基類的函數;derived class會覆寫base class中的實作。
- 聲明non-virtual 函數的目的是為了讓derived classes繼承函數的接口及一份強制性實作。 聲明為non-virtual 函數是希望在derived classes絕不會重新定義該函數,本身行為不改變。
請記住:
- 接口繼承和實作繼承不同。在public繼承之下,dervied class總是繼承base class的接口。
- pure virtual 函數隻具體指定接口繼承。
- 簡樸的(非純)impure virtual 函數具體指定接口的繼承及預設實作繼承。
- non-virtual 函數具體指定接口的繼承以及強制性實作繼承。
條款35——考慮virtual函數以外的其他選擇
由Non-Virtual Interface手法實作Template 模式
Non-Virtual Interface(NVI)手法:通過public non-virtual 成員函數間接調用private virtual 函數
class GameCharacter{
public:
int healthValue() const //這裡聲明為inline隻是為了友善展示,并不是真的inline,下同
{
...
int retVal = doHealthValue();
...
return retVal;
}
...
private:
virtual int doHealthValue() const
{...}
};
通常稱這個non-virtual函數為virtual函數的外覆器(wrapper)。
NVI手法涉及在derived class内重新定義private virtual函數。
由Function Pointers 實作Strategy模式
将算法與類型分開,上述例子我們可以給每一個人物構造函數接受一個指針,該指針指向一個計算健康指數的函數,例子如下:
class GameCharcter;//前置聲明
int defaultHealthCalc(const GameCharcter& gc);
class GameCharcter{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharcter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
{}
int hueathValue() const
{return healthFunc(*this);}
...
private:
HealthCalcFunc healthFunc(*this);
};
運用函數指針替換virtual函數,其優點是每個對象可各自擁有自己的方法以及在運作期可以改變自己的方法,缺點是如果方法需要通路class的non-public部分需要降低封裝性實作。
由tr1::function完成Strategy模式
可調用對象:函數,函數指針,lambda表達式,bind建立的對象,以及重載了函數調用符的類。
class GameCharcter;//前置聲明
int defaultHealthCalc(const GameCharcter& gc);
class GameCharcter{
public:
//HealthCalcFunc可以是任何可調用對象,可被調用并接受任何相容GameCharacter之物,傳回任何相容于int的東西
typedef std::tr1::function<int (const GameCharcter&)> HealthCalFunc;
explicit GameCharcter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
{}
int hueathValue() const
{return healthFunc(*this);}
...
private:
HealthCalcFunc healthFunc(*this);
};
與之前相同,這裡的tr1::function對象相當于一個指向函數的泛化指針。
古典的Strtegy模式
class GameCharacter;
class HealthCalcFunc{
public:
virtual int calc(const GameCharater& gc) const
{...}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharater(HealthCalcFunc* phcf = &defaultHealthCalc): pHealthCalc(phcf)
{}
int healthValue() const { return pHealthCalc->calc(*this); }
...
private:
HealthCalcFunc* pHealthCalc;
};
請記住:
- virtual函數的替代方案包括NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式。
- 将機能從成員函數移到class外部函數,帶來的一個缺點是,非成員函數無法通路class的non-public成員。
- tr1::function 對象的行為就像一般函數指針。這樣的對象可以接納“與給定之目标簽名式(target signature)相容”的所有可調用物。
條款36——絕不重新定義繼承而來的non-virtual函數
non-virtual函數是靜态綁定的,當聲明一個類型為指向基類的指針p時,p調用的non-virtual函數都是基類的版本,即使p指向的是一個派生類對象。
virtual函數是動态綁定,當一個指針不管是類型是指向基類的指針還是指向派生類的指針,其調用virtual函數的版本是根據其所指向的對象來的。
基類指針可以指向基類對象和派生類對象,但派生類指針隻能指向派生類對象,如果想用派生類指針指向基類指針必須進行轉型操作;指向基類對象的基類指針可以調用基類中的所有公有成員,指向派生類對象的基類指針可以調用繼承來的基類公有成員;當基類和派生類定義了同名函數且為virtual時,通過基類指針調用該函數時到底調用哪一個版本取決于該基類指針指向的是基類對象還是派生類對象,指向基類對象則調用基類中定義的版本。當基類和派生類定義了同名函數且為non-virtual時,派生類對象中的版本會遮掩基類的版本。
請記住:
- 絕對不要重新定義繼承而來的non-virtual函數。
條款37——絕不重新定義繼承而來的預設參數值
預設參數,就是在聲明函數的某個參數的時候為之指定一個預設值,在調用該函數的時候如果采用該預設值,你就無須指定該參數。帶預設值的參數必須放在參數表的最後面。
靜态類型(static type): 它在程式中被聲明的類型。
動态類型(dynamic type):目前所指對象的類型,動态類型表現出一個對象将會有什麼行為。
class Shape{
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const =0;
....
};
class Rectangle: public Shape {
public:
//注意,賦予不同的預設參數值,十分糟糕。
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color ) const;
//請注意,以上這麼寫組當客戶以對象調用此函數時一定要指定參數因為靜态綁定下這個函數
//不從base class繼承預設參數值。
//若以指針或引用調用此函數,可以不指定參數,因為動态綁定下這個函數會從其base繼承預設參數值。
};
Shape* ps; //靜态類型為Shape*
Shape* pc = new Circle;//靜态類型為Shape*
Shape* pr = new Rectangle;//靜态類型為Shape*
ps = pc;//ps的動态類型如今為Circle*
ps = pr;//ps的動态類型如今為Rectangle*
virtual函數系動态綁定而來,意思是調用一個virtual函數時,究竟調用哪一版本的函數實作代碼,取決于發出調用的那個對象的動态類型。
pc->draw(Shape::Red);//調用的是Circle::draw(Shape::Red)
ps->draw(Shape::Red);//調用的是Rectangle::draw(Shape::Red)
virtual函數是動态綁定,而預設參數值是靜态綁定的。這意味着你可能在調用一個定義域derived class内的virtual函數時使用的是base class中所指定的預設參數值。
pr->draw();
這裡本來希望預設值為Green,但實際上使用的卻是base中指定的預設參數值Red;
為了解決上述問題,可以采用NVI手法:令base class 内的一個public non-virtual 函數調用private virtual函數,private virtual函數可以被重新定義。這裡讓public non-virtual函數指定預設參數值,而private virtual函數負責真正的工作。
class Shape{
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const
{
doDraw(color);
}
....
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Circle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
};
請記住:
- 絕對不要重新定義一個繼承而來的預設參數值,因為預設參數值是靜态綁定的,而virtual函數是動态綁定的(virtual函數是你唯一需要覆寫的東西)。
條款38——通過複合塑模出has-a或根據某物實作出(Model “has-a” or “is-implemented-in-terms-of” through composition)
複合(composition):某種類型的對象含有它種類型的對象。複合又被稱為包含、聚合、内嵌等等。其含義是“has-a”或“is-implemented-in-terms-of(根據某物實作出)”。
請記住:
- 複合(composition)的意義與public繼承完全不同;
- 在應用域(application domain),複合意味着has-a(有一個);在實作域(implementation domian),複合意味着“is-implemented-in-terms-of(根據某物實作出)”。
條款39——明智而審慎地使用private繼承
private 繼承的兩條規則:
- class之間的繼承關系是private的話,編譯器不會自動将一個derived class對象轉換為一個base class對象
-
由private base class 繼承而來的所有成員,在derived class中都會變成private屬性,即使他們在base class中是protected或public;
private繼承意味着is-implemented-in-terms-of(根據某物實作出)。其意義與複合一樣,我們應當盡量采用複合,必要時(當有protected 成員或virtual函數牽扯進來時)才使用private繼承。
EBO(empty base optimization,空白基類最優化),EBO隻在單一繼承下才可行。這種情況下才有可能使用private繼承一個空類,以達到EBO。
請記住:
- Private繼承意味着is-implemented-in-terms-of(根據某物實作出)。它通常比複合的級别低。但是當derived class需要通路 protected base class的成員,或需要重新定義繼承而來的virtual函數是,這麼設計是合理的。
- 和複合不同,private繼承可以造成empty base 最優化。這對緻力于“對象尺寸最小化”的程式庫開發者而言,可能很重要。
條款40——明智而審慎地使用多重繼承
多重繼承(multiple inheritance; MI)。使用MI可能導緻程式從一個以上的base class繼承相同名稱(如函數,typedef),這也會導緻更多的歧義。為了解決歧義問題,你必須明确指出你調用的是哪一個base class内的函數。
class BorrowableItem{
public:
void CheckOut();
...
};
class ElectronicGadget{
private:
void CheckOut();
...
};
class MP3Plaryer: public BorrowableItem, public ElectronicGadget
{...};
MP3Plaryer mp;
mp.CheckOut();//這裡會出現歧義不知道調用哪一個baseclass中的CheckOut()函數;
mp.BorrowableItem::CheckOut();//指明你調用的是哪一個base class内的函數,這裡是BorrowableItem這一個基類
多重繼承中可能碰到這種情況,繼承一個以上的基類,這些基類又有更進階的同一個基類,即“鑽石型多重繼承”
class File {...};
class InputFile: public File {...};
class OutputFile: public File {...};
class IOFile:public InputFile
public InputFile
{...};
//這是如果File有某個成員變量,IOFile就會有兩份該成員變量,這是錯誤的
為了解決上述問題,令帶有此資料的class為virtual base class,令所有直接繼承自它的class采用“virtual繼承”。
```cpp
class File {...};
class InputFile: virtual public File {...};//virtual繼承
class OutputFile: virtual public File {...};//virtual繼承
class IOFile:public InputFile
public InputFile
{...};
關于virtual base class的忠告:盡量不要使用virtual bases,平常請使用non-virtual 繼承;如果必須使用virtual base class,則盡可能避免在其中放置資料。
請記住:
- 多重繼承比單一繼承複雜。它可能導緻新的歧義性,以及對virtual繼承的需要。
- virtual繼承會增加大小、速度、初始化(及指派)複雜度等等的成本。如果base virtual class不帶任何資料,将是最具有實用價值的情況。
- 多重繼承的确有正當用途。其中一個情節涉及“public 繼承某個Interface class”和“private 繼承某個協助實作的class”的兩相組合的情況。