多型的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的物件去完成時會產生出不同的狀態。
比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優先買票
並沒有構成多型,形參p物件,全部呼叫了Person類的成員函數。
這時候就需要使用虛擬函式來構成多型。
梳理一下,多型的條件:
而重寫的條件
3. 父子類中的函數都是虛擬函式。
4. 函數名引數返回值都要相同(有一個例外,那就是協變,基礎類別的虛擬函式返回基礎類別指標或參照。派生類指標或參照返回派生類指標或參照)
原本都會指向Person類的成員函數,但是當繼承類中對虛擬函式進行重寫,基礎類別的指標或參照去呼叫這個虛擬函式。此時這個指標或參照指向誰就呼叫誰的虛擬函式。這個基礎類別是個相對基礎類別。
即不滿足多型,p呼叫函數,p是什麼型別就呼叫哪個型別的函數。而滿足多型,基礎類別的指標或參照指向誰,就呼叫誰的虛擬函式。
但是注意,派生類中的虛擬函式可以不用寫virtual。會繼承下來,但是我覺得這是C++不嚴格的地方。顯示的帶上virtual更好。
虛擬函式的返回值可以不相同,但是必須滿足,基礎類別的虛擬函式返回值為基礎類別的指標或參照,派生類的虛擬函式返回值為派生類的指標或參照。我們稱它為協變
派生類的解構函式和基礎類別的解構函式實際是構成隱藏的,是因為編譯器將解構函式全部定義為destruct。解構函式沒有返回值,沒有引數,引數名相同。最好將基礎類別的解構函式定義為虛擬函式,那麼派生類的解構函式也會成為虛擬函式(最好加上virtual,不加也可以),構成重寫。
為什麼一定需要構成重寫呢?
假如有這樣一種場景
person型別的指標,指向一個student物件。那麼就會發生記憶體漏失
將函數宣告為虛擬函式,子類進行重寫。
此時不再看P的型別,而是看p指向的是什麼物件,然後呼叫student的解構函式,然後自動呼叫基礎類別的解構函式。
修飾一個函數,表示該函數不能被重寫(我們不寫,子類會預設帶上)
修飾一個類,表示該類不能被繼承
檢查派生類是否重寫了某個虛擬函式,如果沒有則報錯
在虛擬函式的後面寫上 =0 ,則這個函數為純虛擬函式。包含純虛擬函式的類叫做抽象類,抽象類是不能範例化出物件的。
而派生類直接繼承也不行(直接繼承你也是抽象類),必須在對它純虛擬函式進行重寫之後才能範例化出物件。
什麼適合被範例化出抽象類呢?人,植物,車,一些很寬泛的概念
,他們這個抽象類中有一些基本的概念,人的職業,植物的類別,車的品牌。然後讓派生類繼承,實現出具體的行為(人->教師,植物->牡丹,車->賓士)。
而這樣的行為也可以展現多型。因為派生類對函數完成了重寫,基礎類別的指標或者參照呼叫。
抽象類體現了介面繼承,介面繼承,當純虛擬函式是一個宣告的時候,我主要繼承你的,返回值,函數名,引數。
而實現繼承就是,你是一個完整的函數,我繼承你就是為了你裡面的實現,我不需要在寫,直接複用你的。
32位元下,預設對齊數為4,此時類有一個虛擬函式表指標,還有一個int型別變數,所以sizeof大小為8。
此時vfptr陣列指標,指向一個虛擬函式表,這個虛擬函式表實際上是一個陣列,他是一個虛擬函式指標陣列,即 陣列裡面儲存著指標,指標指向一個個函數。
當物件沒有初始化的時候,虛擬函式表也沒有初始化,說明物件裡面的虛擬函式表,是在物件初始化的地方才初始化的。他早早就已經建立,初始化就是把陣列首元素的值給你就好了。
時刻記住,虛擬函式表是一個指標陣列,一個個指標指向了程式碼段中的虛擬函式。func4不是虛擬函式,所以不在表內。
透過記憶體來看一下分佈
跟前面菱形繼承,沒有關係!!!!
菱形繼承是使用虛繼承來解決資料冗餘和二義性,使用虛基指標指向一個虛基表,虛基表裡面儲存著當前地址距離虛基礎類別物件的偏移量,讓原來的地址加偏移量就可以找到虛基礎類別物件。
而這裡的虛擬函式表指標,當物件初始化的時候,虛擬函式表也才會初始化,而且虛擬函式表只有一張。很顯然他是和虛擬函式一樣,都存在程式碼段中。
可以通過列印地址,來驗證
理論上可以 (int)b
但是不支援
可以看出顯然是和程式碼段更加接近。
雖然他是在物件初始化的時候初始化,那麼他是在什麼時候生成的呢,在編譯的時候生成的。
同一個型別用一張虛擬函式表,這個沒有問題。所以子類也是獨有一個虛擬函式表。需要注意的是,假如你多繼承,那就是繼承多張。繼承是複用,而不是共用。
所以可以這麼理解,子類直接將整個虛擬函式表深拷貝下來。當有虛擬函式被重寫,直接在上面覆蓋掉原來的虛擬函式地址。沒有被覆蓋的就留下。所以重寫是語法上的概念,而覆蓋是系統底層的概念。那假如子類一個虛擬函式都沒有重寫呢?雖然虛擬函式的地址都沒變,但是還會單獨生成一個虛擬函式表。
梳理一下,派生類虛表的生成過程,父類別中有虛擬函式,所以子類先會單獨生成一個虛擬函式表,然後深拷貝下來,假如子類重寫了某個虛擬函式,將重寫後的虛擬函式地址覆蓋原來的地址。沒有重寫的地址就不變。
怎麼實現的多型呢?子類中重寫了父類別的虛擬函式,指向或參照子類物件的父類別的指標或參照呼叫這個虛擬函式。
為什麼麼非得是指標或參照呢?
當你是一個普通物件。那肯定是什麼型別就呼叫哪個型別的函數。
A a 或 B b
即使發生切片 ,由於a物件裡面永遠是基礎類別的虛擬函式表,他想實現多型都沒處實現
A a=b
而基礎類別型別指標或參照,指向一個子類物件,這時看到的就是子類物件的虛擬函式表。這樣就能呼叫子類的虛擬函式。
透過組合檢視
那麼假如有多個虛擬函式,我呼叫了不同的虛擬函式,底層是怎麼實現的呢?
也很簡單,按照順序+4位元組即可找到。而他的地址就是按宣告的順序放著的。
所以什麼是多型呢?
多型分為靜態多型和動態多型,靜態多型編譯時就確定,動態多型是執行時在確定。
函數過載,例如
int i=1;
double j=1.1;
cout<<i;
cout<<j;
函數過載了double1型別和int型別,所以可以輸出不同型別。(當然,先實現了運運算元過載)。程式編譯期間確定了行為。
子類對父類別的虛擬函式進行重寫,父類別的指標或參照呼叫這個虛擬函式。當程式執行起來時,才通過虛擬函式表來呼叫虛擬函式。
單繼承中,剛才研究了,子類繼承父類別的虛擬函式表,假如有重寫了某個虛擬函式,會直接覆蓋掉地址,假如沒有重寫就保留。那麼假如子類自己新增了呢。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
};
class Derive:public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func3()
{
cout << "Derive::Func3()" << endl;
}
void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
func1覆蓋,func2保留,自己寫的func3,func4在哪呢?
通過監視視窗看一下,好傢伙,func3,func4影子都沒有。
不用動腦子都知道,肯定是存在的,從記憶體角度看一下。
列印一下他的地址,但是該怎麼傳參呢。
然後main函數中可以呼叫,注意強轉
不太形象,怎麼能看出這是哪個函數呢?
其實我們既然拿到了函數的地址,那麼就可以突破限制,直接呼叫這個函數。(不在像以前一樣只有物件,或者其指標才能呼叫,那是語法上的概念)
兩個物件都呼叫一下,做個對比
這是子類新增的。
說清楚後,再來看多繼承下的虛擬函式表
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()";
}
virtual void Func2()
{
cout << "Base::Func2()";
}
private:
int _b = 1;
};
class Base1
{
public:
virtual void Func1()
{
cout << "Base1::Func1()";
}
virtual void Func2()
{
cout << "Base1::Func2()";
}
private:
int _b1 = 2;
};
class Derive :public Base,public Base1
{
public:
virtual void Func1()
{
cout << "Derive::Func1()";
}
virtual void Func3()
{
cout << "Derive::Func3()";
}
private:
int _d = 2;
};
int main()
{
Base b;
Base1 b1;
Derive d;
cout << sizeof(d) << endl;
return 0;
}
先口算一下,d有多大。d繼承,b和b1的兩個虛擬函式表,8+8+4=20,對齊數為4所以直接放。
列印出來確實是20.
子類重寫了,func1,繼承了func2,自己寫的一個虛擬函式func3。
透過監視視窗看到
func2處於下標1位置,一個是Base型別,一個是Base1型別,在兩個虛擬函式表中地址不同,這是正常的。
在前面我們知道由於子類自己寫的虛擬函式,但是監視視窗不顯示,繼承的func3,但是記憶體中是有對應地址的,第一個疑惑的點,那麼這個自己寫的虛擬函式地址會在這兩個虛擬函式表當中哪一個?還是都有呢?
第二個疑惑的點?子類重寫了繼承下來的func1,為啥是兩個地址,難道不該是一個地址直接覆蓋兩個虛擬函式表嗎?最詭異的是兩個地址,他還呼叫的是相同的函數
先解決第一個問題,在前面我們寫了一個列印虛擬函式表的函數,這裡在複用一下,看看他在哪?
可以看到,自己實現的那個虛擬函式,地址是放到了Base類的虛擬函式表之中。
而與此同時,第二個問題還是沒能得到解答,因為在記憶體中,重寫之後的Func1仍舊是兩個地址,但呼叫同一個。
然後在經過F10偵錯,執行的時候也確實是進入了同一個函數。
那在直接對它取地址,好傢伙,不得了了,3套地址。
所以這裡我們可以推理得,雖然虛擬函式表裡的地址不一樣,但是在組合層面他們會jmp到一個地址處完成對同一個函數的呼叫。
B:虛擬函式表簡稱虛表,虛基表是為了解決菱形繼承引入的,裡面存的是偏移量
D正確,父類別和子類,甚至子類中沒有重寫任何虛擬函式,都會生成不同的虛擬函式表。
參數列初始化的順序是宣告的順序,因為是先繼承的B,在繼承的C。
假如先繼承的C,在繼承B,那麼就會列印 A C B D
p為B型別,當p去呼叫test,由於test沒有重寫,是直接繼承下來,所以裡面的函數原封不動,this指標型別仍然是A,此時把p賦值給A*的this,就相當於
那不是應該列印B->0嗎,錯,其實虛擬函式是一種介面繼承,他將你的引數,返回值,形參列表繼承下來所以B類中,形參的預設引數不起作用,還是A中的val=1,所以列印的是B->1。
A* a=new B;
當delete a
時不構成多型就只會呼叫A的解構函式,從而造成記憶體漏失。定義成虛擬函式,由於構成重寫(解構函式名destructor),基礎類別的指標呼叫,所以構成多型,從而去呼叫B的解構函式,而B的解構函式又會自動呼叫A的解構函式。所以就解決了記憶體漏失問題。