文章目錄
簡單一句話就是,操作接口表現出多種形態。嚴格的多態分爲編譯時多態(靜態多態)和運行時多態(多態多態)。我們一般講的多態都是運行時多態。本文講的多態就是運行時多態。
言歸正傳,講我們的多態。
多態
多態通過繼承和虛函數實現。運行時才綁定函數地址(晚綁定)。最常見的用法就是什麼一個父類指針,讓父類型的指針指向任意一個子類對象,然後通過父類的指針動態地調用實際子類的成員函數。根據指向的子類的不同而實現不同的功能。(或者讓父類型的引用綁定子類對象(不用指針用引用的話))
怎麼寫多態
通過虛函數。
目的:**接口重用。**一個接口,多種形態。
比方,我們有個動物類,吃飯函數。一個接口:吃飯。繼承的類有狗、羊、貓,它們也要吃飯,但是狗要吃骨頭、羊要吃草、貓要吃魚。我們就可以把父類動物類的吃飯函數加個virtual定義爲虛函數。然後子類去重寫(即重新定義)吃飯函數。用的時候,用父類的指針,指向任意一個子類對象,調用的時候去調用實際子類的成員函數,動物類指向貓則調用吃魚,指向羊則調用吃草,指向狗則調用吃骨頭。一個吃飯的接口,表現出了多種形態,吃魚吃骨頭吃草。
Base 動物{virtual 吃函數 };
Derived 貓{重寫吃飯爲吃魚};
Derived 狗{重寫吃飯爲吃骨頭};
Derived 羊{重寫吃飯爲吃草};
貓類 cat;
狗類 dog;
羊類 hseep;
動物類 *a= &dog;
a.吃();//調用的是吃骨頭
動物類 *a1= &cat;
a1.吃();//調用的是吃魚
注意:所有類的吃飯函數都是虛函數了,不僅僅是父類。
多態的實現:by虛函數
c++成員函數有兩種:一種是子類直接繼承而不需要改變的函數,一種是允許子類重寫/覆蓋(override)的函數。把後者定義爲了虛函數。加virtual關鍵字。
什麼時候要聲明爲虛函數?
即,如果子類需要修改父類的行爲,即要重寫,就應該在父類中將相應的函數聲明爲虛函數。
虛函數的目的?
加關鍵字後,virtual告訴編譯器不應當完成早綁定(綁定現在這個函數(虛函數)的地址),應當晚綁定,運行時動態綁定。
虛函數virtual function的實現機制——虛函數表Virtual table
V-table好比一張地圖,指明瞭實際所應該調用的函數。
V-table也是虛函數的一個代價:產生系統開銷。(弊)
編譯器對每一個包含虛函數的類創建一個表。
當一個包含虛函數的類的對象被創建,會有一個虛表指針vptr(vpointer),指向該類的虛表,虛表裏放的是一個類的虛函數的地址。是順序存放虛函數地址的。
eg.
- class Base有: virtual void f(), virtual void g(), virtual void h()
可以通過Base的實例b來得到虛函數表:
地址:虛函數表地址、虛函數表中第一個虛函數地址、表中第二個虛函數地址… - 沒有虛函數重寫/覆蓋(實際上毫無意義,爲了便於理解)
class Derive:public Base{
vir void f1(), vir void g1(), vir void h1()}
對於實例Derive1 d虛函數表如下:
看到:虛函數按照其聲明順序放於表中。父類的虛函數在子類的虛函數前面。 - 子類覆蓋/重寫了父類的虛函數f
class Derive:public Base{
vir void f(), vir void g1(), vir void h1()}
對於子類的實例,其虛函數表如下:
看到:覆蓋的f()函數被放到了虛表中原來父類虛函數f()的位置。沒有被覆蓋的虛函數依舊。
於是,就有了,我們一般寫的程序父類指針指向子類對象Base *b= new Derive(); b->f(); delete b;,實際調用的時候調用的是子類重寫的函數Derive::f(),實現了多態。因爲b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代。
Derive d;(棧上面開闢空間) *Base b= &d;
new是在堆上面。
爲什麼虛函數效率低?
因爲虛函數需要一次間接尋址,而一般的函數可以在編譯時定爲到函數的地址,虛函數(動態類型調用)要根據某個指針定位到函數的地址。多了一個過程,效率低一些,但帶來了運行時的多態。
虛函數的入口地址和普通函數有什麼區別?
虛函數表:
當一個包含虛函數的對象被創建的時候,
它在頭部附加一個指針vptr,執行vtable
中相應位置。調用虛函數的時候,不管
你是用什麼指針調用的,它先要去vtable
裏找到入口地址在再執行,從而實現了
“動態聯編”。不像普通函數那樣簡單地
跳轉到一個固定地址。