初识C++之多态
多態(tài)性是將接口與實(shí)現(xiàn)進(jìn)行分離;用形象的語言來解釋就是實(shí)現(xiàn)以共同的方法,但因個(gè)體差異,而采用不同的策略。
1、什么是多態(tài)
多態(tài)(Polymorphism)按字面的意思就是“多種狀態(tài)”。在面向?qū)ο笳Z言中,接口的多種不同的實(shí)現(xiàn)方式即為多態(tài)。它是面向?qū)ο蟪绦蛟O(shè)計(jì)(OOP)的一個(gè)重要特征。如果一個(gè)語言只支持類而不支持多態(tài),只能說明它是基于對象的,而不是面向?qū)ο蟮?。C++中的多態(tài)性具體體現(xiàn)在運(yùn)行和編譯兩個(gè)方面。它可以簡單地概括為“一個(gè)接口,多種方法”。
那么多態(tài)的作用是什么呢,封裝可以使得代碼模塊化,繼承可以擴(kuò)展已存在的代碼,他們的目的都是為了代碼重用。而多態(tài)的目的則是為了接口重用。也就是說,不論傳遞過來的究竟是那個(gè)類的對象,函數(shù)都能夠通過同一個(gè)接口調(diào)用到適應(yīng)各自對象的實(shí)現(xiàn)方法。
輸出:
可以看到,雖然pa1、pa1都是基類的指針,但是調(diào)用時(shí)卻調(diào)用的是Person、Horse兩個(gè)派生類的方法。這是因?yàn)閜a1和pa2分別指向了Person、Horse兩個(gè)派生類的對象。原來下來慢慢剖析。
2、多態(tài)的分類
①編譯時(shí)多態(tài):也叫靜態(tài)多態(tài),在編譯時(shí)就可以確定對象使用的形式。 實(shí)現(xiàn)機(jī)制使通過函數(shù)重載(運(yùn)算符重載實(shí)質(zhì)也屬于函數(shù)重載)。
②運(yùn)行時(shí)多態(tài):也叫動態(tài)多態(tài),其具體引用的對象在運(yùn)行時(shí)才能確定。 實(shí)現(xiàn)機(jī)制是通過虛函數(shù)。
靜態(tài)多態(tài)性與動態(tài)多態(tài)性的實(shí)質(zhì)區(qū)別就是函數(shù)地址是早綁定還是晚綁定。如果函數(shù)的調(diào)用,在編譯器編譯期間就可以確定函數(shù)的調(diào)用地址,并生產(chǎn)代碼,是靜態(tài)的,就是說地址是早綁定的。而如果函數(shù)調(diào)用的地址不能在編譯器期間確定,需要在運(yùn)行時(shí)才確定,這就屬于晚綁定。
3、動態(tài)多態(tài)性存在的三個(gè)必要條件
①要有類的繼承關(guān)系;
②要有虛函數(shù)(只有重寫了虛函數(shù)的才能算作是體現(xiàn)了C++多態(tài)性);
注意函數(shù)重寫、重載、重定義的區(qū)別,可以參考:
http://blog.csdn.net/ljx_5489464/article/details/51127081
③要有父類指針/引用指向子類對象(用于訪問派生類中同名覆蓋成員函數(shù))。
4、多態(tài)的實(shí)現(xiàn)
C++多態(tài)性是通過虛函數(shù)來實(shí)現(xiàn)的,通過指向派生類的基類指針或引用,訪問派生類中同名覆蓋成員函數(shù)。虛函數(shù)允許子類重新定義成員函數(shù),而子類重新定義父類的做法稱為覆蓋(override),或者稱為重寫(重寫的話可以有兩種,直接重寫成員函數(shù)和重寫虛函數(shù),只有重寫了虛函數(shù)的才能算作是體現(xiàn)了C++多態(tài)性)。
虛函數(shù):在某基類中聲明為 virtual 并在一個(gè)或多個(gè)派生類中被重新定 義的成員函數(shù),用法格式為:
virtual 函數(shù)返回類型 函數(shù)名(參數(shù)表)
{
函數(shù)體;
}
關(guān)于虛函數(shù)這里不做過剖析,可以參考:
http://blog.csdn.net/ljx_5489464/article/details/51138393
前面說了,多態(tài)的動態(tài)性實(shí)現(xiàn)是靠虛函數(shù),它的實(shí)質(zhì)是在基類定義一個(gè)虛函數(shù),在派生類中對其進(jìn)行重寫,然后定義基類的指針指向派生類對象,然后用該指針調(diào)用虛函數(shù),此時(shí)調(diào)用的就是指針指向的對象對應(yīng)的類里面的同名函數(shù)。那么這種機(jī)制在底層是怎樣實(shí)現(xiàn)的呢?
想要弄清楚這個(gè)就必須要清楚一個(gè)東西–>虛表,全稱為虛擬函數(shù)表。什么是虛表,虛表就是一張表,它里面存放虛函數(shù)的入口地址,在C++語言中,每個(gè)有虛函數(shù)的類或者基類有虛函數(shù)的派生類,編譯器都會為它生成一個(gè)虛擬函數(shù)表(注意:虛表是從屬于類的)。此外,編譯器會為包含虛函數(shù)的類加上一個(gè)成員變量,是一個(gè)指向該虛函數(shù)表的指針(常被稱為vfptr,注意:虛表指針是從屬于對象的)。也就是說,如果一個(gè)類含有虛表,則該類的所有對象都會含有一個(gè)虛表指針,并且該虛表指針指向同一個(gè)虛表。
虛表的內(nèi)容是依據(jù)類中的虛函數(shù)聲明次序–填入函數(shù)指針。派生類會繼承基礎(chǔ)類別的虛表(以及所有其他可以繼承的成員),再在此虛表上添加上自己的虛函數(shù)指針,當(dāng)我們在派生類中改寫虛函數(shù)時(shí),虛表就受了影響,改寫后的的虛函數(shù)的地址會替換原虛函數(shù)的地址存放于虛表中。
①no_Virtual類沒有任何數(shù)據(jù)成員,也沒有虛函數(shù),按說它的大小應(yīng)該是0,但這兒它的大小是1。我理解的原因有以下兩點(diǎn):
這個(gè)是因?yàn)閚o_Virtual既然是一個(gè)類型,那么就能用來定義變量,而定義變量肯定要分配空間,分配多大合適呢?0個(gè)?顯然不可能,2個(gè)、4個(gè)?太浪費(fèi)了,它什么都不用存儲,分配那么大干什么,所以編譯器就為我們選了個(gè)折中的辦法,分配一個(gè),既不會浪費(fèi)空間,也能用來定義變量。
既然no_Virtual是類型,用來定義變量的,那么要是它一個(gè)空間都不占的話,用它定義出來的變量就都從同一位置開始存儲了,那我們訪問那塊空間時(shí),怎么知道到底是訪問哪一個(gè)變量呢?
②Virtual類沒有任何數(shù)據(jù)成員,但是有虛函數(shù),按我們上面的說法,它的大小是4,結(jié)果很給面子的正確了。
定義一個(gè)Virtual類的變量,看看是不是像上面說的那樣,有一個(gè)從屬于它的虛表指針,從監(jiān)視窗口看,確實(shí)是的。
輸出結(jié)果:
因?yàn)閂S的監(jiān)視窗口好像有Bug:
像這兒,派生類的對象就只能看到繼承與基類的虛表,而不能看到派生類自己的,所以上面我一個(gè)函數(shù)分別打印了基類與派生類的虛表,可以很直觀的看到,首先,派生類的虛表是繼承于基類的,然后在加上自己的虛函數(shù)的地址到虛表中去,就構(gòu)成了自己的虛表。
現(xiàn)在來剖析一下打印虛表的函數(shù)是怎么實(shí)現(xiàn)的:
首先得清楚帶有虛函數(shù)的類創(chuàng)建的對象,前四個(gè)字節(jié)里面存儲的就是虛表指針的地址,如下圖:
再回過頭來看看虛表的打印函數(shù):
void PrintVirTable() {Base b;Derived d;cout << "Base:" << endl;int *vfpTable = (int *)(*(int*)&b);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}cout << "Derived:" << endl;vfpTable = (int *)(*(int*)&d);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;} }它的原理其實(shí)是這樣的:
再來看一個(gè)例子:
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> using namespace std;class Base { public:virtual void Fun1(){cout << "Base::Fun1()" << endl;}virtual void Fun2() //注意這兒加了關(guān)鍵字virtual{cout << "Base::Fun2()" << endl;} };class Derived: public Base { public:virtual void Fun3() //注意這兒加了關(guān)鍵字virtual{cout << "Derived::Fun3()" << endl;}virtual void Fun4() //注意這兒加了關(guān)鍵字virtual{cout << "Derived::Fun4()" << endl;} public:int data; };void PrintVirTable() {Derived d1;Derived d2;cout << "d1::virTable" << endl;int *vfpTable = (int *)(*(int*)&d1);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}cout << "d2::virTable" << endl;vfpTable = (int *)(*(int*)&d2);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;} }int main() {PrintVirTable();return 0; }輸出結(jié)果:
這兒就證明了如果一個(gè)類有虛函數(shù),那么該不同對象公用同一張?zhí)摫?。因?yàn)楹瘮?shù)的地址是在編譯期間的確定的,一個(gè)函數(shù)的地址只會被存儲一次,雖然累的對象不同,但它們調(diào)用該類的同名函數(shù)其實(shí)是調(diào)用的同一函數(shù),因此它們的虛表會相同。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
- 上一篇: java圆形泳池问题_Java实现 Le
- 下一篇: c++ 字符串相等比较