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