C++之初識(shí)多態(tài)(Visual Studio 2019)的使用
此文章關(guān)于多態(tài)的代碼全部是使用Visua Studio2019 (x86) 實(shí)現(xiàn)的,C++多態(tài)在不同編譯器中的實(shí)現(xiàn)細(xì)節(jié)可能不同,所以部分情況下相同代碼運(yùn)行結(jié)果可能不同,在此聲明。
多態(tài)的概念
多態(tài),顧名思義,即多種形態(tài)。具體來說就是對(duì)于某個(gè)行為,不同的對(duì)象去完成該行為會(huì)產(chǎn)生不同的狀態(tài)。
就像 “叫” 這個(gè)行為,貓來完成時(shí)其叫聲是 “喵喵喵” ,而狗則是 “汪汪汪”。
還有坐公交車,老人和小孩坐車是免票,而其他人則是全價(jià)票。
同樣的一個(gè)動(dòng)作,不同的對(duì)象來執(zhí)行,其實(shí)現(xiàn)的方法不同。
多態(tài)的定義與實(shí)現(xiàn)
多態(tài)是建立在繼承基礎(chǔ)上的,在不同的繼承關(guān)系下的類對(duì)象,調(diào)用同一函數(shù),產(chǎn)生不同的行為。
在C++中,繼承體系下構(gòu)成多態(tài)需要滿足以下兩個(gè)條件:
- 1.必須通過基類指針或引用調(diào)用虛函數(shù)。
- 2.被調(diào)用函數(shù)必須是虛函數(shù),且派生類必須對(duì)基類虛函數(shù)進(jìn)行重寫。
虛函數(shù)
虛函數(shù):即被 virtual 修飾的類的成員函數(shù)。
class A {
public:
virtual int add(int a, int b) {
return a + b;
}
}使用virtual修飾虛函數(shù)時(shí),只需要修飾類內(nèi)部修飾即可。
class A {
public:
virtual void print_1();
};
virtual void A::print_1() {
cout << "1 : class A" << endl;
} 
虛函數(shù)的重寫
虛函數(shù)的重寫指的是:派生類中有一個(gè)與基類完全相同的虛函數(shù) (即派生類的虛函數(shù)與基類虛函數(shù)的返回值類型,參數(shù)列表,函數(shù)名完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。
class A {
public:
virtual void print_1() {
cout << "1 : class A" << endl;
}
};
class B : public A {
public:
virtual void print_1() {
cout << "1 : class B" << endl;
}
};
void func(A& a) {
a.print_1();
}
int main() {
A a;
B b;
func(a);
func(b);
return 0;
}
PS.子類重寫父類虛函數(shù)時(shí)子類虛函數(shù)可以不加virtual關(guān)鍵字,這樣也可以構(gòu)成重寫——繼承后基類的虛函數(shù)被繼承下來了,在派生類之中依舊保持虛函數(shù)屬性。但是這種寫法并不規(guī)范,并不建議使用。
虛函數(shù)重寫的兩個(gè)例外
1.協(xié)變
- 基類與派生類虛函數(shù)返回值不同
- 派生類重寫基類虛函數(shù)時(shí),若基類虛函數(shù)的返回類型是基類的指針或引用,新的返回類型是派生類的指針或引用,依舊可以構(gòu)成重寫。這樣的重寫被稱為返回類型協(xié)變。
class A {
public:
virtual A& print_1() {
cout << "1 : class A" << endl;
return *this;
}
};
class B : public A {
public:
virtual B& print_1() {
cout << "1 : class B" << endl;
return *this;
}
};需要注意的是,只有返回對(duì)應(yīng)類型的指針或引用才會(huì)構(gòu)成協(xié)變,單純返回派生類或基類類型并不構(gòu)成協(xié)變。
class A {
public:
virtual A print_1() {
cout << "1 : class A" << endl;
return *this;
}
};
class B : public A {
public:
virtual B print_1() {
cout << "1 : class B" << endl;
return *this;
}
};
2.析構(gòu)函數(shù)的重寫
- 基類與派生類析構(gòu)函數(shù)函數(shù)名不同
- 當(dāng)基類的析構(gòu)函數(shù)為虛函數(shù)時(shí),此時(shí)派生類的虛構(gòu)函數(shù)只要定義,無論是否被virtual關(guān)鍵字修飾,都與基類析構(gòu)函數(shù)構(gòu)成重寫。
class A {
public:
virtual ~A() {
cout << "A" << endl;
}
};
class B : public A {
public:
virtual ~B() {
cout << "B" << endl;
}
};
int main() {
A* a = new B;
delete a;
return 0;
} 
析構(gòu)函數(shù)的重寫最常見的應(yīng)用便是基類指針指向派生類對(duì)象的情況下,如果我們所使用的派生類對(duì)象是從堆上 new 來的,那么,在沒有重寫析構(gòu)函數(shù)的情況下,在基類對(duì)象 delete 時(shí),只會(huì)調(diào)用基類的析構(gòu)函數(shù)釋放基類的空間,而派生類對(duì)象所占用的空間則不會(huì)被釋放,就會(huì)導(dǎo)致內(nèi)存泄漏。
C++11 override & final
1. override
- 在C++中,override關(guān)鍵字用來表示當(dāng)前虛函數(shù)重寫了基類虛函數(shù)。
- 在函數(shù)比較多,繼承關(guān)系負(fù)責(zé)的情況下,override關(guān)鍵字可以起到提示編寫者與其后繼者某個(gè)函數(shù)重寫了基類的虛函數(shù)——也代表這個(gè)函數(shù)不是派生類中定義的。
- override關(guān)鍵字可以強(qiáng)制編譯器去檢查某個(gè)函數(shù)是否重寫了基類虛函數(shù),若否,則會(huì)報(bào)錯(cuò)。
class A {
public:
virtual void print() {
cout << "a" << endl;
}
};
class B : public A {
public:
virtual void print() override {
cout << "b" << endl;
}
};當(dāng)override修飾的函數(shù)并非重寫的基類虛函數(shù)時(shí),會(huì)報(bào)錯(cuò):
class A {
public:
virtual void print() {
cout << "a" << endl;
}
};
class B : public A {
public:
virtual void not_print() override {
cout << "b" << endl;
}
};
2.final
- final用于修飾虛函數(shù),表明該虛函數(shù)不能被重寫
- 我們是可以直接用final修飾父類虛函數(shù)的,表明該虛函數(shù)不能被重寫,但并不建議這樣做,虛類既然與平常的類不同,其實(shí)例化時(shí)自然會(huì)比普通的類多一份開,而final只是指明了這個(gè)虛函數(shù)不可以被重寫,但是虛類的相關(guān)開銷依舊在。
class A {
public:
int a;
virtual void print() final{
cout << "a" << endl;
}
};
class B : public A {
public:
int b;
void a_print() {
cout << "b" << endl;
}
};
class C {
public:
int a;
void print() {
cout << "a" << endl;
}
};
class D : public C {
public:
int b;
void a_print() {
cout << "b" << endl;
}
};
int main() {
B b;
D d;
b.b = 3;
b.a = 2;
d.a = 2;
d.b = 3;
while (0);
return 0;
} 

重載&重寫&重定義
重載
將語義或功能相似的函數(shù)用同一個(gè)名字表示。
條件:
- 1.兩個(gè)函數(shù)在同一個(gè)作用域
- 2.函數(shù)名相同
- 3.參數(shù)列表不同
- 4.返回值可以不同
重寫 (覆蓋)
覆蓋是C++中多態(tài)的基礎(chǔ),使派生類可以重新定義基類的虛函數(shù)。
條件:
- 1.兩個(gè)函數(shù)別屬于基類成員函數(shù)與派生類成員函數(shù)
- 2.兩個(gè)函數(shù)都必須是虛函數(shù)
- 3.函數(shù)名相同 (析構(gòu)函數(shù)除外,至少看起來不同)
- 4.返回值類型相同 (協(xié)變例外)
- 5.參數(shù)列表相同
- 6.函數(shù)的訪問權(quán)限可以不同 (如基類中的虛函數(shù)的訪問權(quán)限是 private ,派生類中可以重寫為public、protected)
重定義 (同名隱藏)
在繼承體系中,子類與父類中有同名成員,子類可以屏蔽父類指針對(duì)子類中父類成員的直接訪問。(但是可以使用 :: 訪問父類成員)
條件:
- 1.在不同作用域
- 2.函數(shù)名相同
- 3.返回值可以不同
- 4.在繼承體系中,只要不構(gòu)成重寫就是重定義
- 參數(shù)相同時(shí),基類沒有關(guān)鍵字virtual修飾,基類的函數(shù)被隱藏
- 參數(shù)不同時(shí),無論有沒有virtual關(guān)鍵字修飾,基類函數(shù)都被隱藏
class A {
void print() {
cout << "a" << endl;
}
};
class B : public A {
public:
int b;
void print() {
cout << "b" << endl;
}
};
int main() {
A a;
B b;
a.print();
b.print();
while (0);
return 0;
} 
純虛函數(shù)及抽象類
在有些時(shí)候,基類的某些行為在沒有被派生類重寫之前會(huì)非常不明確,或者說,不合常理,不好定義。比如我們先前舉過的例子——動(dòng)物的 “叫” 這個(gè)行為,在動(dòng)物這個(gè)基類中我們很難定義它具體是一種什么樣的行為,表達(dá)方式是怎樣的。我們是無法對(duì)這個(gè)行為做出具體的定義的,無法提供實(shí)現(xiàn)。但是作為一種 “共性” 我們又想要在基類中聲明它。
于是乎,在虛函數(shù)的基礎(chǔ)上,C++又提出了一種新的函數(shù)——純虛函數(shù)。
- 在虛函數(shù)后面寫上 = 0 ,則這個(gè)函數(shù)就是純虛函數(shù)。
- 包含純虛函數(shù)的類叫做接口類——也被稱為抽象類,抽象類不能實(shí)例化對(duì)象。
- 派生類繼承抽象類后,如果不重寫基類的純虛函數(shù),也無法實(shí)例化對(duì)象。
- 純虛函數(shù)規(guī)定了派生類必須重寫,同時(shí)也實(shí)現(xiàn)了接口繼承。
class A {
public:
virtual void print() = 0;
};
class B : public A {
public:
int b;
virtual void print() {
cout << "b" << endl;
}
};
int main() {
B b;
b.print();
while (0);
return 0;
}接口繼承與實(shí)現(xiàn)繼承
普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承了基類的函數(shù),可以使用函數(shù),這種繼承繼承的是函數(shù)的實(shí)現(xiàn)。
虛函數(shù)的繼承則是一種接口繼承,派生類繼承的只是虛函數(shù)的接口,目的是為了重寫,從而達(dá)成多態(tài),繼承的是一個(gè)接口。
所以,如果不實(shí)現(xiàn)多態(tài),就不要把函數(shù)設(shè)置為虛函數(shù)。
多態(tài)的原理
class A {
public:
int _a1;
int _a2;
virtual void study() {
cout << "study" << endl;
}
A()
:_a1(1)
,_a2(2)
{}
};
int main() {
A a;
return 0;
}
以上代碼中,類A的實(shí)例化對(duì)象a在實(shí)例化后,其內(nèi)存如上圖所示,明顯在我們所定義的變量a1, a2之前,還有著一串神奇的數(shù)字——00 75 9b 34。(據(jù)說有些編譯器這玩意可能在后邊)
單從表面上來看,它應(yīng)該是一個(gè)指針。而實(shí)質(zhì)上,它的確是一個(gè)指針。
在VS調(diào)試模式下的局部變量窗口中,我們可以很清楚的看到,這玩意還不是一個(gè)簡(jiǎn)單的指針,它是一個(gè)二級(jí)指針,指針名為 __vfptr (v——virtual f——function)我們通常稱其為虛函數(shù)表指針。

進(jìn)一步將這個(gè)虛函數(shù)表指針展開來看

很容易看到,這個(gè)虛函數(shù)表指針指向的對(duì)象是一個(gè)函數(shù)指針,而函數(shù)指針指向的對(duì)象則是我們代碼中所定義的虛函數(shù)——A::study(void)
一個(gè)含有虛函數(shù)的類中都至少有一個(gè)虛函數(shù)表指針,因?yàn)樘摵瘮?shù)的地址要被存放到虛函數(shù)表中。
虛函數(shù)表也簡(jiǎn)稱虛表。
下面,我們將之放入繼承體系之中來分析。
class A {
public:
int _a1;
int _a2;
virtual void func1() {
cout << "A::func1" << endl;
}
virtual void func2() {
cout << "A::func2" << endl;
}
virtual void func3() {
cout << "A::func3" << endl;
}
A()
:_a1(1)
,_a2(2)
{}
};
class B : public A{
public:
int _b1;
int _b2;
virtual void func1() {
cout << "B::func1" << endl;
}
virtual void func2() {
cout << "B::func2" << endl;
}
B()
:_b1(3)
,_b2(4)
{}
};
int main() {
A a;
B b;
return 0;
}
通過以上調(diào)試圖片,可以看出
1.基類中有一個(gè)虛函數(shù)表指針,虛函數(shù)表中存儲(chǔ)著三個(gè)基類虛函數(shù)的地址。
2.派生類對(duì)象中也有一個(gè)虛函數(shù)表指針,且該虛函數(shù)表由兩部分構(gòu)成——派生類重寫的虛函數(shù)以及從基類中繼承的虛函數(shù)。
3.基類對(duì)象與派生類對(duì)象的虛函數(shù)表中的內(nèi)容是不同的,由于我們?cè)谂缮惔a中重寫了虛函數(shù)func1以及func2,所以派生類的虛函數(shù)表中對(duì)應(yīng)位置存儲(chǔ)的是被派生類重寫后的B::func1(void)以及B::func2(void)。所以虛函數(shù)的重寫也被稱為覆蓋,覆蓋指的是虛表中虛函數(shù)的覆蓋,派生類重寫的虛函數(shù)地址覆蓋了基類的虛函數(shù)地址。重寫是語法層面的稱呼,覆蓋是原理層面的稱呼。
4.虛函數(shù)表本質(zhì)是一個(gè)存放虛函數(shù)指針的指針數(shù)組,一般情況下這個(gè)數(shù)組最后會(huì)放一個(gè)nullptr。
5.派生類虛表生成的過程如下:首先,將基類中虛表內(nèi)容拷貝一份到派生類之中;而后,如果派生類重寫了某個(gè)虛函數(shù),用派生類自身的虛函數(shù)的地址覆蓋虛函數(shù)表中基類虛函數(shù)的地址;最后,派生類將派生類之中新增的虛函數(shù)增加到虛函數(shù)表的最后。
6.虛函數(shù)表中只存放繼承或派生類自身定義的虛函數(shù),非虛函數(shù)不會(huì)被存放。
虛函數(shù)存在哪里?虛表存在哪里?
- 與普通的函數(shù)一樣,虛函數(shù)存放于代碼段中,只是它的指針被存儲(chǔ)到了虛表之中。虛表并不存放于對(duì)象之中,對(duì)象中存儲(chǔ)的是虛表指針。
- 虛表存放于代碼段之中。(個(gè)人實(shí)力原因暫時(shí)無法證明)
多態(tài)的原理
分析了這么多,多態(tài)的原理究竟是什么?這里就需要用到我們之前屢試不爽的方法——通過基類指針/引用調(diào)用函數(shù):
void Func(A* tmp) {
tmp->func1();
}
int main() {
A a;
B b;
Func(&a);
Func(&b);
return 0;
}通過對(duì)以上代碼進(jìn)行反匯編,我們可以看到在使用基類引用調(diào)用虛函數(shù)時(shí)的操作如下:

