文章目錄:
- 1. 多态的概念
- 2. 虛函數
- 3. 虛函數的重寫
-
- 3.1 虛函數重寫的兩個例外
-
- (1)協變(基類與派生類虛函數傳回值類型不同)
- (2)析構函數的重寫(基類與派生類析構函數的名字不同)
- 3.2 final關鍵字和override關鍵字
- 3.3 重載,覆寫(重寫),隐藏(重定義)的對比
- 4. 多态的原理
- 5. 靜态綁定&動态綁定
- 6. 虛函數表
-
- 6.1 前言
- 6.2子類中的虛函數fun3(),是否在虛表中呢?
- 6.3怎麼能證明子類中的虛函數是在子類中的虛表中呢?
- 6.4 如何取出需表中的虛函數?
- 6.5 單繼承中的虛函數表
-
- 6.5.1 無虛函數覆寫
- 6.5.2 有虛函數覆寫
- 6.6 多繼承中的虛函數表
-
- 6.6.1 無虛函數覆寫
- 6.6.2 有虛函數覆寫
1. 多态的概念
【多态的概念】
通俗來說,多态就是多種形态,具體點就是去完成某個行為,當不同的對象去完成時會産生不同的狀态,即相同的接口實作不同的功能
【多态的構成條件】
① 必須通過父類的指針或者引用調用虛函數
② 父類的函數必須是虛函數,且子類必須對父類的虛函數進行重寫
多态是不同繼承關系的類對象,去調用同一函數,産生不同的行為,如下代碼所示:
#include<iostream>
using namespace std;
class Preson
{
public:
virtual void BuyTicket()
{
cout << "買全票" << endl;
}
};
class Student : public Preson
{
public:
virtual void BuyTicket()
{
cout << "買票半價" << endl;
}
};
void Func(Preson& p)
{
p.BuyTicket();
}
void main()
{
Student st;
Func(st);
Preson* p = &st;
p->BuyTicket();
}

