簡介
多態(tài)可以分為靜態(tài)和動(dòng)態(tài)倘感。
靜態(tài)多態(tài)是指當(dāng)調(diào)用名字相同的函數(shù)時(shí)放坏,編譯器在編譯期就能根據(jù)函數(shù)簽名的不同推導(dǎo)出所調(diào)用的函數(shù)。
注釋:函數(shù)簽名包括了函數(shù)名侠仇,參數(shù)列表轻姿,類名犁珠,命名空間逻炊,const關(guān)鍵字;
其實(shí)現(xiàn)也稱為函數(shù)重載(overload)犁享,將類似功能的函數(shù)統(tǒng)一命名余素,增強(qiáng)代碼的可讀性。
動(dòng)態(tài)多態(tài)一般體現(xiàn)在基類的指針可以指向一個(gè)派生類的對象炊昆,當(dāng)該指針在運(yùn)行期調(diào)用某一個(gè)虛函數(shù)時(shí)桨吊,根據(jù)所指向的對象類型的不同而調(diào)用相應(yīng)的虛函數(shù)(《c++ primier》 pp.536 “被調(diào)用的函數(shù)是與綁定到指針或引用上的對象的動(dòng)態(tài)類型相匹配的那一個(gè)”)。
當(dāng)利用設(shè)計(jì)模式來解決問題時(shí)凤巨,會(huì)利用動(dòng)態(tài)多態(tài)來實(shí)現(xiàn)视乐,如達(dá)到“對擴(kuò)展開放,對修改封閉”的目標(biāo)敢茁。
下面將主要介紹動(dòng)態(tài)多態(tài)的幾個(gè)方面:虛函數(shù)表佑淀,虛函數(shù)表指針,動(dòng)態(tài)綁定彰檬。
虛函數(shù)表指針和虛函數(shù)表
多態(tài)的實(shí)現(xiàn)是基于虛函數(shù)表指針和虛函數(shù)表伸刃。
舉例來說:
基類有兩個(gè)虛函數(shù)vf1和vf2谎砾,在編譯時(shí)編譯器會(huì)將這兩個(gè)虛函數(shù)的地址放在基類的虛函數(shù)表(vtbl)內(nèi),供該類型的對象公用捧颅。然后編譯器給基類增加一個(gè)成員變量vptr景图,即虛函數(shù)表指針,然后在基類的構(gòu)造函數(shù)中增加一句賦值的命令碉哑,當(dāng)實(shí)例化對象時(shí)挚币,通過調(diào)用構(gòu)造函數(shù)令vptr指向vtbl。
派生類由于繼承自該基類扣典,編譯器判斷基類有虛函數(shù)忘晤,那么派生類也會(huì)有一個(gè)vptr成員變量指向派生類自己的vtbl。如果派生類沒有重寫基類的虛函數(shù)激捏,則其vtbl中的虛函數(shù)地址同基類的vtbl中虛函數(shù)的地址设塔。
當(dāng)派生類重寫了某個(gè)虛函數(shù)時(shí),派生類的vtbl中相應(yīng)的函數(shù)地址就會(huì)更新為所重寫的虛函數(shù)的地址远舅。
另外闰蛔,某個(gè)類的虛函數(shù)表是該類所共有的,因此會(huì)放在數(shù)據(jù)段图柏,而類對象的虛函數(shù)表指針作為成員變量存在序六。
上述文字可以用下圖表示(來自侯捷老師的視頻):
另外,如果在派生類里面又定義了一個(gè)虛函數(shù)vf3蚤吹,而vf3不在基類中例诀,那么指向基類的指針并不會(huì)調(diào)用vf3,因?yàn)槠洳辉诨惖睦^承體系中裁着。
動(dòng)態(tài)綁定
要理解動(dòng)態(tài)綁定繁涂,自然要理解靜態(tài)綁定。
靜態(tài)綁定是指編譯器在編譯階段就能確定函數(shù)的地址二驰,因此函數(shù)調(diào)用的匯編代碼是call func_address(如圖中的14行扔罪,CALL直接綁定了Base的print()函數(shù))。
而由于要實(shí)現(xiàn)多態(tài)桶雀,那么在編譯階段矿酵,編譯器是不知道所調(diào)用的函數(shù)的地址的,那么需要通過虛函數(shù)表指針和虛函數(shù)表來確定函數(shù)地址(如上圖的21-27行矗积,CALL的地址是經(jīng)過偏移的)全肮,這個(gè)過程就是動(dòng)態(tài)綁定。
動(dòng)態(tài)綁定需要滿足三個(gè)條件:
- 通過指針調(diào)用函數(shù)
Base* p = new Derived();
- 指針指向的對象必須支持向上轉(zhuǎn)型(up_casting)棘捣,即指針是基類的類型辜腺,指向派生類的對象
- 所調(diào)用的函數(shù)是虛函數(shù)
p->vfprint()
此時(shí),編譯后的匯編代碼不再指定具體的函數(shù)地址,而是先指向派生類的vptr哪自,然后根據(jù)函數(shù)名來讀取vtbl中對應(yīng)的函數(shù)地址
可以將虛函數(shù)調(diào)用過程用代碼簡化表示為
(*p->vptr[n])(p);
(*(p->vptr)[n])(p);
即p指向了派生類對象的虛函數(shù)表丰包,然后通過定位找到對應(yīng)的虛函數(shù)。注意括號(hào)內(nèi)的p相當(dāng)于傳入函數(shù)的this壤巷。
根據(jù)測試邑彪,虛函數(shù)的地址是有序的,可以從vfprint1()和vfprint2()的匯編代碼看出胧华,調(diào)用vfprint2()時(shí)地址add了8個(gè)字節(jié)(這里用的64位)
應(yīng)用場景
假設(shè)我們要畫不同的形狀寄症,可以將基類的draw設(shè)置為純虛函數(shù)矩动,然后派生類去各自實(shí)現(xiàn)。
class Shape
{
public:
Shape() = default;
virtual ~Shape() = default;
public:
virtual void draw() = 0;
};
class Rectangle : public Shape
{
public:
Rectangle() = default;
virtual ~Rectangle() = default;
public:
virtual void draw() override { std::cout << "draw a rectangle" << std::endl; };
};
class Circle : public Shape
{
public:
Circle() = default;
virtual ~Circle() = default;
public:
virtual void draw() override { std::cout << "draw a circle" << std::endl; };
};
除了矩形和圓形之外篮迎,我們可能會(huì)在后續(xù)增加各種各樣的形狀,同時(shí)我們絕對不想改變畫出所有圖形的代碼示姿。那么在調(diào)用時(shí)可以寫成下面甜橱,這樣for循環(huán)中的代碼是不會(huì)變的,只要將不同的圖形加入到容器中就行岂傲。
int main()
{
std::vector<Shape *> shapes;
shapes.push_back(new Rectangle);
shapes.push_back(new Circle);
for (const auto &s : shapes)
{
s->draw();
delete s;
}
}