mov eax,dword ptr [tmp]
tmp中存儲(chǔ)的是基類對(duì)象的指針,將tmp中的值移動(dòng)到eax寄存器中。
mov edx,dword ptr [eax]
將eax的值指向的內(nèi)容存儲(chǔ)到edx中,即將基類對(duì)象前4個(gè)字節(jié)(虛表指針)移動(dòng)到edx中。
mov ecx,dword ptr [tmp]
取tmp指針存儲(chǔ)的值到ecx寄存器中
mov eax,dword ptr [edx]
取edx寄存器中的值指向的內(nèi)容,相當(dāng)于取了虛表的前四個(gè)字節(jié),即虛表中存儲(chǔ)的第一個(gè)虛函數(shù)的地址。如果我們此處調(diào)用的是fun2或者func3,則會(huì)在edx基礎(chǔ)上偏移一定字節(jié)。
call eax
調(diào)用eax寄存器中存儲(chǔ)的指針指向的函數(shù)。
動(dòng)態(tài)綁定與靜態(tài)綁定
- 靜態(tài)綁定:
靜態(tài)綁定又被稱為前期綁定或早綁定,在程序編譯期間便已經(jīng)確定了程序的行為,也被稱為靜態(tài)多態(tài)。
- 動(dòng)態(tài)綁定:
動(dòng)態(tài)綁定又被稱為后期綁定或晚綁定,在程序運(yùn)行期間,根據(jù)具體拿到的類型來確定程序的行為,調(diào)用具體的函數(shù),也稱為動(dòng)態(tài)多態(tài)。
多繼承下的虛函數(shù)表
我們上文所主要講解使用的是單繼承情況下的虛函數(shù)表。這里我們就簡(jiǎn)單研究下如果在多繼承體系下,虛函數(shù)表又會(huì)有怎樣的不同。
是將多個(gè)基類的虛函數(shù)表直接合為一個(gè),還是分別存儲(chǔ)與派生類對(duì)象中分屬于不同基類的存儲(chǔ)空間里。
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(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虛表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d個(gè)虛函數(shù)地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}可能是之前對(duì)VS的編譯器做過太多設(shè)置的修改,這個(gè)程序在Debug模式下運(yùn)行會(huì)發(fā)生崩潰,崩潰原因是虛函數(shù)表末尾并沒有按照我們預(yù)期的那樣綴上一個(gè)nullptr。
但是在release模式下可以正常地運(yùn)行,并得到一下結(jié)果:

