介紹有關C++中繼承與多態的基礎虛函數類

2019-10-19     科技i關注

這篇文章主要給大家介紹了關於C++中繼承與多態的基礎虛函數類的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。

前言

本文主要給大家介紹了關於C++中繼承與多態的基礎虛函數類的相關內容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。

虛函數類

繼承中我們經常提到虛擬繼承,現在我們來探究這種的虛函數,虛函數類的成員函數前面加virtual關鍵字,則這個成員函數稱為虛函數,不要小看這個虛函數,他可以解決繼承中許多棘手的問題,而對於多態那他更重要了,沒有它就沒有多態,所以這個知識點非常重要,以及後面介紹的虛函數表都極其重要,一定要認真的理解~ 現在開始概念虛函數就又引出一個概念,那就是重寫(覆蓋),當在子類的定義了一個與父類完全相同的虛函數時,則稱子類的這個函數重寫(也稱覆蓋)了父類的這個虛函數。這裡先提一下虛函數表,後面會講到的,重寫就是將子類裡面的虛函數表里的被重寫父類的函數地址全都改成子類函數的地址。

純虛函數

在成員函數的形參後面寫上=0,則成員函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口類)

抽象類不能實例化出對象。純虛函數在派生類中重新定義以後,派生類才能實例化出對象。

看一個例子:

class Person 
{
virtual void Display () = 0; // 純虛函數
protected :
string _name ; // 姓名
};

class Student : public Person
{};

先總結一下概念:

1.派生類重寫基類的虛函數實現多態,要求函數名、參數列表、返回值完全相同。(協變除外)

2.基類中定義了虛函數,在派生類中該函數始終保持虛函數的特性。

3.只有類的成員函數才能定義為虛函數。

4.靜態成員函數不能定義為虛函數。

5.如果在類外定義虛函數,只能在聲明函數時加virtual,類外定義函數時不能加virtual。

6.不要在構造函數和析構函數裡面調用虛函數,在構造函數和析構函數中,對象是不完整的,可能會發生未定義的行為。

7.最好把基類的析構函數聲明為虛函數。(why?另外析構函數比較特殊,因為派生類的析構函數跟基類的析構函數名稱不一樣,但是構成覆蓋,這裡是因為編譯器做了特殊處理)

8.構造函數不能為虛函數,雖然可以將operator=定義為虛函數,但是最好不要將operator=定義為虛函數,因為容易使用時容易引起混淆.



上面概念大家可能都會問一句為什麼要這樣? 這些內容在接下來的知識里都能找到答案~ 好了那麼我們今天的主角虛函數登場!!!!

何為虛函數表,我們寫一個程序,調一個監視窗口就知道了。

下面是一個有虛函數的類:

#include 
#include
using namespacestd;

class Base
{
public:
virtual void func1()
{}

virtual void func2()
{}

private:
inta;
};

void Test1()
{
Base b1;
}

int main()

{
Test1();
system("pause");
return0;
}

我們現在點開b1的監視窗口



這裡面有一個_vfptr,而這個_vfptr指向的東西就是我們的主角,虛函數表。一會大家就知道了,無論是單繼承還是多繼承甚至於我們的菱形繼承虛函數表都會有不同的形態,虛函數表是一個很有趣的東西。



我們來研究一下單繼承的內存格局

仔細看下面代碼:

#include 
#include
using namespace std;


class Base
{
public:
virtual void func1()
{
cout<< "Base::func1"<< endl;
}

virtual void func2()
{
cout<< "Base::func2"<< endl;
}

private:
inta;
};

class Derive:public Base
{
public:
virtual void func1()
{
cout<< "Derive::func1"<< endl;
}

virtual void func3()
{
cout<< "Derive::func3"<< endl;
}

virtual void func4()
{
cout<< "Derive::func4"<< endl;
}

private:
int b;
};

對於Derive類來說,我們覺得它的虛表里會有什麼?

