C++虛函數(shù)到動(dòng)態(tài)綁定的核心原理深入分析
1. 多態(tài)的概念
多態(tài)(polymorphism)的概念:通俗來(lái)說(shuō)就是指多種形態(tài),多態(tài)分為編譯時(shí)多態(tài)(靜態(tài)多態(tài))和運(yùn)行時(shí)多態(tài)(動(dòng)態(tài)多態(tài))。編譯時(shí)多態(tài)(靜態(tài)多態(tài))主要是前面的函數(shù)重載和函數(shù)模板,它們傳不同類(lèi)型的參數(shù)就可以調(diào)用不同類(lèi)型的函數(shù),通過(guò)參數(shù)不同達(dá)到多種形態(tài),之所以叫做編譯時(shí)多態(tài),是因?yàn)樗鼈儗?shí)參傳給形參的參數(shù)匹配是在編譯時(shí)完成的,我們把編譯時(shí)一般歸為靜態(tài),運(yùn)行時(shí)歸為動(dòng)態(tài)。
運(yùn)行時(shí)多態(tài),具體點(diǎn)就是去完成某個(gè)行為(函數(shù)),傳不同的對(duì)象就可以完成不同的行為,就達(dá)到多種形態(tài)。就如買(mǎi)票這個(gè)行為,當(dāng)普通人買(mǎi)票時(shí)是全價(jià)買(mǎi)票,當(dāng)學(xué)生買(mǎi)票時(shí)是學(xué)生票,軍人買(mǎi)票時(shí)是優(yōu)先買(mǎi)票。再比如,同樣是動(dòng)物叫的一個(gè)行為,傳“貓”過(guò)去就是“喵喵”,傳“狗”過(guò)去就是“汪汪”。
2. 多態(tài)的定義及實(shí)現(xiàn)
2.1 多態(tài)的構(gòu)成條件
多態(tài)是一個(gè)繼承關(guān)系下的類(lèi)對(duì)象,去調(diào)用同一函數(shù)產(chǎn)生了不同行為。比如Student繼承了Person。Person對(duì)象買(mǎi)票全價(jià),Student對(duì)象優(yōu)惠買(mǎi)票。
2.1.1實(shí)現(xiàn)多態(tài)還有兩個(gè)必須重要條件
- 必須是基類(lèi)的指針或者引用調(diào)用虛函數(shù)
- 被調(diào)用的函數(shù)必須是虛函數(shù),并且完成了虛函數(shù)重寫(xiě)/覆蓋。
說(shuō)明:要實(shí)現(xiàn)多態(tài)的效果,第一必須是基類(lèi)的指針或者引用,因?yàn)橹挥谢?lèi)的指針或引用才能既指向基類(lèi)的對(duì)象,又能指向派生類(lèi)的對(duì)象。第二派生類(lèi)必須對(duì)基類(lèi)的虛函數(shù)完成重寫(xiě)/覆蓋,重寫(xiě)/覆蓋了基類(lèi)和派生類(lèi)之間才能有不同的函數(shù),多態(tài)的不同形態(tài)效果才能達(dá)成。