通過觀察圖示結(jié)果我們可以很直觀的看到,多繼承情況下,派生類重寫的虛函數(shù)會(huì)放在第二個(gè)繼承基類部分的虛函數(shù)表中,而未重寫虛函數(shù)則會(huì)在第一個(gè)繼承基類部分的虛函數(shù)表中。
同時(shí),在多個(gè)基類中有相同的函數(shù)(函數(shù)名,返回值,參數(shù)列表)時(shí),默認(rèn)會(huì)重寫先繼承的基類中的對(duì)應(yīng)函數(shù)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺析C/C++中動(dòng)態(tài)鏈接庫的創(chuàng)建和調(diào)用
下面小編就為大家?guī)硪黄獪\析C/C++中動(dòng)態(tài)鏈接庫的創(chuàng)建和調(diào)用。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考,一起跟隨小編過來看看吧2016-05-05
Qt顯示QImage圖像在label上,并保持自適應(yīng)大小問題
這篇文章主要介紹了Qt顯示QImage圖像在label上,并保持自適應(yīng)大小問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
C語言生成隨機(jī)數(shù)以及設(shè)置隨機(jī)數(shù)范圍的方法(超詳細(xì))
文章介紹了C語言中生成隨機(jī)數(shù)的方法,包括使用`rand`和`srand`函數(shù),以及如何通過`time`函數(shù)設(shè)置隨機(jī)種子以確保每次運(yùn)行程序生成的隨機(jī)數(shù)序列不同,此外,還詳細(xì)講解了如何根據(jù)需要設(shè)置隨機(jī)數(shù)的范圍,需要的朋友可以參考下2025-02-02
利用C++實(shí)現(xiàn)一個(gè)線程安全的map
這篇文章主要為大家詳細(xì)介紹了如何利用C++實(shí)現(xiàn)一個(gè)線程安全的map(使用ChatCPT生成),代碼是通過兩輪對(duì)話完善的,感興趣的小伙伴可以了解一下2023-05-05
C/C++實(shí)現(xiàn)目錄監(jiān)視器的方法詳解
這篇文章主要介紹了C/C++ 實(shí)現(xiàn)目錄監(jiān)視器的方法,然后網(wǎng)上查到的基本就有三種方法,使用FindFirstChangeNotification等系列函數(shù),使用ReadDirectoryChangesW函數(shù)和使用change journals,本文使用了第二種方式來實(shí)現(xiàn)一個(gè)目錄監(jiān)視,需要的朋友可以參考下2024-04-04
C++ Invalidaterect()函數(shù)作用案例詳解
這篇文章主要介紹了C++ Invalidaterect()函數(shù)作用案例詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08

