Item 32: 確保你的public inheritance 塑模出 "is-a"關係
如果你寫了一個 class D ("Derived") 從 class B ("Base") 公開繼承,你就是在告訴 C++ 編譯器(以及你的代碼的讀者)每一個類型爲 D 的對象也是一個類型爲 B 的對象,但是反之則不然。你就是在說 B 描繪了一個比 D 更一般的概念,D 描述了一個比 B 更特殊的概念。你就是在聲稱一個類型爲 B 的對象可以使用的任何地方,一個類型爲 D 的對象一樣可以使用,因爲每一個類型爲 D 的對象也就是一個類型爲 B 的對象。另一方面,如果你需要一個類型爲 D 的對象,一個類型爲 B 的對象則不行:每一個 D 都是一個 B,但是反之則不然。
public inheritance 斷言,適用於 base class objects(基類對象)的每一件事——每一件事!——也適用於 derived class objects(派生類對象)。在我們考慮繼承關係的時候,必須考慮這一點。比較典型的是矩形和正方形,通常我們會直覺的認爲正方形應該從矩形繼承。但問題出來了:
考慮如下代碼:
class Rectangle {
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int height() const; // return current values
virtual int width() const;
...
};
void makeBigger(Rectangle& r) // function to increase r's area
{
int oldHeight = r.height();
r.setWidth(r.width() + 10); // add 10 to r's width
assert(r.height() == oldHeight); // assert that r's
} // height is unchanged
很清楚,斷言應該永遠不會失敗。makeBigger 僅僅改變了 r 的寬度,它的高度始終沒有變化。
現在,考慮以下代碼,使用 public inheritance 使得 squares 可以像 rectangles 一樣進行處理:
class Square: public Rectangle {...};
Square s;
...
assert(s.width() == s.height()); // this must be true for all squares
makeBigger(s); // by inheritance, s is-a Rectangle,
// so we can increase its area
assert(s.width() == s.height()); // this must still be true
// for all squares
和剛纔那個一樣明顯,第二個斷言也應該永遠不會失敗。根據定義,正方形的寬度和高度是相等的。
現在,如果在square的實例上調用setWidth或setHeight,就出現問題了。
總結:
在確定public繼承之前,請確定適用於base class的每一件事也一定適用於derived class,因爲每一個derived class對象也都是一個base class對象。
Item 33: 避免覆蓋(hiding)通過繼承而來的名稱
編譯器的搜索順序:假設有以下代碼
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf4();
...
};
void Derived::mf4()
{
...
mf2();
...
}
當編譯器看到一個名爲mf2的使用,它就必須斷定它指涉什麼。它通過搜索名爲 mf2 的某物的定義的作用域來做這件事。首先它在 local 作用域中搜索(也就是 mf4 的作用域),但是它沒有找到被稱爲 mf2 的任何東西的聲明。然後它搜索它的包含作用域,也就是 class Derived 的作用域。它依然沒有找到叫做 mf2 的任何東西,所以它上移到它的上一層包含作用域,也就是 base class 的作用域。在那裏它找到了名爲 mf2 的東西,所以搜索停止。如果在 Base 中沒有 mf2,搜索還會繼續,首先是包含 Base 的 namespace(s)(如果有的話),最後是 global 作用域。
因此,Derived class中的同名成員會覆蓋base class中的成員,包括變量或函數。對成員函數來說,被覆蓋與否只與函數名相關,與函數參數,函數類型(pure virtual, virtual, non-virtual)等無關。
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();
...
};
base class 中的所有名爲 mf1 和 mf3 的函數被 derived class 中的名爲 mf1 和 mf3 的函數覆蓋。從名字搜索的觀點看,Base::mf1 和 Base::mf3 不再被 Derived 繼承!
Derived d;
int x;
...
d.mf1(); // fine, calls Derived::mf1
d.mf1(x); // error! Derived::mf1 hides Base::mf1
d.mf2(); // fine, calls Base::mf2
d.mf3(); // fine, calls Derived::mf3
d.mf3(x); // error! Derived::mf3 hides Base::mf3
就像你看到的,即使 base 和 derived classes 中的函數具有不同的參數類型,它也同樣適用,而且不管函數是 virtual 還是 non-virtual,它也同樣適用。覆蓋只與名字相關!
解決覆蓋的辦法:
1.使用using聲明式
class Derived: public Base {
public:
using Base::mf1; // make all things in Base named mf1 and mf3
using Base::mf3; // visible (and public) in Derived's scope
virtual void mf1();
void mf3();
void mf4();
...
};
現在 inheritance 就可以起到預期的作用:
Derived d;
int x;
...
d.mf1(); // still fine, still calls Derived::mf1
d.mf1(x); // now okay, calls Base::mf1
d.mf2(); // still fine, still calls Base::mf2
d.mf3(); // fine, calls Derived::mf3
d.mf3(x); // now okay, calls Base::mf3
這意味着如果你從一個帶有重載函數的 base class 繼承,而且你只想重定義或替換它們中的一部分,你需要爲每一個你不想覆蓋的名字使用 using declaration。如果你不這樣做,一些你希望繼承下來的名字會被覆蓋。
3.使用轉交函數: (該方法通常適用於private繼承,因爲對public繼承來說,任何一件能對base class做的事情也應該可以對derived class生效!!參見條款32)
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
... // as before
};
class Derived: private Base {
public:
virtual void mf1() // forwarding function; implicitly
{ Base::mf1(); } // inline (see Item 30)
...
};
...
Derived d;
int x;
d.mf1(); // fine, calls Derived::mf1
d.mf1(x); // error! Base::mf1() is hidden
總結:
derived classes 中的名字覆蓋 base classes 中的名字,在 public inheritance 中,這從來不是想要的。
爲了使隱藏的名字重新可見,使用 using declarations 或者 forwarding functions(轉調函數)。