首先子類的fun1()重寫了父類的fun1() ,虛表里存的是子類的fun1() ,接下來父類的fun2() ,子類的fun3() , fun4()都是虛函數,所以虛表里會有4個元素,分別為子類的fun1() ,父類fun2() ,子類fun3() ,子類fun4() 。然後我們調出監視窗口看我們想的到底對不對呢?



我預計應該是看到fun1() ,fun2() ,fun3() ,fun4()的虛函數表,但是呢這裡監視窗口只有兩個fun1() , fun2() ,難道我們錯了????

這裡並不是這樣的,只有自己靠得住,我覺得這裡的編譯器有問題,那我們就得自己探索一下了。 但是在探索之前我們必須來實現一個可以列印虛函數表的函數。

typedef void(*FUNC)(void); 
void PrintVTable(int* VTable)
{
cout<< " 虛表地址"<
for(inti = 0;VTable[i] != 0; ++i)
{
printf(" 第%d個虛函數地址 :0X%x,->", i,VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}

cout<< endl;
}


int main()
{
Derive d1;
PrintVTable((int*)(*(int*)(&d1)));
system("pause");
return0;
}

下圖來說一下他的緣由:



我們來使用這個函數,該函數代碼如下:

//單繼承 
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}

virtual void func2()
{
cout << "Base::func2" << endl;
}

private:
int a;
};

class Derive :public Base
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}

virtual void func3()
{
cout << "Derive::func3" << endl;
}

virtual void func4()
{
cout << "Derive::func4" << endl;
}

private:
int b;
};
typedef void(*FUNC)(void);
void PrintVTable(int* VTable)
{
cout<< " 虛表地址"<
for(inti = 0;VTable[i] != 0; ++i)
{
printf(" 第%d個虛函數地址 :0X%x,->", i,VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}

cout<< endl;
}


int main()
{
Derive d1;
PrintVTable((int*)(*(int*)(&d1))); //重點
system("pause");
return0;
}

這裡我就要講講這個傳參了,注意這裡的傳參不好理解,應當細細的"品味".

PrintVTable((int*)(*(int*)(&d1)));

首先我們肯定要拿到d1的首地址,把它強轉成int*,讓他讀取到前4個位元組的內容(也就是指向虛表的地址),再然後對那個地址解引用,我們已經拿到虛表的首地址的內容(虛表裡面存儲的第一個函數的地址)了,但是此時這個變量的類型解引用後是int,不能夠傳入函數,所以我們再對他進行一個int*的強制類型轉換,這樣我們就傳入參數了,開始函數執行了,我們一切都是在可控的情況下使用強轉,使用強轉你必須要特別清楚的知道內存的分布結構。

最後我們來看看輸出結果:



到底列印的對不對呢? 我們驗證一下:


這裡我們通過&d1的首地址找到虛表的地址,然後訪問地址查看虛表的內容,驗證我們自己寫的這個函數是正確的。(這裡VS還有一個bug,當你第一次列印虛表時程序可能會崩潰,不要擔心你重新生成解決方案,再運行一次就可以了。因為當你第一次列印是你虛表最後一個地方可能沒有放0,所以你就有可能停不下來然後崩潰。)我們可以看到d1的虛表並不是監視器裡面列印的那個樣子的,所以有時候VS也會有bug,不要太相信別人,還是自己靠得住。哈哈哈,臭美一下~

我們來研究一下多繼承的內存格局

探究完了單繼承,我們來看看多繼承,我們還是通過代碼調試的方法來探究對象模型

看如下代碼:

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;
};

typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
cout << " 虛表地址>" << VTable << endl;