2.1.2 虛函數(shù)
類(lèi)成員函數(shù)前面加virtual修飾,那么這個(gè)成員函數(shù)被稱(chēng)為虛函數(shù)。??注意:非成員函數(shù)不能加virtual修飾。
class Person
{
public:
virtual void BuyTicket() { cout << "買(mǎi)票-全價(jià)" << endl;}
};
2.1.3 虛函數(shù)的重寫(xiě)/覆蓋
虛函數(shù)的重寫(xiě)/覆蓋:派生類(lèi)中有一個(gè)跟基類(lèi)完全相同的虛函數(shù)(即派生類(lèi)虛函數(shù)與基類(lèi)虛函數(shù)的返回值類(lèi)型,函數(shù)名字,參數(shù)列表完全相同),成派生類(lèi)的虛函數(shù)重寫(xiě)了基類(lèi)的虛函數(shù)。
注意:再重寫(xiě)基類(lèi)虛函數(shù)時(shí),派生類(lèi)的虛函數(shù)在不加virtual關(guān)鍵字時(shí),雖然也可以構(gòu)成重寫(xiě),因?yàn)槔^承后基類(lèi)的虛函數(shù)被繼承下來(lái)了,在派生類(lèi)中依舊保持虛函數(shù)的屬性),但是該種寫(xiě)法不是很規(guī)范,不建議這樣使用。在考試題中會(huì)經(jīng)常埋這樣一個(gè)坑,讓我們判斷是否構(gòu)成多態(tài)。
//指針
class Person
{
public:
virtual void BuyTicket()
{
cout << "買(mǎi)票-全價(jià)" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "買(mǎi)票-打折" << endl;
}
};
void Func(Person* ptr)
{
//這里雖然都是Person指針ptr調(diào)用BuyTicket
//但是和ptr沒(méi)關(guān)系,而是由ptr指向的對(duì)象決定的
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
//引用
class Person
{
public:
virtual void BuyTicket()
{
cout << "買(mǎi)票-全價(jià)" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "買(mǎi)票-打折" << endl;
}
};
void Func(Person& ptr)
{
//這里雖然都是Person指針ptr調(diào)用BuyTicket
//但是和ptr沒(méi)關(guān)系,而是由ptr指向的對(duì)象決定的
ptr.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}2.1.4 多態(tài)場(chǎng)景的一個(gè)選擇題
以下程序輸出結(jié)果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 編譯出錯(cuò) F: 以上都不正確
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();
return 0;
}
這里由this調(diào)用func,構(gòu)成多態(tài)必須是父類(lèi)的指針或者引用,這里是否構(gòu)成多態(tài)呢?這里的this是A*,所以它是一個(gè)父類(lèi)的指針。
繼承它會(huì)把父類(lèi)的成員拿下來(lái)是一種形象的說(shuō)法,其實(shí)不會(huì)把它拿下來(lái),它的復(fù)用是先去B里面去找有沒(méi)有test,再去A里面找,如果A里面還找不到就會(huì)報(bào)錯(cuò)。復(fù)用的本質(zhì)先去B里面找,再去A里面找。包括成員變量,如果A里面有一個(gè)_a,B里面有一個(gè)_b,它不是把A里面的拷貝下來(lái)在B里面也生成一份,是B對(duì)象生成的時(shí)候會(huì)先生成一個(gè)A對(duì)象的編譯器,在這個(gè)對(duì)象模型在內(nèi)存里面放的時(shí)候再放B的成員,不會(huì)說(shuō)是把分類(lèi)的成員函數(shù)和成員對(duì)象都拷貝一份下來(lái)。
所以說(shuō)第一個(gè)條件是滿(mǎn)足的。
第二個(gè)條件是虛函數(shù)的重寫(xiě),也就是func,func的函數(shù)名,參數(shù)類(lèi)型,返回值都相同,它是父類(lèi)的重寫(xiě),所以也滿(mǎn)足是虛函數(shù)。
所以是滿(mǎn)足多態(tài)的,滿(mǎn)足多態(tài)是指向誰(shuí)調(diào)用誰(shuí),p傳給了this,p是指向一個(gè)派生類(lèi)的B對(duì)象的。
滿(mǎn)足多態(tài)的情況下,調(diào)用子類(lèi)重寫(xiě)的虛函數(shù)

