C++?超詳細分析多態(tài)的原理與實現(xiàn)
多態(tài)的定義及實現(xiàn)
多態(tài)的概念:通俗來說,就是多種形態(tài),具體點就是去完成某個行為,當(dāng)不同的對象去完成時會產(chǎn)生出不同的狀態(tài)。
比如買票這個行為,當(dāng)普通人買票時,是全價買票;學(xué)生買票時,是半價買票;軍人買票時是優(yōu)先買票。
多態(tài)的構(gòu)成條件
多態(tài)是在不同繼承關(guān)系的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。比如Student繼承了Person。Person對象買票全價,Student對象買票半價。
在繼承中構(gòu)成多態(tài)還有兩個條件:
- 必須通過基類的指針或者引用調(diào)用虛函數(shù)
- 被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對基類的虛函數(shù)進行重寫。
虛函數(shù)重寫
虛函數(shù):即被virtual修飾的類成員函數(shù)。
class Person{
public:
virtual void BuyTicket() {
cout << "買票-全價" << endl;
}
};虛函數(shù)的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。
注:在重寫基類虛函數(shù)時,派生類的虛函數(shù)在不加virtual關(guān)鍵字時,雖然也可以構(gòu)成重寫(因為繼承后基類的虛函數(shù)被繼承下來了,在派生類依舊保持虛函數(shù)屬性),但該種寫法不是很規(guī)范。
class Person{
public:
virtual void BuyTicket(){ cout << "買票-全價" << endl; }
};
class Student : public Person{
public:
virtual void BuyTicket(){ cout << "買票-半價" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
} 

虛函數(shù)重寫的兩個例外:
1.協(xié)變(基類與派生類虛函數(shù)返回值類型不同) 派生類重寫基類虛函數(shù)時,與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對象的指針或者引用,派生類虛函數(shù)返回派生類對象的指針或引用時,稱為協(xié)變。
class A{};
class B : public A{};
class Person{
public:
virtual A* f(){ return new A; }
};
class Student : public Person {
public:
virtual B* f(){ return new B; }
}; 析構(gòu)函數(shù)的重寫 (基類與派生類析構(gòu)函數(shù)的名字不同) 如果基類的析構(gòu)函數(shù)為虛函數(shù),此時派生類析構(gòu)函數(shù)只要定義,無論是否加virtual關(guān)鍵字,都與基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然基類與派生類析構(gòu)函數(shù)名字不同。這里可以理解為編譯器對析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱同一處理成destructor
class Person{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}只有派生類Student的析構(gòu)函數(shù)重寫了Person的析構(gòu)函數(shù),這里的delete對象調(diào)用析構(gòu)函數(shù),才能構(gòu)成多態(tài),才能保證p1和p2指向的對象正確調(diào)用析構(gòu)函數(shù)。
C++11的override和final
從上面可以看出,C++對函數(shù)重寫的要求比較嚴格,但有些情況下由于疏忽,可能會導(dǎo)致函數(shù)名字母次序?qū)懛炊鵁o法構(gòu)成重寫,而這種錯誤在編譯期間是不會報錯的,但程序運行時不會得到預(yù)期結(jié)果。因此C++11提供了override和final兩個關(guān)鍵字,可以幫助用戶檢測是否重寫。
final:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫。即final一個類,這個類不能被繼承。
class Car
{
public:
virtual void Drive() final {}
};
class Benz : public Car
{
public:
virtual void Drive() { cout << "Benz-舒適" << endl; }
};override:檢查派生類虛函數(shù)是否重寫了基類某個虛函數(shù),如果沒有重寫,編譯器將會報錯。
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
virtual void Drive() override { cout << "Benz-舒適" << endl;}
};重載、覆蓋(重寫)、因此(重定義)的對比

抽象類
概念: 在虛函數(shù)的后面寫上 = 0,則這個函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫虛函數(shù),派生類才能實例化出對象。純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒適" << endl;
}
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}接口繼承和實現(xiàn)繼承
普通函數(shù)的繼承是一種實現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實現(xiàn)。虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達成多態(tài),繼承的是接口。所以如果不實現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
多態(tài)的原理
虛函數(shù)表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
public:
int _b = 1;
};sizeof(Base)是多少?

