C++中繼承與多態(tài)的基礎(chǔ)虛函數(shù)類(lèi)詳解
前言
本文主要給大家介紹了關(guān)于C++中繼承與多態(tài)的基礎(chǔ)虛函數(shù)類(lèi)的相關(guān)內(nèi)容,分享出來(lái)供大家參考學(xué)習(xí),下面話不多說(shuō)了,來(lái)一起看看詳細(xì)的介紹吧。
虛函數(shù)類(lèi)
繼承中我們經(jīng)常提到虛擬繼承,現(xiàn)在我們來(lái)探究這種的虛函數(shù),虛函數(shù)類(lèi)的成員函數(shù)前面加virtual關(guān)鍵字,則這個(gè)成員函數(shù)稱(chēng)為虛函數(shù),不要小看這個(gè)虛函數(shù),他可以解決繼承中許多棘手的問(wèn)題,而對(duì)于多態(tài)那他更重要了,沒(méi)有它就沒(méi)有多態(tài),所以這個(gè)知識(shí)點(diǎn)非常重要,以及后面介紹的虛函數(shù)表都極其重要,一定要認(rèn)真的理解~ 現(xiàn)在開(kāi)始概念虛函數(shù)就又引出一個(gè)概念,那就是重寫(xiě)(覆蓋),當(dāng)在子類(lèi)的定義了一個(gè)與父類(lèi)完全相同的虛函數(shù)時(shí),則稱(chēng)子類(lèi)的這個(gè)函數(shù)重寫(xiě)(也稱(chēng)覆蓋)了父類(lèi)的這個(gè)虛函數(shù)。這里先提一下虛函數(shù)表,后面會(huì)講到的,重寫(xiě)就是將子類(lèi)里面的虛函數(shù)表里的被重寫(xiě)父類(lèi)的函數(shù)地址全都改成子類(lèi)函數(shù)的地址。
純虛函數(shù)
在成員函數(shù)的形參后面寫(xiě)上=0,則成員函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類(lèi)叫做抽象類(lèi)(也叫接口類(lèi))
抽象類(lèi)不能實(shí)例化出對(duì)象。純虛函數(shù)在派生類(lèi)中重新定義以后,派生類(lèi)才能實(shí)例化出對(duì)象。
看一個(gè)例子:
class Person
{
virtual void Display () = 0; // 純虛函數(shù)
protected :
string _name ; // 姓名
};
class Student : public Person
{};
先總結(jié)一下概念:
1.派生類(lèi)重寫(xiě)基類(lèi)的虛函數(shù)實(shí)現(xiàn)多態(tài),要求函數(shù)名、參數(shù)列表、返回值完全相同。(協(xié)變除外)
2.基類(lèi)中定義了虛函數(shù),在派生類(lèi)中該函數(shù)始終保持虛函數(shù)的特性。
3.只有類(lèi)的成員函數(shù)才能定義為虛函數(shù)。
4.靜態(tài)成員函數(shù)不能定義為虛函數(shù)。
5.如果在類(lèi)外定義虛函數(shù),只能在聲明函數(shù)時(shí)加virtual,類(lèi)外定義函數(shù)時(shí)不能加virtual。
6.不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)里面調(diào)用虛函數(shù),在構(gòu)造函數(shù)和析構(gòu)函數(shù)中,對(duì)象是不完整的,可能會(huì)發(fā)生未定義的行為。
7.最好把基類(lèi)的析構(gòu)函數(shù)聲明為虛函數(shù)。(why?另外析構(gòu)函數(shù)比較特殊,因?yàn)榕缮?lèi)的析構(gòu)函數(shù)跟基類(lèi)的析構(gòu)函數(shù)名稱(chēng)不一樣,但是構(gòu)成覆蓋,這里是因?yàn)榫幾g器做了特殊處理)
8.構(gòu)造函數(shù)不能為虛函數(shù),雖然可以將operator=定義為虛函數(shù),但是最好不要將operator=定義為虛函數(shù),因?yàn)槿菀资褂脮r(shí)容易引起混淆.

上面概念大家可能都會(huì)問(wèn)一句為什么要這樣? 這些內(nèi)容在接下來(lái)的知識(shí)里都能找到答案~ 好了那么我們今天的主角虛函數(shù)登場(chǎng)!!!!
何為虛函數(shù)表,我們寫(xiě)一個(gè)程序,調(diào)一個(gè)監(jiān)視窗口就知道了。
下面是一個(gè)有虛函數(shù)的類(lèi):
#include<iostream>
#include<windows.h>
using namespacestd;
class Base
{
public:
virtual void func1()
{}
virtual void func2()
{}
private:
inta;
};
void Test1()
{
Base b1;
}
int main()
{
Test1();
system("pause");
return0;
}
我們現(xiàn)在點(diǎn)開(kāi)b1的監(jiān)視窗口