2.1.5 虛函數(shù)重寫(xiě)的一些其他問(wèn)題
協(xié)變(了解)
派生類(lèi)重寫(xiě)基類(lèi)虛函數(shù)時(shí),與基類(lèi)虛函數(shù)返回值類(lèi)型不同。即基類(lèi)虛函數(shù)返回基類(lèi)對(duì)象的指針或引用,派生類(lèi)虛函數(shù)返回派生類(lèi)對(duì)象的指針或引用時(shí),稱(chēng)為協(xié)變。
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket()
{
cout << "買(mǎi)票-全價(jià)" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "買(mǎi)票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}析構(gòu)函數(shù)的重寫(xiě)
基類(lèi)的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類(lèi)的析構(gòu)函數(shù)只要有定義,無(wú)論是否加virtual關(guān)鍵字都與基類(lèi)函數(shù)構(gòu)成重寫(xiě),雖然基類(lèi)與派生類(lèi)析構(gòu)函數(shù)的名字不同,看起來(lái)不符合重寫(xiě)規(guī)則,實(shí)際上編譯器對(duì)派生類(lèi)的析構(gòu)函數(shù)的名稱(chēng)做了特殊處理,編譯后析構(gòu)函數(shù)的名稱(chēng)同一處理成destructor,所以基類(lèi)的析構(gòu)函數(shù)加了virtual修飾,派生類(lèi)的析構(gòu)函數(shù)就構(gòu)成了重寫(xiě)。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 只有派生類(lèi)Student的析構(gòu)函數(shù)重寫(xiě)了Person的析構(gòu)函數(shù),下面的delete對(duì)象調(diào)用析構(gòu)函數(shù),才能
//構(gòu)成多態(tài),才能保證p1和p2指向的對(duì)象正確的調(diào)用析構(gòu)函數(shù)。
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}2.1.6 override和final關(guān)鍵字
C++對(duì)虛函數(shù)的重寫(xiě)要求比較嚴(yán)格,但是有些情況寫(xiě)由于疏忽,比如函數(shù)名寫(xiě)錯(cuò)或者是參數(shù)寫(xiě)錯(cuò)導(dǎo)致無(wú)法構(gòu)成重寫(xiě),而這種錯(cuò)誤是在編譯期間是不會(huì)報(bào)錯(cuò)的,只有在程序運(yùn)行時(shí)沒(méi)有得到預(yù)期結(jié)果,再進(jìn)行找錯(cuò)誤就會(huì)得不償失,因此C++11提供了override,可以幫助用戶(hù)檢測(cè)是否重寫(xiě)。如果我們不想讓派生類(lèi)重寫(xiě)這個(gè)虛函數(shù),那么就可以用final去修飾。
// error C3668: “Benz::Drive”: 包含重寫(xiě)說(shuō)明符“override”的方法沒(méi)有重寫(xiě)任何基類(lèi)方法
class Car {
public:
virtual void Dirve()
{}
};
class Benz :public Car {
public:
virtual void Drive() override
{
cout << "Benz-舒適" << endl;
}
};
int main()
{
return 0;
}
// error C3248: “Car::Drive”: 聲明為“final”的函數(shù)?法被“Benz::Drive”重寫(xiě)
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒適" << endl;
}
};
int main()
{
return 0;
}2.1.7 重載/重寫(xiě)/隱藏的對(duì)比
注意:這個(gè)概念對(duì)比經(jīng)常考?。?!

3. 純虛函數(shù)和抽象類(lèi)
在虛函數(shù)的后面加上=0,則這個(gè)函數(shù)為純虛函數(shù),純虛函數(shù)不需要定義實(shí)現(xiàn)(實(shí)現(xiàn)沒(méi)啥意義,因?yàn)橐慌缮?lèi)重寫(xiě),但語(yǔ)法上可以實(shí)現(xiàn)),只要聲明即可。包含純虛函數(shù)的類(lèi)稱(chēng)為抽象類(lèi),抽象類(lèi)不能實(shí)例化出對(duì)象,如果派生類(lèi)繼承后不重寫(xiě)虛函數(shù),那么派生類(lèi)也是抽象類(lèi)。純虛函數(shù)某種程度上強(qiáng)制了派生類(lèi)必須重寫(xiě)虛函數(shù),因?yàn)椴恢貙?xiě)實(shí)例化不出對(duì)象。
4. 多態(tài)的原理
4.1 虛函數(shù)表指針
下面編譯為32位程序的運(yùn)行結(jié)果是什么()
A. 編譯報(bào)錯(cuò) B. 運(yùn)行報(bào)錯(cuò) C. 8 D. 12
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}

