前幾天面試時被問及C++中的覆蓋、隱藏,概念基本答不上來,只答了怎麼用指針實現多態,也還有遺漏。最終不歡而散。回來後在網上查找學習了一番,做了這個總結。
一、重載(overload)
指函數名相同,但是它的參數表列個數或順序,類型不同。但是不能靠返回類型來判斷。
(1)相同的范圍(在同一個作用域中) ;
(2)函數名字相同;
(3)參數不同;
(4)virtual 關鍵字可有可無。
(5)返回值可以不同;
二、重寫(也稱為覆蓋 override)
是指派生類重新定義基類的虛函數,特征是:
(1)不在同一個作用域(分別位於派生類與基類) ;
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有 virtual 關鍵字,不能有 static 。
(5)返回值相同(或是協變),否則報錯;<—-協變這個概念我也是第一次才知道…
(6)重寫函數的訪問修飾符可以不同。盡管 virtual 是 private 的,派生類中重寫改寫為 public,protected 也是可以的
三、重定義(也成隱藏)
(1)不在同一個作用域(分別位於派生類與基類) ;
(2)函數名字相同;
(3)返回值可以不同;
(4)參數不同。此時,不論有無 virtual 關鍵字,基類的函數將被隱藏(注意別與重載以及覆蓋混淆) 。
(5)參數相同,但是基類函數沒有 virtual關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆) 。
#include <iostream>
using namespace std;
class SParent
{
public:
SParent( ){};
SParent( const SParent &p )
{
cout << "parent copy construct" << endl;
}
int add( int a,int b )
{
cout << "parent int add" << endl;
return a + b;
}
double add( double a,double b )
{
cout << "parent double add" << endl;
return a + b;
}
virtual int dec( int a,int b )
{
cout << "parent int dec" << endl;
return a - b;
}
};
class SChild : public SParent
{
public:
//using SParent::add;
float add( float a,float b )
{
cout << "child float add" << endl;
return a + b;
}
int dec(int a, int b)
{
cout << "child int dec" << endl;
return a - b;
}
};
int main()
{
/* 測試重載 */
SParent parent;
parent.add( 3,5 );
parent.add( (double)3,(double)5 );
cout << endl;
/* 測試覆蓋 */
SChild *pchild = (SChild *)new SParent();/* 基類強轉為子類...危險...,用dynamic_cast轉換也不行 */
pchild->dec( 10,3 );
SParent *pparent = new SChild();
pparent->dec( 11,3 );
cout << endl;
/* 測試隱藏 */
SChild child;
child.add( (int)3,(int)5 );
cout << endl;
/* 測試函數表 */
((SParent *)NULL)->add( 4,6 );
((SChild *)NULL)->add( 4,6 );
int a = 0;
((SChild *)&a)->add( 4,6 );
cout << endl;
/* 測試函數地址 */
((SParent)child).add( (int)4,(int)8 );
child.SParent::add( 3,5 );
return 0;
}
輸出結果:
parent int add
parent double add
parent int dec
child int dec
child float add
parent int add
child float add
child float add
parent copy construct
parent int add
parent int add
按 <RETURN> 來關閉窗口...
理解
int SParent::add(int a,int b)與double SParent::add( double a,double b )是重載
int SParent::add(int a,int b)與double SParent::add( double a,double b )都被子類SChild中的float SChild::add( float a,float b )隱藏
int SParent::dec( int a,int b )被子類SChild中的int SChild::dec( int a,int b )覆蓋
1.重載測試,簡單易懂,略過。
2.覆蓋測試。dec函數在基類、子類中同名同參,為虛函數,故稱覆蓋。
SChild *pchild = (SChild *)new SParent()創建的是一個基類對象,其函數表應該為
SParent *pparent = new SChild();創建一個子類對象,其函數表應該為
由上面的函數表可見,當發生覆蓋時,子類的函數名會把基類的同名函數覆蓋(這也就是為什麼叫覆蓋的原因吧)。這樣我們可以利用一個指向子類的基類指針實現多態。但重點只有一
個,就是函數表裡到底指向誰(不管這個指針經過轉換後是什麼類型的).故輸出分別為父類、子類。這是一個運行時多態。
3.隱藏測試
int SParent::add(int a,int b)與double SParent::add( double a,double b )都被子類SChild中的float SChild::add( float a,float b )覆蓋,是因為他們同名,而且在不同的作用域中(基類、子類作用域是不同的)。child.add( (int)3,(int)5 );這行代碼中,編譯器在子類中查找add函數,只找到了一個(基類的add(int a,int b)會被編譯根據隱藏規則略過),再根據隱式類型轉換發現該函數適用。如果無法隱式轉換,則編譯不過。隱藏的原因:防止隱式類型轉換造成錯誤。比如int也是可以轉換成char的,假如基類有一函數add(char a,char b),子類也有一函數add(double a,double b)。程序員想著在子類隱式把int轉換為double,但編譯器可能調的是基類的。這也防止了一些庫或封裝好的基類對程序員造成困擾。
像上面的代碼,如果你確實需要基類的函數,可以用using SParent:add。則把基類的add函數域擴大到了子類,構成重載。
4.函數表測試
上面我們說到函數表,這個是在編譯時定好的,程序運行時加載到內存中。這意味著我們可以直接通過地址去調用函數。所以((SChild *)NULL)->add( 4,6 );這種代碼也是能運行通過的。網上還有人通過計算直接取到了函數表的地址直接調用了。但這種代碼不安全不規范不說,還有個更大的問題。當成員函數裡需要調用成員變量時,通過這種假的對象指針肯定找不到成員變量表,直接訪問了非法內存。
5.函數地址測試
有了隱藏、覆蓋,哪麼我們要怎麼調用被隱藏、覆蓋的函數呢。下面有兩種方法:
((SParent)child).add( (int)4,(int)8 );
child.SParent::add( 3,5 );
第一種是比較低效的方法。事實上它是通過拷貝構造函數生成一個臨時的基類變量去調用基類的add函數。
第二種通過::指定域去訪問。這種方法是編譯器根據域直接找到了基類的函數地址,跟函數表沒有多大關系。