1、類繼承關係的作用域
我們都知道在如下的代碼中:
int x; //全局變量
void someFunc()
{
double x; //局部變量
std::cin >> x; //讀取一個新值賦予 local 變量 x
}
同名的 x 變量,作用域不同,而根據 C++的名稱遮掩規則:內層作用域的名稱會遮蓋外層作用域的名稱。,在賦值語句時,涉及的是局部變量 x。如下圖所示:
當編譯器處於 someFunc 的作用域內,並遭遇名稱 x 時,它在 local 作用域內查找是否有什麼東西帶着這個名稱。如果找到就停止,如果找不到就去更大的作用域中查找。
而 在繼承關係中,派生類的作用域被嵌套在基類的作用域內。我們假設有兩個類的繼承關係如下所示:
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::mf1 和 Base::mf3 不在被 Derived 繼承。如下:
Derived d;
int x;
d.mf1(); //沒有問題,調用 Derived::mf1
d.mf1(x); //錯誤! 因爲 Derived::mf1 遮掩了 Base::mf1
d.mf2(); //沒有問題,調用 Base::mf2
d.mf3(); //沒有問題,調用 Derived::mf3
d.mf3(x); //錯誤!因爲 Derived::mf3 遮掩了 Base::mf3
如你所見,即使 base class 和 derived class 內的函數有不同的參數類型也遮掩。這就是之前整理 隱藏和覆蓋 的深層原理。
而如果我們想讓 derived class 對 base class 的函數可見,一種方法是:可以使用我們之前整理的 using ,則上面的代碼就可編譯通過。具體的使用如下所示:
class Derived : public Base
{
public:
using Base::mf1; //讓 Base class 內名爲 mf1 和 mf3 的所有東西
using Base::mf3; //在 Derived class 作用域中都可見(並且public)
....
};
第二種方法是:使用轉交函數,顯示調用基類中的函數。因爲 using 聲明式會令所有的同名函數都可見,而且老舊編譯器並不支持 using 聲明式。具體使用如下:
class Base //與之前一樣
{
....
};
class Derived : private Base
{
public:
virtual void mf1() //轉交函數
{
Base::mf1();
};
};
2、絕不重新定義繼承而來的 non-virtual 函數
假設有兩個類的繼承關係如下所示:
class B
{
public:
void mf();
...
};
class D : public B
{
...
};
D x; //x 是一個類型爲 D 的對象
B* pb = &x; //獲得一個指向 x 的指針
pb->mf();
D* pd = &x; //獲得一個指向 x 的指針
pd->mf();
看代碼,因爲基類中沒有使用 virtual 修飾 mf() ,上述代碼中 pb 和 pd 調用 mf 想實現的效果應該是一樣的。但是事實可能不是如此。
這裏先說一下幾個概念:
靜態類型:被聲明時所採用的類型。
動態類型:用於指針和引用,是指目前所指對象的類型。
靜態綁定:調用函數時只與靜態類型有關。
動態綁定:調用函數時,究竟調用哪一份,取決於發出調用的那個對象的動態類型。
而在 C++ 中 virtual 函數是 動態綁定,而 non-virtual 函數 靜態綁定。通過上一節整理,我們知道派生類與基類中的同名函數會發生遮掩,所以如果 D 類中有一個 mf 的同名函數,則上述代碼就可能表現不一樣。
如果是 virtual 函數,總是會調用 D 類 中的 mf 函數。
如果是 non-virtual 函數,調用哪一個函數,決定因素不在原始對象類型,而是指向該對象指針的類型,如果指針類型爲 D*,則調用 D::mf,如果指針類型爲 B*,則調用 B::mf。
所以記住:任何情況下都不該重新定義一個繼承而來的 non-virtual 函數。
3、絕不重新定義繼承而來的缺省參數值
上面一節我們排除了 non-virtual 函數,這裏就可以只看 virtual 函數了。
本條成立的理由是:virtual 函數是動態綁定,而缺省參數值確實靜態綁定。
如果你寫出以下代碼:
//一個用來描述幾何形狀的 class
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;
};
Shape* pr = new Rectangle;
pr->draw(); //調用 Rectangle::draw(Shape::Red);
pr 的動態類型是 Rectangle,所以調用 Rectangle 的 virtual 函數,但是 pr 的靜態類型是 Shape ,所以調用的缺省參數是 Shape::Red**。
C++ 這麼設計是爲了 運行期效率。如果缺省參數值是動態綁定,編譯器就必須有某種辦法在運行期爲 virtual 函數決定適當的參數缺省值。這比目前實行的 “在編譯器決定” 的機制更慢而且更復雜。
上述代碼應該寫爲:
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 = Red) const;
};
所以請記住:絕對不要重新定義一個繼承而來的缺省參數值,因爲缺省參數值都是靜態綁定,而 virtual 函數卻是動態綁定。
感謝大家,我是假裝很努力的YoungYangD(小羊)。
參考資料:
《Effective C++》