4.2 多態(tài)的原理
4.2.1 多態(tài)是如何實(shí)現(xiàn)的
從底層的角度Func函數(shù)中ptr->BuyTicket(),是如何作為ptr指向Person對(duì)象調(diào)用Person::BuyTicket,ptr指向Student對(duì)象調(diào)用Student::BuyTicket的呢?通過(guò)下圖我們可以看到滿(mǎn)足多態(tài)條件后,底層不再是編譯時(shí),通過(guò)調(diào)用函數(shù)對(duì)象確定函數(shù)地址,而是運(yùn)行時(shí)到指向?qū)ο蟮奶摫碇校_定對(duì)應(yīng)的虛函數(shù)的地址。
第一張圖,ptr指向Person對(duì)象,調(diào)用的是Person的虛函數(shù);第二張圖ptr指向的是Student對(duì)象,調(diào)用的是Student的虛函數(shù)。




多態(tài):指向誰(shuí)調(diào)用誰(shuí)的虛函數(shù)
指向父類(lèi),運(yùn)行時(shí)到指向父類(lèi)對(duì)象的虛函數(shù)表中找到對(duì)應(yīng)的虛函數(shù)進(jìn)行調(diào)用
指向子類(lèi),運(yùn)行時(shí)到指向子類(lèi)對(duì)象切片出的父類(lèi)的虛函數(shù)表中找到對(duì)應(yīng)的虛函數(shù)進(jìn)行調(diào)用。
4.2.2 動(dòng)態(tài)綁定與靜態(tài)綁定
- 對(duì)不滿(mǎn)足多態(tài)條件(指針或者引用 + 調(diào)用虛函數(shù))的函數(shù)調(diào)用是在編譯時(shí)綁定,也就是編譯時(shí)確定調(diào)用函數(shù)的地址,叫做靜態(tài)綁定。
- 滿(mǎn)足多態(tài)條件的函數(shù)調(diào)用是在運(yùn)行時(shí)綁定,也就是在運(yùn)行時(shí)到指向?qū)ο蟮奶摵瘮?shù)表中,找到調(diào)用函數(shù)的地址,叫做動(dòng)態(tài)綁定。
// ptr是指針+BuyTicket是虛函數(shù)滿(mǎn)?多態(tài)條件。 // 這?就是動(dòng)態(tài)綁定,編譯在運(yùn)?時(shí)到ptr指向?qū)ο蟮奶摵瘮?shù)表中確定調(diào)?函數(shù)地址 ptr->BuyTicket(); 00EF2001 mov eax,dword ptr [ptr] 00EF2004 mov edx,dword ptr [eax] 00EF2006 mov esi,esp 00EF2008 mov ecx,dword ptr [ptr] 00EF200B mov eax,dword ptr [edx] 00EF200D call eax // BuyTicket不是虛函數(shù),不滿(mǎn)?多態(tài)條件。 // 這?就是靜態(tài)綁定,編譯器直接確定調(diào)?函數(shù)地址 ptr->BuyTicket(); 00EA2C91 mov ecx,dword ptr [ptr] 00EA2C94 call Student::Student (0EA153Ch)
4.2.3 虛函數(shù)表
- 基類(lèi)對(duì)象的虛函數(shù)表中存放,同類(lèi)型的對(duì)象公用同一張?zhí)摫恚煌?lèi)型的對(duì)象各自有獨(dú)立的虛表,所以基類(lèi)和派生類(lèi)各自有獨(dú)立的虛表。
- 派生類(lèi)由兩部分構(gòu)成,繼承下來(lái)的基類(lèi)和自己的成員,一般情況下,繼承下來(lái)的基類(lèi)中有虛函數(shù)表指針,自己就不再生成虛函數(shù)表指針。但是要注意的是這里繼承下來(lái)的虛函數(shù)表指針和基類(lèi)對(duì)象的虛函數(shù)表指針不是同一個(gè),就像基類(lèi)對(duì)象的成員,和派生類(lèi)繼承下來(lái)的基類(lèi)對(duì)象的成員也是獨(dú)立的。
- 派生類(lèi)中重寫(xiě)的基類(lèi)的虛函數(shù),派生類(lèi)的虛函數(shù)表中對(duì)應(yīng)的虛函數(shù)就會(huì)被覆蓋成派生類(lèi)重寫(xiě)的虛函數(shù)地址。
- 派生類(lèi)的虛函數(shù)表中包括:(1)基類(lèi)的虛函數(shù)地址(2)派生類(lèi)重寫(xiě)的虛函數(shù)地址完成覆蓋(3)派生類(lèi)自己的虛函數(shù)地址
- 虛函數(shù)表本質(zhì)上是一個(gè)存儲(chǔ)虛函數(shù)指針的指針數(shù)組,一般情況下這個(gè)指針數(shù)組最后面放了一個(gè)0x00000000標(biāo)記。(這個(gè)C++并沒(méi)有進(jìn)行規(guī)定,各個(gè)編譯器自行定義的,vs系列編譯器會(huì)再后?放個(gè)0x00000000標(biāo)記,g++系列編譯不會(huì)放)
- 虛函數(shù)存在哪的?虛函數(shù)和普通函數(shù)一樣,編譯好是一段指令,都是存放在代碼段的,只是虛函數(shù)的地址又存在虛表中。
- 虛函數(shù)表存放在哪的?這個(gè)問(wèn)題嚴(yán)格說(shuō)并沒(méi)有標(biāo)準(zhǔn)答案C++標(biāo)準(zhǔn)并沒(méi)有規(guī)定,我們寫(xiě)下面的代碼可以對(duì)比驗(yàn)證?下。vs下是存在代碼段(常量區(qū))
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base
{
public:
// 重寫(xiě)基類(lèi)的func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("棧:%p\n", &i);
printf("靜態(tài)區(qū):%p\n", &j);
printf("堆:%p\n", p1);
printf("常量區(qū):%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Base虛表地址:%p\n", *(int*)p3);
printf("Derive虛表地址:%p\n", *(int*)p4);
printf("虛函數(shù)地址:%p\n", &Base::func1);
printf("普通函數(shù)地址:%p\n", &Base::func5);
return 0;
}


