多態與虛函數
面向對象編程中,多態的含義是“一個接口,多種實現”。
多態分爲靜態多態和動態多態。靜態多態是通過模板化和重載技術來實現,在編譯的時候確定。動態多態通過虛函數和繼承關係來實現,執行動態綁定,在運行的時候確定。
C++中運行時的多態是指根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。
C++多態性是通過虛函數來實現的,虛函數允許子類重新定義成員函數,而子類重新定義父類方法稱爲覆蓋或重寫(override)。
虛函數:
用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
虛函數的作用就是實現動態綁定,也就是在程序的運行階段動態地選擇合適的成員函數。
具體的實現方式,在基類中定義了虛函數後,可以在基類的派生類中對虛函數重新定義,在派生類中重新定義的函數應與虛函數具有相同的形參個數和形參類型,以實現統一的接口,不同定義過程。
如果在派生類中沒有對虛函數重新定義,則它繼承其基類的虛函數,此時派生類也爲抽象類,不能實例化對象。
虛函數表:
當一個類含有一個乃至多個虛函數的時候,將在全局數據區(靜態區)中存儲該類相應的虛函數表vtbl,編譯器給類的每個對象添加一個相同隱藏的成員——虛函數表指針vptr,指向自身類的虛函數表。
虛函數表的大小在編譯時確定,不必動態分配內存空間進行存儲,因此不虛函數表不存在堆中,根據以上特徵,虛函數表類似於類中靜態成員變量。靜態成員變量也是全局共享,大小確定,所以虛函數表和靜態成員變量一樣,存放在全局數據區。
虛函數表中保存了類對象進行聲明的虛函數的地址,即虛函數表的元素是指向類成員函數的指針。也就是說我們可以通過vptr訪問虛函數表,進而訪問被聲明的虛函數的的地址,從而調用相應的虛函數。
同一個類的不同對象的vptr實際上指向同一張虛函數表。vptr的設定和重置都由每一個類的構造函數,析構函數和拷貝賦值運算符自動完成。一般來說,將在構造函數中進行虛表的創建和虛表指針的初始化。
基類的虛函數表和派生類的虛函數表分別爲保存在不同位置的兩個獨立數組,也就是說基類的隱藏成員和派生類的隱藏成員指向不同的地址。
如果派生類沒有重新定義基類的某個虛函數A,則派生類的虛函數表vtbl將保存基類的虛函數A的原始地址(此時派生類和基類的虛函數表中保存的虛函數A的地址是一樣的)。
如果派生類重寫了基類的某個虛函數B,則派生類的虛函數表vtbl將保存新的虛函數B的地址(此時的虛函數B其實有兩個版本,分別被基類和派生類的虛函數表分開保存)。
純虛函數:
純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型後加“=0”,即virtual void funtion1() = 0;
含有純虛函數的類爲抽象類,不能聲明對象,只是作爲基類爲派生類服務。除非在派生類中完全實現基類的所有虛函數,否則派生類也是抽象類,不能實例化對象。
抽象類不能定義對象,但是可以作爲指針或引用類型使用。
不能聲明虛函數:
常見的不能聲明爲虛函數的有:普通函數(非成員函數);靜態成員函數;內聯成員函數;構造函數;友元函數。
普通函數(非成員函數)只能被overload,不能被override,聲明爲虛函數也沒有意義。
靜態成員函數對於每個類來說只有一份代碼,所有的對象都共享這一份代碼,也沒有要動態綁定的必要性。
內聯函數在編譯時被展開,從而可減少函數調用花費的代價,虛函數是在運行時才進行動態綁定,從而使得繼承對象能夠準確的執行自己的動作。
構造函數目的是爲了生成對象時進行對象初始化,虛函數目的是在不同類型的對象中調用不同的方法以產生不同的動作,當對象還沒有生成時,虛函數是沒有意義的,而構造函數是爲了在對象還沒有生成時實例化對象。
友元函數不支持繼承,對於沒有繼承特性的函數就沒有虛函數的說法。
早綁定和晚綁定:
c++編譯器在編譯的時候,要確定每個對象調用的函數(非虛函數)的地址,這稱爲早期綁定,當我們將Son類對象的地址賦給指針pFather時,C++編譯器進行了類型轉換,此時C++編譯器認爲指針變量pFather保存的就是Father對象的地址,當在main函數中執行pFather->Say(),調用的是Father對象的Say函數。
從內存角度看:
前面輸出的結果是因爲編譯器在編譯的時候,就已經確定了對象調用的函數地址,要解決這個問題就要使用晚綁定,當編譯器使用晚綁定時候,就會在運行時再去確定對象的類型以及正確的調用函數,而要讓編譯器採用晚綁定,就要在基類中聲明函數時使用virtual關鍵字.一旦某個函數在基類中聲明爲virtual,那麼在所有的派生類中該函數都是virtual,而不需要再顯式地聲明爲virtual。
編譯器爲每個對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表,在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向了所屬類的虛表,從而在調用虛函數的時候,能夠找到正確的函數。由於pFather實際指向的對象類型是Son,因此vptr指向的Son類的vtable,當調用pFather->Son()時,根據虛表中的函數地址找到的就是Son類的Say()函數。
從內存角度看:
構造函數與虛函數
構造函數不能爲虛函數。
從C++之父Bjarne的回答我們應該知道C++爲什麼不支持構造函數是虛函數了,簡單講就是沒有意義。
虛函數的作用在於通過子類的指針或引用來調用父類的那個成員函數。而構造函數是在創建對象時自己主動調用的,不可能通過子類的指針或引用去調用。
構造函數目的是爲了生成對象時進行對象初始化,虛函數目的是在不同類型的對象中調用不同的方法以產生不同的動作,當對象還沒有生成時,內存中不存在虛函數指針和虛函數表,此時虛函數是沒有意義的,而構造函數是爲了在對象還沒有生成時實例化對象,如果構造函數被聲明爲虛函數,內存空間還沒有虛函數表,該構造函數將變得沒有意義,所以構造函數不能是虛函數。
析構函數與虛函數
當派生類指針指向用new運算符生成的派生類對象時,delete派生類指針,將執行派生類的析構函數,再執行基類的析構函數。因爲在實例化派生類對象時,先實例了基類對象。
當基類指針指向用new運算符生成的派生類對象時,delete基類指針,因爲編譯器又進行了類型轉換,默認爲基類指針指向基類對象的地址,根據早綁定中內存的關係,如果基類析構函數沒有聲明爲虛函數,將只執行基類的構造函數,如果基類析構函數聲明爲虛函數,儘管進行類型轉換,根據晚綁定中內存的關係,不管基類對象還是派生類對象都有相應的虛函數表指針,因此析構時會先調用派生類的析構函數(vptr指向自身的析構函數),再調用基類的析構函數(聲明派生類對象先實例了基類對象)。
#include<iostream>
using namespace std;
class Base {
public:
Base()
{
cout<<"Base::Base()"<<endl;
fun();
}
virtual ~Base()
{
cout<<"Base::~Base()"<<endl;
fun();
}
virtual void fun()
{
cout<<"Base::fun() virtual"<<endl;
}
};
class Derived:public Base
{
public:
Derived()
{
cout<<"Derived::Derived()"<<endl;
fun();;
}
~Derived()
{
cout<<"Derived::~Derived()"<<endl;
fun();
}
virtual void fun()
{
cout<<"Derived::fun() virtual"<<endl;
}
};
int main()
{
//basa
Base *b = new Base();
delete b;
cout<<endl;
/*
Base::Base()
Base::fun() virtual
Base::~Base()
Base::fun() virtual
*/
//Derived
Derived *d = new Derived();
delete d;
cout<<endl;
/*
Base::Base()
Base::fun() virtual //派生類還不存在
Derived::Derived()
Derived::fun() virtual //派生類已存在
Derived::~Derived()
Derived::fun() virtual //派生類還存在
Base::~Base()
Base::fun() virtual //派生類不存在
*/
//Base* Derived,父類指針指向子類對象的實現原理
Base *bd = new Derived();
delete bd;
cout<<endl;
/*
Base::Base()
Base::fun() virtual //派生類不存在
Derived::Derived()
Derived::fun() virtual //派生類已存在
//當基類析構函數沒用聲明爲虛函數時,將不調用派生類的析構函數
Derived::~Derived()
Derived::fun() virtual //派生類還存在
Base::~Base()
Base::fun() virtual //派生類不存在
*/
system("pause");
return 0;
}