2. 虛函數
被virtual修飾的類成員函數稱為虛函數
如下代碼,在32位作業系統下當我們計算包含虛函數的類大小時,我們會發現不管類中有幾個虛函數,類的大小都會比沒有虛函數時類的大小大4
通過調試,我們可以看到,加虛函數的類對象中除了有一個a成員,還多了一個__vfptr放在對象的前面,對象中的這個指針我們叫做虛函數表指針,虛函數指針指向的這個表叫虛函數表簡稱虛表,虛表中存的是虛函數的位址
圖解如下:
3. 虛函數的重寫
虛函數的重寫(覆寫):子類中有一個和父類完全相同的虛函數(即子類虛函數和父類虛函數的傳回值類型系統,函數名字相同,參數清單相同),稱子類的虛函數重寫了父類的虛函數
注意:在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,雖然也可以構成重寫(因為繼承後基類的虛函數被繼承下來了在派生類依舊保持虛函數屬性),但是該種寫法不是很規範,不建議這樣使用
3.1 虛函數重寫的兩個例外
(1)協變(基類與派生類虛函數傳回值類型不同)
基類虛函數傳回基類對象的指針或者引用,派生類虛函數傳回派生類對象的指針或者引用時,稱為協變。
也就是說父類虛函數傳回父類的指針或引用,子類虛函數傳回子類的指針或引用
如下圖所示:
測試代碼:
#include<iostream>
using namespace std;
class preson
{
public:
virtual preson& func()
{
cout << "preson::func" << endl;
return *this;
}
};
class student : public preson
{
public:
virtual student& func()
{
cout << "student::func" << endl;
return *this;
}
};
void main()
{
student st;
preson* p = &st;
p->func();
}
(2)析構函數的重寫(基類與派生類析構函數的名字不同)
如果基類的析構函數為虛函數,此時派生類析構函數隻要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這裡可以了解為編譯器對析構函數的名稱做了特殊處理,編譯後析構函數的名稱統一處理成destructor。
隻有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函數,才能構成多态,才能保證pd指向的對象正确的調用析構函數,若父類的析構函數不寫成虛函數,則隻能調用父類的析構函數,不會調用子類的析構函數,會造成記憶體洩漏
代碼驗證如下:
#include<iostream>
using namespace std;
class preson
{
public:
preson()
{
cout << "preson::preson()" << endl;
}
virtual ~preson()
{
cout << "~preson::preson()" << endl;
}
public:
virtual void func()
{
cout << "preson::func" << endl;
}
};
class student : public preson
{
public:
student()
{
cout << "student::student()" << endl;
}
virtual ~student()
{
cout << "~student::student()" << endl;
}
public:
virtual void func()
{
cout << "student::func" << endl;
}
};
void main()
{
preson* pd = new student;
delete pd;
}
若不将父類的析構函數寫成虛函數,如下圖所示,可以看到子類的析構函數并未被調用
注意:多态機制在構造函數中不發揮作用
3.2 final關鍵字和override關鍵字
final關鍵字:修飾虛函數,表示該虛函數不能再被繼承,如果被子類重寫,則會報錯
override關鍵字:檢查子類虛函數是否重寫了父類某個虛函數,如果沒有重寫則編譯報錯
3.3 重載,覆寫(重寫),隐藏(重定義)的對比
重載:函數名相同,參數清單不同并且在同一作用域就構成了重載,傳回值可以相同也可以不同
特征是:
①範圍相同(在同一個類中)
②函數名字相同
③參數不同
覆寫(重寫):指派生類函數覆寫基類函數
特征是:
①範圍不同(分别位于子類和父類)
②函數名字相同
③參數相同
④傳回值相同(協變除外)
⑤父類函數必須有virtual關鍵字
隐藏(重定義):當派生類中有一個函數和基類的函數名相同,不管參數是否相同,隻要該函數不為虛函數,則他就是重定義(同名隐藏)
特征是:
①範圍不同(分别位于子類和基類)
②函數名相同
③兩個基類和派生類的同名函數不構成重寫就是同名隐藏
如下圖所示:
4. 多态的原理
使用如下代碼來分析:
#include<iostream>
using namespace std;
class preson
{
public:
preson()
{
cout << "preson::preson()" << endl;
}
~preson()
{
cout << "preson::preson()" << endl;
}
public:
virtual void func()
{
cout << "preson::func" << endl;
}
virtual void func1()
{
cout << "preson::func1" << endl;
}
virtual void func2()
{
cout << "preson::func2" << endl;
}
void show()
{
cout << "preson::show" << endl;
}
private:
int a;
};
class student : public preson
{
public:
student()
{
cout << "student::student()" << endl;
}
~student()
{
cout << "student::student()" << endl;
}
public:
virtual void func()
{
cout << "student::func" << endl;
}
virtual void func2()
{
cout << "student::func2" << endl;
}
virtual void print()
{
cout << "student::print" << endl;
}
void show()
{
cout << "student::show" << endl;
}
private:
int b;
};
void main()
{
student st;
preson* p = &st;
p->func();
p->func1();
p->func2();
p->show();
}
通過調試我們可以看到在構造子類前會先構造父類,而在構造父類的時候,通過this指針看到如下圖左所示,虛函數表中存的父類中的三個虛函數的位址,而在構造子類的時候,可以看到如下圖右所示,父類中的虛函數在子類中被重寫的兩個虛函數将父類的虛函數覆寫了,這就是多态父類指針儲存子類位址卻可以通過父類指針通路子類成員的原因
當我們在子類中對父類的虛函數重寫後,當父類去調用該虛函數的時候,就會通路虛表,然而虛表中存放的是已經被子類覆寫的子類的函數,是以就會轉去調用子類中的重寫的虛函數。
其圖解如下:
5. 靜态綁定&動态綁定
- 靜态綁定又稱為前期綁定(早綁定),在程式編譯期間确定了程式的行為,也稱為靜态多态,比如:函數重載
- 動态綁定又稱後期綁定(晚綁定),是在程式運作期間,根據具體拿到的類型确定程式的具體行為,調用具體的函數,也稱為動态多态
6. 虛函數表
6.1 前言
在上文2中我們了解到虛函數表本質是一個存虛函數指針的指針數組,裡面存的是虛函數的位址,接下來我們将通過如下代碼來深入了解一下虛函數表
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "Base::fun()" << endl;
}
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
private:
int m_a = 10;
int m_b = 20;
};
class child : public Base
{
public:
virtual void fun3()
{
cout << "child::fun3()" << endl;
}
private:
int c_a = 1;
int c_b = 2;
};
void main()
{
Base b;
child ch;
}
通過監視視窗我們可以看到在b對象中可以看到父類的虛表中有虛函數fun(),fun1(),fun2()的位址,而在ch對象中我們可以看到一個父類,父類中有一個虛表裡面也隻有父類中的虛函數的位址,沒有子類虛函數fun3(),如下圖所示:
6.2子類中的虛函數fun3(),是否在虛表中呢?
其實子類的虛函數是在虛表中的,隻不過從螢幕的角度看不到,因為從螢幕的角度__vfptr的成員始終屬于父類的成員,而從父類的角度是看不到子類的方法的,是以編譯器的監視視窗故意在子類的虛表中隐藏了子類的虛函數
6.3怎麼能證明子類中的虛函數是在子類中的虛表中呢?
可以通過調試的記憶體視窗看到,如下圖所示:
父類虛表記憶體
子類虛表記憶體
6.4 如何取出需表中的虛函數?
先對對象取位址,再取出對象的前4個位元組大小的空間,并對其解引用,即找到了虛表指針__vfptr,将虛表指針__vfptr所在空間強轉為 int *,并對其+0再解引用,這樣我們這個指針就指向了第一個虛函數的位址,如何我們typedef一個函數指針,将其強轉之後,就可以調用對應的虛表中的函數
圖解如下:
代碼如下:
typedef void(*pfun)();
void main()
{
Base b;
((pfun)(*((int*)(*(int*)(&b)) + 0)))();
((pfun)(*((int*)(*(int*)(&b)) + 1)))();
((pfun)(*((int*)(*(int*)(&b)) + 2)))();
}
6.5 單繼承中的虛函數表
6.5.1 無虛函數覆寫
虛函數表的畫法:
① 虛函數按照其聲明順序放于表中
② 父類的虛函數在子類的虛函數前面
如下代碼所示:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "Base::fun()" << endl;
}
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
private:
int m_a = 10;
};
class child : public Base
{
public:
virtual void fun3()
{
cout << "child::fun3()" << endl;
}
virtual void fun4()
{
cout << "child::fun4()" << endl;
}
private:
int c_a = 1;
};
void main()
{
child ch;
}
在這種繼承關系中,子類沒有重載任何父類的函數,那麼在派生類的執行個體中,其虛函數表如下圖所示:
6.5.2 有虛函數覆寫
虛函數表的畫法:
① 覆寫的fun1()函數被放到了虛函數表中原來父類虛函數的位置
② 沒被覆寫的函數依舊
如下代碼所示:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "Base::fun()" << endl;
}
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
private:
int m_a = 10;
};
class child : public Base
{
public:
virtual void fun1()
{
cout << "child::fun1()" << endl;
}
virtual void fun4()
{
cout << "child::fun4()" << endl;
}
private:
int c_a = 1;
};
void main()
{
child ch;
}
子類的fun1()覆寫了父類的fun1(),對于派生類執行個體,其虛函數表如下圖所示:
6.6 多繼承中的虛函數表
6.6.1 無虛函數覆寫
虛函數表的畫法:
① 每個父類都有自己的虛表
② 子類的成員函數被放到第一個父類的表中(所謂第一個父類是按照聲明順序來判斷的)------>這樣做是為了解決不同的父類類型的指針指向同一個子類執行個體,而能夠調用到實際的函數
假設有如下繼承關系,注意,子類沒有覆寫父類的函數
子類執行個體中的虛函數表如下圖所示:
6.6.2 有虛函數覆寫
虛函數表的畫法:
① 隻要子類中重寫的父類的虛函數都會覆寫
② 沒被覆寫的函數依舊
假設有如下繼承關系,在子類中覆寫了父類的 f() 函數
子類執行個體中的虛函數表如下圖所示:
我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜态類型的父類來指向子類,并調用子類的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()