(1)相同的範圍(在同一個類中);
(2)函數名字相同;
(3)參數不同(類型或者個數不同);
(4)virtual 關鍵字可有可無。
覆蓋是指派生類函數覆蓋基類函數,特徵是
(1)不同的範圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字。
“隱藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下
(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
(2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual 關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)
3。覆蓋:調用派生類
#include<iostream>
#include<string>
using namespace std;
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
void j(int x){cout<<"Base::j(int)"<<x<<endl;}
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
void j(float x){cout<<"Derived::j(float)"<<x<<endl;}
};
void main()
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
//覆蓋情況的方法f,pb不能找到自己的f方法
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
//隱藏情況的方法g,pb可以找到自己的g方法
pb->g(3.14f); // Base::g(float) 3.14 (運用基類類型的指針變量來實現是可以的)
pd->g(3.14f); // Derived::g(int) 3 (受到自動類型轉換的影響)
//隱藏情況的方法h,pb可以找到自己的h方法
pb->h(3.14f); // Base::h(float) 3.14
pd->h(3.14f); // Derived::h(float) 3.14
//隱藏情況的方法j,pb可以找到自己的j方法
pb->j(3.14f);//Base::j(int) 3(受到自動類型轉換的影響)
pd->j(3.14f);//Derived::j(float)3.14
}
接下來的幾個例子具體說明一下什麼叫隱藏
例1
#include <iostream>
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
//新的函數版本,基類所有的重載版本都被屏蔽,在這裏,我們稱之爲函數隱藏hide
//派生類中有基類的同名函數的聲明,則基類中的同名函數不會作爲候選函數,即使基類有不同的參數表的多個版本的重載函數。
void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun(1,2);
//下面一句錯誤,故屏蔽掉(不使用指針是無法訪問基類被隱藏的函數的)
//d.fun();error C2660: 'fun' : function does not take 0 parameters
return 0;
}
//函數名相同但是參數不同,所以是基類的函數與派生類中同名函數被隱藏。
例2
#include <iostream>
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
void fun(){cout << "Derive::fun()" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();
//下面一句錯誤,故屏蔽掉
//d.fun(1);error C2660: 'fun' : function does not take 1 parameters
return 0;
}
//覆蓋override基類的其中一個函數版本,同樣基類所有的重載版本都被隱藏hide
//派生類中有基類的同名函數的聲明,則基類中的同名函數不會作爲候選函數,即使基類有不同的參數表的多個版本的重載函數
例3
#include <iostream>
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
using Basic::fun;
void fun(){cout << "Derive::fun()" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();//正確
d.fun(1);//正確
return 0;
}
/*
輸出結果
Derive::fun()
Base::fun(int i)
Press any key to continue
*/
//hide 和 顯式聲明基類名字空間作用域
例4
#include <iostream>
using namespace std;
class Basic{
public:
void fun(){cout << "Base::fun()" << endl;}//overload
void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
using Basic::fun;
void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
Derive d;
d.fun();//正確
d.fun(1);//正確
d.fun(1,2);//正確
return 0;
}
/*
輸出結果
Base::fun()
Base::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
//同例3
如果基類有某個函數的多個重載(overload)版本,而你在派生類中重寫(override)了基類中的一個或多個函數版本,或是在派生類中重新添加了新的函數版本(函數名相同,參數不同),則所有基類的重載版本都被屏蔽,在這裏我們稱之爲隱藏hide。所以,在一般情況下,你想在派生類中使用新的函數版本又想使用基類的函數版本時,你應該在派生類中重寫基類中的所有重載版本。你若是不想重寫基類的重載的函數版本,則你應該使用例3或例4方式,顯式聲明基類名字空間作用域。
事實上,C++編譯器認爲,相同函數名不同參數的函數之間根本沒有什麼關係,它們根本就是兩個毫不相關的函數。只是C++語言爲了模擬現實世界,爲了讓程序員更直觀的思維處理現實世界中的問題,才引入了重載和覆蓋的概念。重載是在相同名字空間作用域下,而覆蓋則是在不同的名字空間作用域下,比如基類和派生類即爲兩個不同的名字空間作用域。在繼承過程中,若發生派生類與基類函數同名問題時,便會發生基類函數的隱藏。當然,這裏討論的情況是基類函數前面沒有virtual 關鍵字。在有virtual 關鍵字關鍵字時的情形我們另做討論。
繼承類重寫了基類的某一函數版本,以產生自己功能的接口。此時C++編繹器認爲,你現在既然要使用派生類的自己重新改寫的接口,那我基類的接口就不提供給你了(當然你可以用顯式聲明名字空間作用域的方法,而不會理會你基類的接口是有重載特性的。若是你要在派生類裏繼續保持重載的特性,那你就自己再給出接口重載的特性吧。所以在派生類裏,只要函數名一樣,基類的函數版本就會被無情地屏蔽。在編繹器中,屏蔽是通過名字空間作用域實現的。
所以,在派生類中要保持基類的函數重載版本,就應該重寫所有基類的重載版本。重載只在當前類中有效,繼承會失去函數重載的特性。也就是說,要把基類的重載函數放在繼承的派生類裏,就必須重寫。
例5
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun() { cout << "Base::fun()" << endl; }//overload
virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload
};
class Derive : public Base{
public:
void fun() { cout << "Derive::fun()" << endl; }//override
void fun(int i) { cout << "Derive::fun(int i)" << endl; }//override
void fun(int i,int j){ cout<< "Derive::fun(int i,int j)" <<endl;}//overload
};
int main()
{
Base *pb = new Derive();
pb->fun();
pb->fun(1);
//下面一句錯誤,故屏蔽掉
//pb->fun(1,2);virtual函數不能進行overload,error C2661: 'fun' : no overloaded function takes 2 parameters
cout << endl;
Derive *pd = new Derive();
pd->fun();
pd->fun(1);
pd->fun(1,2);//overload
delete pb;
delete pd;
return 0;
}
/*
輸出結果
Derive::fun()
Derive::fun(int i)
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/
例6-1
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
class Derive : public Base{};
int main()
{
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
delete pb;
return 0;
}
例6-2
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
class Derive : public Base{
public:
void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
int main()
{
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
delete pb;
return 0;
}
例7-1
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
class Derive : public Base{
public:
void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
};
int main()
{
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
delete pb;
return 0;
}
例7-2
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
class Derive : public Base{
public:
void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
int main()
{
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
delete pb;
return 0;
}
例8
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
class Derive : public Base{
public:
void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
void fun(char c){ cout <<"Derive::fun(char c)"<< endl; }
void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
int main()
{
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun('a');//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
Derive *pd =new Derive();
pd->fun(1);//Derive::fun(int i)
//overload
pd->fun('a');//Derive::fun(char c)
//overload
pd->fun(0.01);//Derive::fun(double d)
delete pb;
delete pd;
return 0;
}
例6-1和例7-1很好理解,我把這兩個例子放在這裏,是讓大家作一個比較擺了,也是爲了幫助大家更好的理解:
n 例7-1中,派生類沒有覆蓋基類的虛函數,此時派生類的vtable中的函數指針指向的地址就是基類的虛函數地址。
n 例8-1中,派生類覆蓋了基類的虛函數,此時派生類的vtable中的函數指針指向的地址就是派生類自己的重寫的虛函數地址。
在例7-2和8-2看起來有點怪怪,其實,你按照上面的原則對比一下,答案也是明朗的:
n 例7-2中,我們爲派生類重載了一個函數版本:void fun(double d) 其實,這只是一個障眼法。我們具體來分析一下,基類共有幾個函數,派生類共有幾個函數:
類型 |
基類 |
派生類 |
Vtable部分 |
void fun(int i) |
指向基類版的虛函數void fun(int i) |
靜態部分 |
void fun(double d) |
我們再來分析一下以下三句代碼
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
這第一句是關鍵,基類指針指向派生類的對象,我們知道這是多態調用;接下來第二句,運行時基類指針根據運行時對象的類型,發現是派生類對象,所以首先到派生類的vtable中去查找派生類的虛函數版本,發現派生類沒有覆蓋基類的虛函數,派生類的vtable只是作了一個指向基類虛函數地址的一個指向,所以理所當然地去調用基類版本的虛函數。最後一句,程序運行仍然埋頭去找派生類的vtable,發現根本沒有這個版本的虛函數,只好回頭調用自己的僅有一個虛函數。
這裏還值得一提的是:如果此時基類有多個虛函數,此時程序編繹時會提示”調用不明確”。示例如下
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
virtual void fun(char c){ cout <<"Base::fun(char c)"<< endl; }
};
class Derive : public Base{
public:
void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
int main()
{
Base *pb = new Derive();
pb->fun(0.01);//error C2668: 'fun' : ambiguous call to overloaded function
delete pb;
return 0;
}
好了,我們再來分析一下例7-2。
n 例7-2中,我們也爲派生類重載了一個函數版本:void fun(double d) ,同時覆蓋了基類的虛函數,我們再來具體來分析一下,基類共有幾個函數,派生類共有幾個函數:
類型 |
基類 |
派生類 |
Vtable部分 |
void fun(int i) |
void fun(int i) |
靜態部分 |
void fun(double d) |
從表中我們可以看到,派生類的vtable中函數指針指向的是自己的重寫的虛函數地址。
我們再來分析一下以下三句代碼
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
第一句不必多說了,第二句,理所當然調用派生類的虛函數版本,第三句,嘿,感覺又怪怪的,其實呀,C++程序很笨的了,在運行時,埋頭闖進派生類的vtable表中,隻眼一看,靠,競然沒有想要的版本,真是想不通,基類指針爲什麼不四處轉轉再找找呢?呵呵,原來是眼力有限,基類年紀這麼老了,想必肯定是老花了,它那雙眼睛看得到的僅是自己的非Vtable部分(即靜態部分)和自己要管理的Vtable部分,派生類的void fun(double d)那麼遠,看不到呀!再說了,派生類什麼都要管,難道派生類沒有自己的一點權力嗎?哎,不吵了,各自管自己的吧^_^
唉!你是不是要嘆氣了,基類指針能進行多態調用,但是始終不能進行派生類的重載調用啊(參考例5)~~~
再來看看例8,本例的效果同例5,異曲同工。想必你理解了上面的這些例子後,這個也是小case了。
小結:
重載overload是根據函數的參數列表來選擇要調用的函數版本,而多態是根據運行時對象的實際類型來選擇要調用的虛virtual函數版本,多態的實現是通過派生類對基類的虛virtual函數進行覆蓋override來實現的,若派生類沒有對基類的虛virtual函數進行覆蓋override的話,則派生類會自動繼承基類的虛virtual函數版本,此時無論基類指針指向的對象是基類型還是派生類型,都會調用基類版本的虛virtual函數;如果派生類對基類的虛virtual函數進行覆蓋override的話,則會在運行時根據對象的實際類型來選擇要調用的虛virtual函數版本,例如基類指針指向的對象類型爲派生類型,則會調用派生類的虛virtual函數版本,從而實現多態。
使用多態的本意是要我們在基類中聲明函數爲virtual,並且是要在派生類中覆蓋override基類的虛virtual函數版本,注意,此時的函數原型與基類保持一致,即同名同參數類型;如果你在派生類中新添加函數版本,你不能通過基類指針動態調用派生類的新的函數版本,這個新的函數版本只作爲派生類的一個重載版本。還是同一句話,重載只有在當前類中有效,不管你是在基類重載的,還是在派生類中重載的,兩者互不牽連。如果明白這一點的話,在例5、例8中,我們也會對其的輸出結果順利地理解。
重載是靜態聯編的,多態是動態聯編的。進一步解釋,重載與指針實際指向的對象類型無關,多態與指針實際指向的對象類型相關。若基類的指針調用派生類的重載版本,C++編繹認爲是非法的,C++編繹器只認爲基類指針只能調用基類的重載版本,重載只在當前類的名字空間作用域內有效,繼承會失去重載的特性,當然,若此時的基類指針調用的是一個虛virtual函數,那麼它還會進行動態選擇基類的虛virtual函數版本還是派生類的虛virtual函數版本來進行具體的操作,這是通過基類指針實際指向的對象類型來做決定的,所以說重載與指針實際指向的對象類型無關,多態與指針實際指向的對象類型相關。
最後闡明一點,虛virtual函數同樣可以進行重載,但是重載只能是在當前自己名字空間作用域內有效(請再次參考例5)。
原文鏈接:http://blog.163.com/hi_qiqiy@126/blog/static/1440667912010111544228731/