动态联编和静态联编
一、動態聯編和靜態聯編的基本概念
將源代碼中的函數調用解釋為執行特定的函數代碼塊被稱為函數名聯編。在C語言中這個步驟更簡單,因為C語言的函數名不允許重復,即沒有函數重載這一說,每個函數名都對應著一個函數代碼塊。但是在C++中要復雜,因為C++中允許函數重載,必須得根據函數名和形參列表來對應一個函數代碼塊。C/C++編譯器在編譯過程就完成了這種聯編。在編譯過程中進行聯編被稱為靜態聯編 (早期聯編)。
但是虛函數的產生使得靜態聯編變的困難,因為父類的虛函數允許被子類重寫。當我們用一個父類指針指向一個子類對象的時候,編譯器編譯階段可以知道父類指針的類型,但是它不能夠直接用父類指針的類型中的虛函數作為本次調用的函數代碼塊。因為可能子類對虛函數進行重寫了,這種情況下用戶明顯是想要調用重寫后的函數。那么可能要說了,那編譯器通過對象類型調用該類型中的重寫后的函數不就可以實現編譯階段早期綁定了?然而,通常情況下,只有在運行時才能確定對象的模型。對于虛函數,編譯器要通過動態聯編(晚期聯編)的方式確定對應的函數代碼塊。即在運行時,通過對象類型確定調用的虛函數的函數代碼塊。重寫了,就調用在對象類型中重寫的函數對應的函數代碼塊,沒有重寫,那么就調用父類虛函數對應的函數代碼塊。
總結:
對于普通成員函數,如果通過對象調用普通成員函數,編譯器直接調用該對象類型中的該函數對應的函數代碼塊;如果通過指針調用普通成員函數,那么編譯器會直接根據指針類型調用該類型中的該函數對應的函數代碼塊。這些都是在編譯階段確定的,都是靜態聯編。
對于虛函數,如果通過對象調用虛函數,編譯器會直接通過對象的類型如果是通過指針或者引用的方式調用虛函數,那么編譯器將無法確定該指針類型中的虛函數對應的代碼塊是否是用戶想要調用的。因為如果是父類指針指向子類對象的話,當子類沒有對父類虛函數重寫,我們意思肯定是調用父類的虛函數,如果重寫了,我們意思肯定是調用子類重寫后的函數,這個時候編譯器不能直接說因為是父類指針,就直接去調用父類中的虛函數對應的代碼塊。所以有了虛函數指針和虛函數表的概念,通過運行時查虛函數表的方式,確定要調用的函數的代碼塊的地址,因為如果子類沒有重寫這個虛函數的話,虛函數表中放的是父類的虛函數,如果子類對父類的虛函數重寫了,那么重寫后的函數的地址會覆蓋掉父類虛函數的地址,調用的就是重寫后的函數了。(對于虛函數指針和虛函數表,請點擊此處)
有一種說法,說對于調用普通成員函數來說,都是靜態聯編,這個沒問題。但是說對于虛函數來說,如果是通過對象調用虛函數,不會經過查虛函數表,是靜態聯編;如果是通過指針調用虛函數,就會經過查虛函數表,是動態聯編。
二、指針和引用類型的兼容性
父類指針可以指向子類對象。指向子類對象的父類指針的使用請點擊此處。將子類對象的類型轉化為父類對象,為向上強制轉換,編譯器可以直接隱式轉換,而父類對象的類型轉換為子類對象的類型,為向下強制轉換,必須顯示轉換。
隱式向上轉換是基類指針或者基類引用可以指向基類對象或派生類對象,因此需要動態聯編。
運行結果:
對于test1和test2很好理解。參考指向子類對象的父類指針的使用。
對于test3而言,形參是值拷貝的臨時對象,這個值拷貝不針對虛函數指針,也就是說用哪個類創建的對象,這個對象的虛函數指針就指向哪個類的虛函數表,所以對于test3不論是傳A的對象還是B的對象,利用拷貝構造函數創建出來的形參是臨時對象,且對象隱藏的虛函數指針指向的都是A類的虛函數表。所以調用的都是A類的虛函數中的fun函數,而不是B類的虛函數表中被重寫的fun函數。
三、靜態聯編和動態聯編的效率問題
既然動態聯編這么好用,為什么還要存在靜態聯編呢?
(1)效率方面
靜態聯編是在編譯期間就執行好的,而動態聯編是運行期間才開始。不僅如此,動態聯編還需要通過查虛函數表,找到虛函數地址,再去這個地址里找虛函數。而且動態聯編需要生成虛函數指針(存在對象中),還需要生成虛函數表。所以說動態聯編的步驟比靜態聯編的步驟復雜,而且還需要生成虛函數指針虛函數表。時間和空間都比靜態聯編消耗的多。
(2)概念模型方面
虛函數一般是為了子類涉設計的,預期子類需要重寫這個函數,所以將這個函數寫成虛函數。然而,有些函數并不需要被子類重新定義,那么父類就沒必要將這個函數寫成虛函數。效率提高了,還告訴子類我沒有把這個設置成虛函數,就是為了告訴你不要重新定義這個函數。
四、有關虛函數的注意事項
1.編譯器不允許將構造函數設置成虛函數
構造函數不能是虛函數。因為如果構造函數放到虛函數表中,那么子類創建對象會調用父類的構造函數,然而事實上是子類先調用父類的構造函數,再用自己的構造函數。所以這個不符合繼承構造函數調用的邏輯。
2.有繼承關系時,析構函數盡量設置成虛函數
有繼承關系時,特別是如果子類有指針成員變量,我們需要在子類的析構函數中判斷指針是否指向堆空間,是的話,我們需要在子類的析構函數中對堆空間進行釋放。那么如果不將父類析構函數設置成虛析構函數的話,那么如果用父類指針指向子類對象,父類指針將無法調用子類的析構函數,無法將申請的堆空間釋放。(對于為什么父類析構函數寫成虛函數,指向子類對象的父類指針就可以調用子類的析構函數,而不把父類析構函數寫成虛函數,指向子類對象的父類指針不可以調用子類的析構函數,請點擊此處查看指向父類對象的父類指針的使用)
所以有繼承關系時,盡量將析構函數寫成虛函數,哪怕這個類不需要用析構函數做什么。
3.友元不能是虛函數
虛函數必須是對類的成員函數而言的,不可以將友元函數設置成虛函數。
4.重新定義問題
如果派生類重新定義父類的成員函數,那么父類的所用同名的成員函數都被隱藏。包括虛函數。如下代碼:
class A { public:virtual void fun(int a) {}void fun() {} };class B :public A { public:void fun() {} };int main() {B b;//b.fun(10);//errorb.fun();//調用的是子類的成員函數return 0; }子類寫了一個fun函數,父類的兩個fun函數讀背隱藏了(注意只要是函數同名,就被隱藏,和函數重載無關)。
如果再調用父類的fun函數,需要顯示調用。
如下:
b.A::fun();
b.A::fun(10);
如果重新定義繼承的方法,應該確保與原來的原型完全相同(函數名,形參列表)。當然,可以將函數返回類型修改。
(1)只要子類的函數名和形參列表與父類的虛函數相同,編譯器就會認為這個是對父函數的虛函數的重寫,這個時候如果返回類型和父類不一樣,編譯器會報錯。(除去唯一一個例外:返回類型可以協變,比如父類虛函數返回類型是父類指針(引用)形式,子類重寫父類的虛函數,允許將函數的返回類型改為子類指針(引用)形式)。
(2)如果子類重寫父類的虛函數,那么子類的其他重名函數(不論是不是虛函數)也會被隱藏。
總的來說,只要父類的函數名和子類的函數名相同,都會被隱藏。
如下示例:
總結
- 上一篇: 数组——找重复元素
- 下一篇: 单链表——判断两个单链表(无头节点)是否