處理_b成員,還多一個_vfptr放在對象的前面(有些平臺可能會放到對象的最后面,這與平臺有關(guān)),對象中的這個指針叫做虛函數(shù)表指針(v代表virtual,f代表function)。一個含有虛函數(shù)的類中都至少有一個虛函數(shù)表指針,因為虛函數(shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也稱虛表。

針對上面的代碼做出以下改造:
1.增加一個派生類Derive去繼承Base
2.Derive中重寫Func1
3.Base再增加一個虛函數(shù)Func2和一個普通函數(shù)Func3
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Fun3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::unc1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
我們發(fā)現(xiàn):
1.派生類對象d中也有一個虛表指針,d對象由兩部分構(gòu)成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員。
2.基類b對象和派生類d對象虛表是不一樣的,這里我們發(fā)現(xiàn)Func1完成了重寫,所以d的需表中存的是重寫的Derive::Func1,所以虛函數(shù)的重寫也叫做覆蓋,覆蓋就是指需表中虛函數(shù)的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
3.另外Func2繼承下來后是虛函數(shù),所以放進了虛表,F(xiàn)unc3也繼承下來了,但不是虛函數(shù),所以不會放進虛表。
4.虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組 ,一般情況這個數(shù)組最后面放了一個nullptr。
5.總結(jié)一下派生類的虛表生成:a.先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中。b.如果派生類重寫了基類中某個虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)。c.派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。
6.虛表存的是虛函數(shù)指針,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣,都存在代碼段,只是它的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。
動態(tài)綁定與靜態(tài)綁定
1.靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也成為靜態(tài)多態(tài)。比如:函數(shù)重載。
2.動態(tài)綁定又稱為后期綁定(晚綁定),是在程序運行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也成為動態(tài)多態(tài)。
單繼承和多繼承關(guān)系的虛函數(shù)表
單繼承中的虛函數(shù)表
使用代碼打印出虛表中的函數(shù):
取出b、d對象的頭4bytes,就是虛表的指針,虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,這個數(shù)組最后面放了一個nullptr
1.先取b的地址,強轉(zhuǎn)成一個int的指針
2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針。
3.再強轉(zhuǎn)成VFPTR,因為虛表就是一個存VFPTR類型(虛函數(shù)指針類型)的數(shù)組。
4.虛表指針傳遞給PrintVFT進行打印虛表。
5.注意:這個打印虛表的代碼經(jīng)常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后沒有放nullptr,導(dǎo)致越界,這時編譯器的問題。需要點目錄欄的-生成-清理解決方案,再編譯。
typedef void(*VFPTR)();
void PrintVFT(VFPTR vft[])
{
printf("%p\n", vft);
for (size_t i = 0; vft[i] != nullptr; ++i)
{
printf("vft[%d]:%p->", i, vft[i]);
vft[i]();
}
printf("\n");
}
int main()
{
Base b;
Derive d;
PrintVFT((VFPTR*)(*(int*)&b));
PrintVFT((VFPTR*)(*(int*)&d));
return 0;
}
int main()
{
Base bb;
int a = 0;
int* p1 = new int;
const char* p2 = "hello world";
auto pf = PrintVFT;
static int b = 1;
printf("棧幀變量:%p\n", &a);
printf("堆變量:%p\n", p1);
printf("常量區(qū)變量:%p\n", p2);
printf("函數(shù)地址變量:%p\n", pf);
printf("靜態(tài)區(qū)變量:%p\n", &b);
printf("虛函數(shù)表地址:%p\n", *(int*)&bb);
return 0;
}

從以上代碼也可以看出,虛函數(shù)表地址與常量區(qū)變量地址非常相近,它也存在常量區(qū)。
多繼承中的虛函數(shù)表
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 << "Base2::func1" << endl; }
virtual void func3() { cout << "Base2::func3" << endl; }
private:
int d1;
};
int main()
{
Derive d;
PrintVFT((VFPTR*)(*(int*)&d));
PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
Base1* p1 = &d;
p1->func1();
Base2* p2 = &d;
p2->func1();
return 0;
}

多繼承派生類的未重寫的虛函數(shù)放在第一個繼承基類部分的虛函數(shù)表中。

常見問題
1.inline函數(shù)可以是虛函數(shù)嗎
inline函數(shù)沒有地址,虛函數(shù)需要放到虛表中,這樣是矛盾的。但VS編譯器此時就會忽略inline屬性,這個函數(shù)就不再是inline。
2.靜態(tài)成員可以是虛函數(shù)嗎? 不能
靜態(tài)成員函數(shù)沒有this指針,使用類型::成員函數(shù)的調(diào)用方式無法訪問虛函數(shù)表,所以靜態(tài)成員函數(shù)無法放進虛函數(shù)表。
3.構(gòu)造函數(shù)可以是虛函數(shù)嗎? 不能
(1)因為虛函數(shù)表指針是在構(gòu)造函數(shù)初始化列表階段初始化的。如果構(gòu)造函數(shù)是虛函數(shù),那么調(diào)用構(gòu)造函數(shù)時對象中虛表指針都沒有初始化。
(2)沒有意義,因為子類中要調(diào)用父類構(gòu)造函數(shù)初始化。寫成虛函數(shù)目的是多態(tài),構(gòu)造函數(shù)不需要多態(tài)的方式。
4.析構(gòu)函數(shù)可以是虛函數(shù)嗎?
是,最好把基類的析構(gòu)函數(shù)定義成虛函數(shù)。
5.對象訪問普通函數(shù)快還是虛函數(shù)更快?
如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調(diào)用的普通函數(shù)快,因為構(gòu)成多態(tài),運行時調(diào)用虛函數(shù)需要到虛函數(shù)表中去查找。
6.虛函數(shù)表是在什么階段生成的?存在哪里?
虛函數(shù)表是在編譯階段就生成的,一般情況下存在代碼段(常量區(qū))。
7.抽象類的作用?
抽象類強制重寫了虛函數(shù),另外抽象類體現(xiàn)出了接口繼承關(guān)系。
到此這篇關(guān)于C++ 超詳細分析多態(tài)的原理與實現(xiàn)的文章就介紹到這了,更多相關(guān)C++ 多態(tài)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C語言中不同類型的數(shù)據(jù)轉(zhuǎn)換規(guī)則
這篇文章給大家講解不同類型數(shù)據(jù)間的混合運算與類型轉(zhuǎn)換,有自動類型轉(zhuǎn)換和強制類型轉(zhuǎn)換,針對每種轉(zhuǎn)換方法小編給大家介紹的非常詳細,需要的朋友參考下吧2021-07-07