以上就是C++虛函數(shù)到動(dòng)態(tài)綁定的核心原理深入分析的詳細(xì)內(nèi)容,更多關(guān)于C++虛函數(shù)到動(dòng)態(tài)綁定的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)酒店管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)酒店管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
Win10+VS2017新CUDA項(xiàng)目配置教程
這篇文章主要為大家詳細(xì)介紹了Win10+VS2017新CUDA項(xiàng)目配置教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09
淺談c++構(gòu)造函數(shù)問(wèn)題,初始化和賦值問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談c++構(gòu)造函數(shù)問(wèn)題,初始化和賦值問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12
C語(yǔ)言標(biāo)準(zhǔn)時(shí)間與秒單位相互轉(zhuǎn)換
這篇文章主要介紹了C語(yǔ)言標(biāo)準(zhǔn)時(shí)間與秒單位相互轉(zhuǎn)換,秒單位與標(biāo)準(zhǔn)時(shí)間的轉(zhuǎn)換方式,這份代碼一般用在嵌入式單片機(jī)里比較多,比如:設(shè)置RTC時(shí)鐘的時(shí)間,從RTC里讀取秒單位時(shí)間后,需要轉(zhuǎn)換成標(biāo)準(zhǔn)時(shí)間顯示。下文分享需要的小伙伴可以參考一下2022-05-05
C++使用模板實(shí)現(xiàn)單鏈表(類(lèi)外實(shí)現(xiàn))
這篇文章主要為大家詳細(xì)介紹了C++使用模板實(shí)現(xiàn)單鏈表的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
利用Matlab編寫(xiě)簡(jiǎn)易版連連看小游戲
連連看作為經(jīng)典的小游戲,一定是很多人的回憶吧。本文將用Matlab實(shí)現(xiàn)這一經(jīng)典的游戲,文中示例代碼具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
C語(yǔ)言數(shù)組按協(xié)議存儲(chǔ)與按協(xié)議解析數(shù)據(jù)的實(shí)現(xiàn)
今天小編就為大家分享一篇關(guān)于C語(yǔ)言數(shù)組按協(xié)議存儲(chǔ)與按協(xié)議解析數(shù)據(jù)的實(shí)現(xiàn),小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12

