C++ 繼承關係的作用域 和 注意事項

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++》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章