目錄
- 多型的概念
- 多型的定義及使用
- 虛函式
- 虛函式的重寫
- 虛函式重寫的兩個例外
- C++11中 override 和 final
- 多載、重寫、隱藏的區別與聯系
- 抽象類
- 多型的原理
- 虛函式指標、虛函式、虛函式表指標
- 實作原理
- 多繼承中的虛函式表
- 多型零碎知識匯總
多型的概念
不同類的物件對同一訊息作出不同的回應就叫做多型,通俗來講,就是去完成某個行為,當不同的物件去完成時會產生出不同的結果
例如去網吧上機,如果你是vip,那么你上網的價格就會比普通用戶上網的價格低;如果你不是vip,那么你上網的價格就是原價,這就是生活中的一種多型現象
多型的定義及使用
例如以下程式,就是一種多型的體現
class Vip
{
public:
virtual void online()
{
cout << "7折優惠" << endl;
}
};
class Common : public Vip
{
public:
virtual void online()
{
cout << "全價無優惠" << endl;
}
};
void fun(Vip& v)
{
v.online();
}
void test()
{
Vip v;
Common m;
fun(v);
fun(m);
}
運行結果:

vip物件以參考的方式賦值給父類物件v,v呼叫online函式,此時呼叫的是基類的online函式,輸出7折優惠;common物件以參考的方式賦值給父類物件v,v呼叫online函式,此時呼叫的是子類的online函式,輸出全價無優惠;這就體現了不同物件去完成一件事會有不同的結果,這就是多型,那想要實作多型,需要什么必要條件呢?
實作多型的前提(缺一不可):
- 多型的體現是體現在基類和派生類中的,所以多型必須有繼承
- 該函式必須是虛函式
- 虛函式需要被子類重寫
- 通過父類的指標或者參考呼叫虛函式,切片操作
前3點都好理解,第四點用代碼解釋
//多型
void fun(Vip& v)
{
v.online();
}
void fun1(Vip* v)
{
v->online();
}
//非多型
void fun2(Vip v)
{
v.online();
}

虛函式
在函式名面前加上關鍵字virtual,則該函式就為虛函式,
格式:virtual 回傳值 函式名(引數)
class Vip
{
public:
virtual void online()
{
cout << "7折優惠" << endl;
}
};
虛函式的重寫
虛函式的重寫(覆寫):派生類中有一個跟基類完全相同的虛函式稱子類的虛函式重寫了基類的虛函式
重寫(覆寫)要求:派生類中的虛函式要與基類中的虛函式的函式名、引數串列、回傳值都要完全相同
虛函式在多型中的注意事項:在重寫基類虛函式時,派生類的虛函式在不加virtual關鍵字時,雖然也可以構成重寫,但是該種寫法不是很規范,不建議這樣使用(因為繼承后基類的虛函式被繼承下來了在派生類依舊保持虛函式屬性)
虛函式重寫的兩個例外
1、協變(回傳值不同):
派生類重寫基類虛函式時,與基類虛函式回傳值型別可以不同,但是回傳值型別必須是有繼承關系的指標或者參考
class A //基類A
{};
class B : public A //派生類B
{};
class Vip
{
public:
virtual A* online() //回傳值是基類的指標,且必須是基類的指標
{
cout << "7折優惠" << endl;
return new A;
}
};
class Common : public Vip
{
public:
virtual B* online()//回傳值是派生類的指標,且必須是派生類的指標
{
cout << "全價無優惠" << endl;
return new B;
}
};
運行結果:

2、解構式的重寫(名字不同)
如果將基類的解構式置為虛函式,那么派生類中的顯示定義的解構式無論是否是虛函式,都與基類中的解構式構成了重寫,因為在底層,編譯器對解構式名做了特殊處理,在底層的名字都為destructor,所以構成了重寫
我們來看以下代碼
class Vip
{
public:
virtual void online()
{
cout << "7折優惠" << endl;
}
~Vip()
{
cout << "~Vip" << endl;
}
};
class Common : public Vip
{
public:
virtual void online()
{
cout << "全價無優惠" << endl;
}
~Common()
{
if (_name)
{
delete[] _name;
cout << "delete[] _name" << endl;
}
cout << "~Common" << endl;
}
private:
char* _name = new char[100];
};
運行結果:此時并不會釋放子類中的資源,產生了記憶體泄漏的問題

要想防止記憶體泄漏,就必須使解構式有多型的行為,在基類中的解構式置為虛函式,使此解構式與派生類中的解構式構成重寫
virtual ~Vip()
{
cout << "~Vip" << endl;
}
運行結果:這樣子就不會發生記憶體泄漏了