這里面有一個(gè)_vfptr,而這個(gè)_vfptr指向的東西就是我們的主角,虛函數(shù)表。一會(huì)大家就知道了,無(wú)論是單繼承還是多繼承甚至于我們的菱形繼承虛函數(shù)表都會(huì)有不同的形態(tài),虛函數(shù)表是一個(gè)很有趣的東西。

我們來(lái)研究一下單繼承的內(nèi)存格局
仔細(xì)看下面代碼:
#include<iostream>
#include<windows.h>
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;
};
對(duì)于Derive類(lèi)來(lái)說(shuō),我們覺(jué)得它的虛表里會(huì)有什么?
首先子類(lèi)的fun1()重寫(xiě)了父類(lèi)的fun1() ,虛表里存的是子類(lèi)的fun1() ,接下來(lái)父類(lèi)的fun2() ,子類(lèi)的fun3() , fun4()都是虛函數(shù),所以虛表里會(huì)有4個(gè)元素,分別為子類(lèi)的fun1() ,父類(lèi)fun2() ,子類(lèi)fun3() ,子類(lèi)fun4() 。然后我們調(diào)出監(jiān)視窗口看我們想的到底對(duì)不對(duì)呢?

我預(yù)計(jì)應(yīng)該是看到fun1() ,fun2() ,fun3() ,fun4()的虛函數(shù)表,但是呢這里監(jiān)視窗口只有兩個(gè)fun1() , fun2() ,難道我們錯(cuò)了????
這里并不是這樣的,只有自己靠得住,我覺(jué)得這里的編譯器有問(wèn)題,那我們就得自己探索一下了。 但是在探索之前我們必須來(lái)實(shí)現(xiàn)一個(gè)可以打印虛函數(shù)表的函數(shù)。
typedef void(*FUNC)(void);
void PrintVTable(int* VTable)
{
cout<< " 虛表地址"<<VTable<< endl;
for(inti = 0;VTable[i] != 0; ++i)
{
printf(" 第%d個(gè)虛函數(shù)地址 :0X%x,->", i,VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout<< endl;
}
int main()
{
Derive d1;
PrintVTable((int*)(*(int*)(&d1)));
system("pause");
return0;
}
下圖來(lái)說(shuō)一下他的緣由:

我們來(lái)使用這個(gè)函數(shù),該函數(shù)代碼如下:
//單繼承
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<< " 虛表地址"<<VTable<< endl;
for(inti = 0;VTable[i] != 0; ++i)
{
printf(" 第%d個(gè)虛函數(shù)地址 :0X%x,->", i,VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout<< endl;
}
int main()
{
Derive d1;
PrintVTable((int*)(*(int*)(&d1))); //重點(diǎn)
system("pause");
return0;
}
這里我就要講講這個(gè)傳參了,注意這里的傳參不好理解,應(yīng)當(dāng)細(xì)細(xì)的"品味".
PrintVTable((int*)(*(int*)(&d1)));
首先我們肯定要拿到d1的首地址,把它強(qiáng)轉(zhuǎn)成int*,讓他讀取到前4個(gè)字節(jié)的內(nèi)容(也就是指向虛表的地址),再然后對(duì)那個(gè)地址解引用,我們已經(jīng)拿到虛表的首地址的內(nèi)容(虛表里面存儲(chǔ)的第一個(gè)函數(shù)的地址)了,但是此時(shí)這個(gè)變量的類(lèi)型解引用后是int,不能夠傳入函數(shù),所以我們?cè)賹?duì)他進(jìn)行一個(gè)int*的強(qiáng)制類(lèi)型轉(zhuǎn)換,這樣我們就傳入?yún)?shù)了,開(kāi)始函數(shù)執(zhí)行了,我們一切都是在可控的情況下使用強(qiáng)轉(zhuǎn),使用強(qiáng)轉(zhuǎn)你必須要特別清楚的知道內(nèi)存的分布結(jié)構(gòu)。
最后我們來(lái)看看輸出結(jié)果:

到底打印的對(duì)不對(duì)呢? 我們驗(yàn)證一下: 
這里我們通過(guò)&d1的首地址找到虛表的地址,然后訪問(wèn)地址查看虛表的內(nèi)容,驗(yàn)證我們自己寫(xiě)的這個(gè)函數(shù)是正確的。(這里VS還有一個(gè)bug,當(dāng)你第一次打印虛表時(shí)程序可能會(huì)崩潰,不要擔(dān)心你重新生成解決方案,再運(yùn)行一次就可以了。因?yàn)楫?dāng)你第一次打印是你虛表最后一個(gè)地方可能沒(méi)有放0,所以你就有可能停不下來(lái)然后崩潰。)我們可以看到d1的虛表并不是監(jiān)視器里面打印的那個(gè)樣子的,所以有時(shí)候VS也會(huì)有bug,不要太相信別人,還是自己靠得住。哈哈哈,臭美一下~
我們來(lái)研究一下多繼承的內(nèi)存格局
探究完了單繼承,我們來(lái)看看多繼承,我們還是通過(guò)代碼調(diào)試的方法來(lái)探究對(duì)象模型
看如下代碼:
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個(gè)虛函數(shù)地址 :0X%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
void Test1()
{
Derive d1;
//Base2虛函數(shù)表在對(duì)象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;
}
現(xiàn)在我們現(xiàn)在知道會(huì)有兩個(gè)虛函數(shù)表,分別是Base1和Base2的虛函數(shù)表,但是呢!我們的子類(lèi)里的fun3()函數(shù)怎么辦?它是放在Base1里還是Base2里還是自己開(kāi)辟一個(gè)虛函數(shù)表呢?我們先調(diào)一下監(jiān)視窗口:

監(jiān)視窗口又不靠譜了。。。。完全沒(méi)有找到fun3().那我們直接看打印出來(lái)的虛函數(shù)表。

現(xiàn)在很清楚了,fun3()在Base1的虛函數(shù)表中,而B(niǎo)ase1是先繼承的類(lèi),好了現(xiàn)在我們記住這個(gè)結(jié)論,當(dāng)涉及多繼承時(shí),子類(lèi)的虛函數(shù)會(huì)存在先繼承的那個(gè)類(lèi)的虛函數(shù)表里。記住了!
我們現(xiàn)在來(lái)看多繼承的對(duì)象模型:

現(xiàn)在我們來(lái)結(jié)束一下上面我列的那么多概念現(xiàn)在我來(lái)逐一的解釋為什么要這樣.
1.為什么靜態(tài)成員函數(shù)不能定義為虛函數(shù)?
因?yàn)殪o態(tài)成員函數(shù)它是一個(gè)大家共享的一個(gè)資源,但是這個(gè)靜態(tài)成員函數(shù)沒(méi)有this指針,而且虛函數(shù)變只有對(duì)象才能能調(diào)到,但是靜態(tài)成員函數(shù)不需要對(duì)象就可以調(diào)用,所以這里是有沖突的.
2.為什么不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)里面調(diào)用虛函數(shù)?
構(gòu)造函數(shù)當(dāng)中不適合用虛函數(shù)的原因是:在構(gòu)造對(duì)象的過(guò)程中,還沒(méi)有為“虛函數(shù)表”分配內(nèi)存。所以,這個(gè)調(diào)用也是違背先實(shí)例化后調(diào)用的準(zhǔn)則析構(gòu)函數(shù)當(dāng)中不適用虛函數(shù)的原因是:一般析構(gòu)函數(shù)先析構(gòu)子類(lèi)的,當(dāng)你在父類(lèi)中調(diào)用一個(gè)重寫(xiě)的fun()函數(shù),虛函數(shù)表里面就是子類(lèi)的fun()函數(shù),這時(shí)候已經(jīng)子類(lèi)已經(jīng)析構(gòu)了,當(dāng)你調(diào)用的時(shí)候就會(huì)調(diào)用不到.
現(xiàn)在我在寫(xiě)最后一個(gè)知識(shí)點(diǎn),為什么盡量最好把基類(lèi)的析構(gòu)函數(shù)聲明為虛函數(shù)??
現(xiàn)在我們?cè)賮?lái)寫(xiě)一個(gè)例子,我們都知道平時(shí)正常的實(shí)例化對(duì)象然后再釋放是沒(méi)有一點(diǎn)問(wèn)題的,但是現(xiàn)在我這里舉一個(gè)特例:
我們都知道父類(lèi)的指針可以指向子類(lèi),現(xiàn)在呢我們我們用一個(gè)父類(lèi)的指針new一個(gè)子類(lèi)的對(duì)象。
//多態(tài) 析構(gòu)函數(shù)
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;
}
這里面可能會(huì)有下一篇要說(shuō)的多態(tài),所以可能理解起來(lái)會(huì)費(fèi)勁一點(diǎn)。
注意這里我先讓父類(lèi)的析構(gòu)函數(shù)不為虛函數(shù)(去掉virtual),我們看看輸出結(jié)果:

這里它沒(méi)有調(diào)用子類(lèi)的析構(gòu)函數(shù),因?yàn)樗且粋€(gè)父類(lèi)類(lèi)型指針,所以它只能調(diào)用父類(lèi)的析構(gòu)函數(shù),無(wú)權(quán)訪問(wèn)子類(lèi)的析構(gòu)函數(shù),這種調(diào)用方法會(huì)導(dǎo)致內(nèi)存泄漏,所以這里就是有缺陷的,但是C++是不會(huì)允許自己有缺陷,他就會(huì)想辦法解決這個(gè)問(wèn)題,這里就運(yùn)用到了我們下次要講的多態(tài)?,F(xiàn)在我們讓加上為父類(lèi)析構(gòu)函數(shù)加上virtual,讓它變回虛函數(shù),我們?cè)龠\(yùn)行一次程序的:

誒! 子類(lèi)的虛函數(shù)又被調(diào)用了,這里發(fā)生了什么呢?? 來(lái)我們老方法打開(kāi)監(jiān)視窗口。

剛剛這種情況就是多態(tài),多態(tài)性可以簡(jiǎn)單地概括為“一個(gè)接口,多種方法”,程序在運(yùn)行時(shí)才決定調(diào)用的函數(shù),它是面向?qū)ο缶幊填I(lǐng)域的核心概念。這個(gè)我們下一個(gè)博客專(zhuān)門(mén)會(huì)總結(jié)多態(tài).
當(dāng)然虛函數(shù)的知識(shí)點(diǎn)遠(yuǎn)遠(yuǎn)沒(méi)有這么一點(diǎn),這里可能只是冰山一角,比如說(shuō)菱形繼承的虛函數(shù)表是什么樣?然后菱形虛擬繼承又是什么樣子呢? 這些等我總結(jié)一下會(huì)專(zhuān)門(mén)寫(xiě)一個(gè)博客來(lái)討論菱形繼承。虛函數(shù)表我們應(yīng)該已經(jīng)知道是什么東西了,也知道單繼承和多繼承中它的應(yīng)用,這些應(yīng)該就足夠了,這些其實(shí)都是都是為你讓你更好的理解繼承和多態(tài),當(dāng)然你一定到分清楚重寫(xiě),重定義,重載的他們分別的含義是什么. 這一塊可能有點(diǎn)繞,但是我們必須要掌握.
總結(jié)
以上就是對(duì)虛函數(shù)的一點(diǎn)簡(jiǎn)單見(jiàn)解,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- 深入解析C++中的虛函數(shù)與多態(tài)
- C++中的多態(tài)與虛函數(shù)的內(nèi)部實(shí)現(xiàn)方法
- c++語(yǔ)言中虛函數(shù)實(shí)現(xiàn)多態(tài)的原理詳解
- 深入淺析C++多態(tài)性與虛函數(shù)
- 詳細(xì)分析C++ 多態(tài)和虛函數(shù)
- 虛函數(shù)表-C++多態(tài)的實(shí)現(xiàn)原理解析
- C++的多態(tài)和虛函數(shù)你真的了解嗎
- C++的多態(tài)與虛函數(shù)你了解嗎
- 深入了解C++的多態(tài)與虛函數(shù)
- C++?多態(tài)虛函數(shù)的底層原理深入理解
相關(guān)文章
解析為何要關(guān)閉數(shù)據(jù)庫(kù)連接,可不可以不關(guān)閉的問(wèn)題詳解
本篇文章是對(duì)為何要關(guān)閉數(shù)據(jù)庫(kù)連接,可不可以不關(guān)閉的問(wèn)題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
Windows網(wǎng)絡(luò)編程之winsock實(shí)現(xiàn)文件傳輸示例
這篇文章主要介紹了Windows網(wǎng)絡(luò)編程之winsock實(shí)現(xiàn)文件傳輸示例,對(duì)于學(xué)習(xí)Windows網(wǎng)絡(luò)程序設(shè)計(jì)來(lái)說(shuō)具有很好的學(xué)習(xí)借鑒價(jià)值,需要的朋友可以參考下2014-08-08
C語(yǔ)言植物大戰(zhàn)數(shù)據(jù)結(jié)構(gòu)二叉樹(shù)遞歸
這篇文章主要為大家介紹了C語(yǔ)言植物大戰(zhàn)數(shù)據(jù)結(jié)構(gòu)二叉樹(shù)遞歸,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
C++實(shí)現(xiàn)賓館房間管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)賓館房間管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05

