1、靜態類型和動態類型
如果大家希望學好C++的話那請務必多學習一下c++內部的對象模型,前面有很多博客都是關於這方面的不過都不是自己寫的(因爲自己很懶)。
舉個例子:類A,類B,如果B沒有繼承A那這就沒什麼關係了,不過我這裏主要說的就是說它們有關係,所以我這裏就假如B繼承了A。那B將繼承A的某些特性(比如成員函數,成員變量等等)。那假如沒有什麼虛函數什麼的,那這種關係如下:
B b;
而如果有虛函數的話(A有,B無)(A無,B有)(A有,B有並重寫)模型如下。
B b
這裏我就給出最簡單的圖,詳細的可找相關資料看下,該圖說明一下,只要A或者B類有虛函數那編譯器就會爲他生成vtable和type_info其中都是爲了動態檢查用的。vtable是虛函數用的,而type_info爲了支持RTTI用的。
那我們看重點靜態類型和動態類型
靜態多態性:函數多態性——函數重載
模板多態性——C++模板(類模板、函數模板)
動態多態性:虛函數(只有用地址才能實現動態多態性)
只有採用“指針->函數()”或“引用變量.函數()”的方式調用C++類中的虛函數纔會執行動態綁定。對於C++中的非虛函數,因爲其不具備動態綁定的特徵,所以不管採用什麼樣的方式調用,都不會執行動態綁定。
代碼形式 |
對於虛函數 |
對於非虛函數 |
||
作用 |
綁定方式 |
作用 |
綁定方式 |
|
類名::函數() |
調用指定類的指定函數 |
靜態綁定 |
調用指定類的指定函數 |
靜態綁定 |
對象名.函數() |
調用指定對象的指定函數 |
靜態綁定 |
調用指定對象的指定函數 |
靜態綁定 |
引用變量.函數() |
調用被引用對象所屬類的指定函數 |
動態綁定 |
調用引用變量所屬類的指定函數 |
靜態綁定 |
指針->函數() |
調用被引用對象所屬類的指定函數 |
動態綁定 |
調用指針變量所屬類的指定函數 |
靜態綁定 |
注:被引用對象所屬類 是 指針 或 引用 指向的對象的實際類型;
引用變量所屬類、指針變量所屬類 是 定義 引用變量、指針變量的類型;
以上兩種類型可能相同,也可能不同。
下面看一個例子:
- <span style="font-size:18px">#include <iostream>
- using namespace std;
- class A
- {
- public:
- void fun()
- {
- cout<<"A::fun"<<endl;
- }
- void fun2()
- {
- cout<<"A::fun2"<<endl;
- }
- };
- class B:public A
- {
- public:
- void fun()
- {
- cout<<"B::fun"<<endl;
- }
- void fun3()
- {
- cout<<"B::fun3()"<<endl;
- }
- };
- void main()
- {
- A a;
- a.fun();//靜態檢查
- B b;
- b.fun();//靜態檢查
- b.fun2();//繼承會將函數也繼承
- A *x = &b;
- x->fun();
- x->fun2();
- x->fun3();
- }
- </span>
上面解釋一下,第一句和第三句以爲都是對象所以進行靜態檢查所以調用對應的自己函數,而第五句因爲B繼承了A的成員函數所以也可以調用(但此時要注意調用權限這裏爲public),到第六句出現了指針所以後三句都將進行動態檢查但由於該程序沒有虛函數所以不會進行動態綁定。首先編譯器知道x靜態爲A類所以x->fun();即爲A::fun,同理fun2(),但最後一句就會出現錯誤了,因爲fun3()不是父類的就相當於父類無法繼承子類。
那如果程序改一下,將A類中fun()聲明爲虛函數。B類fun3()聲明爲虛函數,那執行過程將是x靜態爲A*,不過編譯器有type_info所以知道指向的是B對象,因此執行虛函數fun的時候將通過vptr綁定到B的fun,而fun2不是虛函數也就不會去用vptr到vtable裏找執行函數所以不會動態綁定。
所以我們也就知道動態綁定需要額外的時間開銷的。
另外如果B中fun3爲虛函數那x->fun3()是否成功呢?答案是不可以的,因爲首先x是A*此時A中並沒有fun3的類型說明所以就報錯說A中沒有該成員函數。
那我們就可以總結下:c++實現並沒有將對虛函數的調用採用運行時進行而是派生類定義中的名字(對象或函數名)將義無反顧地遮蔽(即隱藏)掉基類中任何同名的對象或函數(跟普通函數一樣的),只是說虛函數的調用會進行轉換(通過vptr到vtable裏面查找),而普通函數只進行簡單的調用。
上面簡單來說就是說比如父類指針指向子類對象,那首先指針進行靜態檢查時A*的,然後看看調用的函數是虛函數還是普通成員函數,如果是虛函數那該指針通過vptr到vtable裏面找改函數對應得函數指針如果該函數被子類修改過了那該槽被改寫了那將調用子類的該函數,而如果沒有被修改也就是說該槽沒有被重寫那就直接調用父類自己的該虛函數。第二,如果該函數是父類的普通成員函數的話那直接進行調用不管子類有沒重寫過,因爲進行靜態檢查該指針只是父類型的。
所以最後說一句:虛函數調用語句的參數類型檢查並沒有採用推遲到運行時進行的。其實一開始就已經靜態檢查好了,只是調用的時候進行動態尋址而已。
再說一句,呵呵:爲什麼父類要是虛析構函數。我想應該清楚了吧!
因爲首先怕出現這樣的情況:A* a = new B();
那麼當delete a;的時候因爲a靜態檢查時A*那如果不是虛函數的直接調用~a了。因爲vtable的槽沒有被子類改寫所以會出現無法調用~B()。而假如寫了呢那將出現就相當於~B()改寫了~A()對應的槽變爲了~B(),所有delete a先調用~B()然後調用父類~A()(這個調用子類析構函數然後調用父類析構函數是編譯器規定的)。怎麼樣大家清楚了吧~