為什么解構式要被定義成虛函式?
答:實作多型時,我們通過基類指標指向子類物件,在delete基類指標時,我們希望先呼叫子類的解構式,再呼叫父類的解構式,要實作這個目的,解構式就必須定義成虛函式,否則只會呼叫父類的解構式,子類的解構式不會被呼叫
C++11中 override 和 final
如果我們在寫多型的代碼時,由于我們的疏忽,將函式的名字、回傳值或者引數寫錯了,此時就無法構成重寫,而這種錯誤在編譯期間是不會報錯的,只有在程式運行時才能發現,這時候為了解決這種問題,C++11引入了兩個關鍵字override 和 final
override
格式:重寫的函式 override ----用在派生類中的虛函式
作用:檢查派生類虛函式是否重寫了基類某個虛函式,如果沒有重寫編譯報錯
class Vip
{
public:
virtual void online()
{
cout << "7折優惠" << endl;
}
};
class Common : public Vip
{
public:
//介面繼承,不繼承基類中的實作,需要自己重寫實作
virtual void online() override
{
cout << "全價無優惠" << endl;
}
};

final
格式1:class 類名 final ----用在基類中
格式2:函式名 final ----用在基類中的虛函式
作用:1、被final修飾的類不能被繼承;2、被final修飾的虛函式不能被重寫
class A final
{};
class B : A
{};

class Vip
{
public:
//定義繼承,將該函式的實作也繼承過去且無法修改
virtual void online() final
{
cout << "7折優惠" << endl;
}
};
class Common : public Vip
{
public:
virtual void online()
{
cout << "全價無優惠" << endl;
}
};

多載、重寫、隱藏的區別與聯系
函式多載:同一作用域內被宣告的幾個具有不同引數的同名函式,根據引數串列確定呼叫哪個函式,且不關心函式的回傳值
函式隱藏:是指派生類的函式屏蔽了與其同名的基類函式,只要同名函式,不管引數串列是否相同,基類函式都會被隱藏
重寫覆寫:是指派生類中存在重新定義的函式,其函式名,引數串列,回傳值型別,所有都必須同基類中被重寫的函式一致,只有函式體不同,派生類呼叫時會呼叫派生類的重寫函式,不會呼叫被重寫函式,重寫的基類中被重寫的函式必須有virtual修飾
| 類別 | 作用域 | 函式名 | 引數串列 | 回傳值型別 | 是否有virtual修飾 |
|---|---|---|---|---|---|
| 函式多載 | 同一作用域 | 相同 | 不同 | 無要求 | 無要求 |
| 函式隱藏 | 不同作用域(父類和子類) | 相同 | 無要求 | 無要求 | 父類函式不能有virtua |
| 重寫覆寫 | 不同作用域(父類和子類) | 相同 | 相同 | 相同(協變除外) | 父類函式必須有 |
抽象類
純虛函式的定義:在虛函式的后面寫上=0 ,則這個函式為純虛函式
//純虛函式
virtual void fun() = 0
{}
抽象類的定義:包含純虛函式的類叫做抽象類(也叫介面類)
//抽象類
class A
{
public:
//純虛函式
virtual void fun() = 0
{}
};
class B : public A
{
public:
virtual void fun()
{
cout << "B::fun()" << endl;
}
};
class C : public A
{
public:
virtual void fun()
{
cout << "C::fun()" << endl;
}
};
注意:抽象類不能實體化出物件,派生類繼承后也不能實體化出物件,只有重寫純虛函式,派生類才能實體化出物件
class D: public A
{};

抽象類具有規劃性,如假如你要去網吧上網,那么你必須提供身份證,也就是派生類必須提供重寫提供身份證的這個虛函式,如果不提供,就不能上網,不重寫,也就是不能使用該類
引入純虛函式的目的:
1、為了方便使用多型特性,我們常常需要在基類中定義虛擬函式
2、在很多情況下,基類本身生成物件是不合情理的,例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理
為了解決上述問題,引入了純虛函式的概念,將函式定義為純虛函式,則編譯器要求在派生類中必須予以重寫以實作多型性,同時含有純虛擬函式的類稱為抽象類,它不能生成物件,這樣就很好地解決了上述兩個問題
多型的原理
虛函式指標、虛函式、虛函式表指標
我們先來看看以下類中的大小
class Base
{
public:
virtual void fun()
{
cout << "fun()" << endl;
}
protected:
int _a = 1;
};
運行結果:

我們發現是8個位元組,為什么會是8個位元組呢?難道函式也占用空間了嗎?我們創建一個Base物件看看這個物件中都包含了哪些成員

我們可以發現,在物件b中不僅包含了自己定義的一個成員變數_a,好包含了一個void**型別的指標,所以大小才會8個位元組,那為什么會有這個指標呢?那肯定跟這個虛函式脫不了關系,其實這個指標就是虛函式表指標----__vfptr(v代表virtual,f代表 function,ptr代表指標),我們再來看看虛函式表指標里的內容

虛函式表指標指向的是一個虛表----vftable,也就是說虛表指標是虛表的首地址 ,而虛表中存放的是虛函式指標,虛函式指標也就是就是虛函式的地址,所以虛表也就是一個指標陣列
我們再來分析一下虛函式指標和虛函式表的關系
class Base
{
public:
virtual void fun1()
{
cout << "fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
void fun3()
{
cout << "Base::fun3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void fun1()
{
{
cout << "Derive::fun1()" << endl;
}
}
private:
int _d = 2;
};
創建Base物件和Derive物件,查看他們的虛表指標,我們在基類中創建了兩個虛函式,而虛函式指標會存放在虛表中,虛表中的兩個元素,第一個元素是就是fun1的虛函式指標,第二個元素就是fun2的虛函式指標

我們畫一幅圖來形象理解虛表指標、虛表、虛函式指標、虛函式的關系

搞懂了虛表指標和虛表的關系后我們再來看看派生類中的虛表指標,派生類中也有一個虛表指標,派生類繼承了基類的虛表,也就是將基類中的虛表拷貝一份,再用一個新的虛表指標指向該虛表,派生類當存在同名、回傳值、引數串列都相同的虛函式時,會將虛表中的相同虛函式指標給覆寫掉,所以重寫也可以叫做覆寫,原因就是這樣子得來的,例如派生類中的fun1重寫基類中的fun1,此時派生類中的虛表存放的就是派生類fun1的虛函式指標,所以之所以可以產生多型行為,其實就是派生類中的虛函式指標覆寫了基類中的指定的虛函式指標,從而呼叫時就會呼叫派生類的虛函式了

但是我們要知道,其實虛表不是存放在物件當中的,只有虛表指標才存放在物件中,我們先證明物件中只存放虛表指標而不存放虛表,我們知道虛表的大小是和虛函式的個數有關,那么我們創建不同個數的虛函式的類,他的大小如果一樣,那么就表示虛表是不存放在物件中的,反之存在,
class A
{
public:
virtual void fun1()
{
cout << "fun1()" << endl;
}
virtual void fun2()
{
cout << "fun2()" << endl;
}
private:
int _a = 1;
};
class B
{
public:
virtual void fun3()
{
cout << "fun3()" << endl;
}
private:
int _a = 2;
};
運行結果:我們發現即使虛函式的個數不同,類的大小都是相同的,也就表明虛表是不存放在類中的

我們再總結理一理虛表指標、虛函式指標、虛函式它們各自存放的位置,虛表指標是存放在一個物件中的起始位置或者末尾位置(平臺不同位置不同,一般是起始位置);虛函式指標是存放在虛表中的;虛函式和普通函式一樣,都是存放在代碼段中的那虛表到底存在哪里呢?
在我們程式員能用的到的記憶體有堆疊、堆、資料段、代碼段,也就是所虛表肯定存在在這四個記憶體中的其中一個,那么我們如何判斷呢?我們可以粗糙得查看地址的遠近,查看虛表的地址和那塊記憶體的地址相近,最相近的那地址,也就可以認為是在那一塊記憶體中
void test()
{
int a = 10; //堆疊
int* ptr = new int;//堆
static int s = 1; //資料段
const char* str = "123"; //文字常量區/代碼段
cout << "堆疊:" << &a << endl;
cout << "堆:" << ptr << endl;
cout << "資料段:" << &s << endl;
printf("代碼段:%p\n", str);
}
運行結果:堆疊和堆地址相差還是很大的,而資料段和代碼段的地址相差也不小,這四個地址都分別代表了記憶體中的不同位置,我們接下來在列印虛表的地址,看看和哪個地址接近

但是現在的問題是,我們如何拿到虛表的地址,我們來分析分析
1、先取b的地址,強轉成一個int* 的指標,獲取到類的前4個地址,也就獲得了虛表指標的地址;
Base b;
&b;
(int*)&b
2、再解參考取值,就取到了b物件頭4bytes的值,這個值就是指向虛表的指標,也就是虛表的首地址
*(int*)b;
3、但是這個值是int型別的值,我們必須強轉為虛表存放的型別,虛表存放的型別也就是虛函式指標,我們這里定義的虛函式是無引數無回傳值的函式,此時我們就拿到了虛表的地址
//虛函式指標變數
//沒有回傳值,引數串列為空的指標
typedef void(*vfptr)();
vfptr* vfp = (vfptr*)(int*)&b;
運行結果:我們可以推出

我們發現虛表的地址更接近資料段的地址,所以虛表存放在資料段中,虛函式表本質是一個存虛函式指標的指標陣列,這個陣列最后面放了一個nullptr
實作原理
如何找到虛函式?
1、從物件中獲取虛表指標
2、通過虛表指標找到虛表
3、從虛表中找到虛函式的地址
4、執行虛函式的指令

我們再通過匯編來看代碼的執行

第一行:v中存的是v物件的指標,將v移動到eax中
第二行:[eax]就是取eax值指向的內容,這里相當于把vip物件頭4個位元組(虛表指標)移動到了edx
第五行:[edx]就是取edx值指向的內容,這里相當于把虛表中的頭4位元組存的虛函式指標移動到了eax
第六行:call eax,呼叫虛函式
這里可以看出滿足多型的呼叫,不是在編譯時確定的,是運行起來以后到物件的中取找的
我們來獲取虛表中的虛函式,執行并列印
class Base
{
public:
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
virtual void fun2()
{
cout << "Base::fun2()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void fun1()
{
cout << "Derive::fun1()" << endl;
}
virtual void fun3()
{
cout << "Derive::fun3()" << endl;
}
virtual void fun4()
{
cout << "Derive::fun4()" << endl;
}
private:
int _d = 2;
};
typedef void(*vfptr)();
void printfVftable(vfptr vtable[])
{
cout << "虛表地址:" << vtable << endl;
//訪問虛表元素:虛函式指標
vfptr* fptr = vtable;
while (*fptr != nullptr)
{
(*fptr)();
++fptr;
}
}

多繼承中的虛函式表
在多繼承中,都會繼承每個類的虛表

我們先來看看第一個虛表的地址,并執行虛表中的虛函式
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private: int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private: int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private: int d1;
};
運行結果:func2函式是使用的是第一個父類的虛函式

我們再來看看第二個虛表的地址,我們如何獲得第二個虛表的地址呢?此時我們就必須要偏移base1這個類的大小的距離

我們就可以看到第二個虛表的虛函式了,并且子類的fun1函式也將第二個虛表的fun1覆寫了,但是我們發現fun3只在第一個虛表中出現,并不在第二個虛表中出現,所以可以說是,新定義的虛函式指標都會默認放到第一個虛表中,所以fun3指標只會存放第一個虛表中
多型零碎知識匯總
1、virtual關鍵字只在宣告時加上,在類外實作時不能加
2、static和virtual是不能同時使用的
3、靜態成員函式屬于整個類,不能被重寫,不能設定為虛函式,虛表指標是存在物件中的,通過類名是拿不到虛表指標的
4、編譯時的多型性可通過函式多載和模板實作;運行時的多型性可通過虛函式實作
5、一個類的不同物件共享該類的虛表
6、虛表是在編譯期間生成的
7、多繼承的時候,就會可能有多張虛表
8、純虛函式不一定是空函式,只是寫函式體的意義不大
9、行內函式不能是虛函式,因為inline函式沒有地址,無法把地址放到虛函式表中
10、如果存在虛函式和虛擬繼承,物件的前4個位元組依然是虛表指標,緊接后面的是虛基表指標
11、建構式是不能是虛函式的,虛函式的執行依賴于虛函式表,而虛函式表在建構式中進行初始化作業,即初始化vptr,讓他指向正確的虛函式表,而在構造物件期間,虛函式表還沒有被初始化,將無法進行
12、如果是普通物件,呼叫普通函式和虛函式的速度是一樣快的;如果是參考或者指標,由于構成多型,運行呼叫虛函式需要到虛函式表中去查找,則普通函式更快
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d; //*p1=_b1
Base2* p2 = &d; //*p2=_b2
Derive* p3 = &d; //*p3=_b1
//p1 == p3 != p2
return 0;
}
class A {
public:
virtual void func(int val = 1)
{ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A {
public: void func(int val=0)
{ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[]) {
B*p = new B;
p->test();
//B->1,A類中的func才是真正的定義
//而B類中是對func的重寫,編譯器看到是重寫時,不看預設值,只看定義
return 0;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/282666.html
標籤:其他
上一篇:第二講:PN結與二極管的特性
下一篇:資料結構(二):鏈表