for (int i = 0; VTable[i] != 0; ++i)
{
printf(" 第%d個虛函數地址 :0X%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}


void Test1()
{
Derive d1;
//Base2虛函數表在對象Base1後面
int* VTable = (int*)(*(int*)&d1);
PrintVTable(VTable);
int* VTable2 = (int *)(*((int*)&d1 + sizeof (Base1) / 4));
PrintVTable(VTable2);
}
int main()
{
Test1();
system("pause");
return 0;
}

現在我們現在知道會有兩個虛函數表,分別是Base1和Base2的虛函數表,但是呢!我們的子類里的fun3()函數怎麼辦?它是放在Base1里還是Base2里還是自己開闢一個虛函數表呢?我們先調一下監視窗口:



監視窗口又不靠譜了。。。。完全沒有找到fun3().那我們直接看列印出來的虛函數表。



現在很清楚了,fun3()在Base1的虛函數表中,而Base1是先繼承的類,好了現在我們記住這個結論,當涉及多繼承時,子類的虛函數會存在先繼承的那個類的虛函數表里。記住了!

我們現在來看多繼承的對象模型:


現在我們來結束一下上面我列的那麼多概念現在我來逐一的解釋為什麼要這樣.

1.為什麼靜態成員函數不能定義為虛函數?

因為靜態成員函數它是一個大家共享的一個資源,但是這個靜態成員函數沒有this指針,而且虛函數變只有對象才能能調到,但是靜態成員函數不需要對象就可以調用,所以這裡是有衝突的.

2.為什麼不要在構造函數和析構函數裡面調用虛函數?

構造函數當中不適合用虛函數的原因是:在構造對象的過程中,還沒有為「虛函數表」分配內存。所以,這個調用也是違背先實例化後調用的準則析構函數當中不適用虛函數的原因是:一般析構函數先析構子類的,當你在父類中調用一個重寫的fun()函數,虛函數表裡面就是子類的fun()函數,這時候已經子類已經析構了,當你調用的時候就會調用不到.

現在我在寫最後一個知識點,為什麼儘量最好把基類的析構函數聲明為虛函數??

現在我們再來寫一個例子,我們都知道平時正常的實例化對象然後再釋放是沒有一點問題的,但是現在我這裡舉一個特例:

我們都知道父類的指針可以指向子類,現在呢我們我們用一個父類的指針new一個子類的對象。

//多態 析構函數 
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}

virtual void func2()
{
cout << "Base::func2" << endl;
}

virtual ~Base()
{
cout << "~Base" << endl;
}

private:
int a;
};

class Derive :public Base
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual ~Derive()
{
cout << "~Derive"<< endl;
}
private:
int b;
};

void Test1()
{
Base* q = new Derive;
delete q;
}
int main()
{
Test1();
system("pause");
return 0;
}

這裡面可能會有下一篇要說的多態,所以可能理解起來會費勁一點。

注意這裡我先讓父類的析構函數不為虛函數(去掉virtual),我們看看輸出結果:

這裡它沒有調用子類的析構函數,因為他是一個父類類型指針,所以它只能調用父類的析構函數,無權訪問子類的析構函數,這種調用方法會導致內存泄漏,所以這裡就是有缺陷的,但是C++是不會允許自己有缺陷,他就會想辦法解決這個問題,這裡就運用到了我們下次要講的多態。現在我們讓加上為父類析構函數加上virtual,讓它變回虛函數,我們再運行一次程序的:

誒! 子類的虛函數又被調用了,這裡發生了什麼呢?? 來我們老方法打開監視窗口。

剛剛這種情況就是多態,多態性可以簡單地概括為「一個接口,多種方法」,程序在運行時才決定調用的函數,它是面向對象編程領域的核心概念。這個我們下一個博客專門會總結多態.

當然虛函數的知識點遠遠沒有這麼一點,這裡可能只是冰山一角,比如說菱形繼承的虛函數表是什麼樣?然後菱形虛擬繼承又是什麼樣子呢? 這些等我總結一下會專門寫一個博客來討論菱形繼承。虛函數表我們應該已經知道是什麼東西了,也知道單繼承和多繼承中它的應用,這些應該就足夠了,這些其實都是都是為你讓你更好的理解繼承和多態,當然你一定到分清楚重寫,重定義,重載的他們分別的含義是什麼. 這一塊可能有點繞,但是我們必須要掌握.

以上就是介紹有關C++中繼承與多態的基礎虛函數類的詳細內容,更多請關注其它相關文章!

更多技巧請《轉發 + 關注》哦!

文章來源: https://twgreatdaily.com/zh-cn/nrOY4G0BMH2_cNUgS0w8.html