漫談 C++ 虛函數 的 實現原理

文中講述的原理是推理和探討 , 和現實中的實現不必定徹底相同 。數組

 

C++ 的 虛函數 , 編譯器 會 生成一個 虛函數表 。 ide

虛函數表, 其實是 編譯器 在 內存 中 劃定 的 一塊 區域, 用於存放 類 的 虛函數 的 override 實現 的 函數指針 。函數

就是說, 若是 類 對於 基類 的 虛函數 override 了, 那麼 override 的 函數 的 函數指針 就會 被記錄到 虛函數表 裏 。性能

程序在運行時會 查找 虛函數表, 找到 override 的 函數, 而後 調用 override 的 函數, 這樣就實現了 調用 子類 實現的函數, 實現了 「多態」 。優化

虛函數表 是一個 線性表, 這樣能夠快速的訪問 。spa

訪問線性表 的 時間複雜度 是 O(1)  。指針

相對於 普通的函數 調用, 虛函數 的 調用 多了一次 查找 虛函數表 的 工做, 至關於 多了 一次 尋址, 因此 效率會更低一點 。對象

 

C++ 編譯器 會爲 每一個成員(類 函數 字段) 指定一個 連續的 數字編號 做爲 ID 。繼承

用 連續的 數字編號 做爲 ID 是爲了能夠以 線性表 的 方式 快速檢索 。ip

 

虛函數表 由  2 個 線性表 組成 。

等, 我下面 把 線性表 稱爲 數組 好了 。

 

虛函數表 由 2 個 數組 組成 。

數組 1 是 類表, 保存 類 的 實現函數表 的 地址 。 實現函數 就是 override 了 虛函數 的 函數 。

數組 2 是 實現函數表, 保存 類 的 實現函數 的 函數指針 。

 

假設有 3 個類, A 、 B 、 C , 那麼 編譯器 會 給 這 3 個類 分別指定 ID 爲   0 、 1 、 2 。

在 數組 1 (類表) 裏, 就會 3 個元素,  咱們用 僞碼 來表示好了 :

 

類表 [ 0 ] = A 類 的 實現函數表 的 地址

類表 [ 1 ] = B 類 的 實現函數表 的 地址

類表 [ 2 ] = C 類 的 實現函數表 的 地址

 

這樣, 用 類 的 ID 做爲 下標(index) 來 訪問 類表, 就能夠取得 該類 的 實現函數表 的 地址 。

 

實現函數表 咱們也能夠用 僞碼 來表示, 假設 A 類裏有 Hello() 、 Thank() 、 Goodbye()     3 個 override 了 基類 虛函數 的 實現函數, 那麼, 編譯器 會給 這 3 個 實現函數 分別 指定 ID 爲    0 、 1 、 2 。 這裏只會給 實現函數 指定 ID , 不會把 其它 普通函數 包括進來 。

實現函數表 會是這樣 :

 

實現函數表 [ 0 ] = Hello() 的 函數地址

實現函數表 [ 1 ] = Thank() 的 函數地址

實現函數表 [ 2 ] = Goodbye() 的 函數地址

 

這樣, 用 實現函數 的 ID 做爲 下標(index) 來 訪問 實現函數表, 就能夠取得 這個 實現函數 的 地址 。

 

編譯的時候, 對於 普通函數 的調用, 會直接編譯成       「函數地址 -> 調用」       這樣的 目標代碼, 

對於 虛函數, 則會編譯成          「根據 當前對象 的 類 ID 和 函數 ID -> 查找 虛函數表 -> 找到 實現函數 地址 -> 調用」           這樣的 目標代碼 。

 

從上面的 原理 看到, 查找 虛函數表 自己 就須要 2 次尋址, 查找 2 個 線性表(數組) 。

同時, 也能夠看到, 對於 不須要 override 的 函數, 不要 聲明 爲 虛函數, 由於 虛函數 會 增長 查找 虛函數表 的 時間花費, 性能 比 普通函數 調用 更低一點 。

固然, 編譯器 可能會做一些 優化, 好比 對於 能在 代碼中 明確判斷出 對象類型 的 狀況, 即便是 虛函數 調用, 也會編譯成 和 普通函數 同樣的 處理方式    「函數地址 -> 調用」 , 即 要調用的 函數地址 在 編譯時就肯定了 。

那什麼是 編譯時不能肯定 對象類型 的 狀況 ?   好比 工廠方法 。

 

編譯時, 對於 虛函數, 編譯器 會 檢查 類 是否 進行了 override, 若是 override 了, 則 將 實現函數 列入 虛函數表, 若是沒有 override, 就 查找 上一層 父類 是否 override 了, 若是 override 了, 則 將 實現函數 列入 虛函數表, 若是沒有 override, 就 繼續 查找 上一層 父類, 以此遞推, 直到 聲明 這個 虛函數 的 父類 。 若是在整個繼承層級中都沒有 override 這個 虛函數, 則 不會將這個 虛函數 列入 虛函數表, 固然 也不會給 這個 虛函數 指定 虛函數 ID 。全部 子類 對象 對 這個 虛函數 的 調用 會被編譯成     「聲明這個虛函數的 父類 裏 這個虛函數 的 函數地址 -> 調用」     方式, 這種狀況 和 普通函數 是同樣的了 。

 

咱們再來談談 「後期綁定」 。

咱們先說說 「動態綁定」 。 在 Javascript 裏, 對象 和 函數 能夠 任意 的 綁定, 因此叫 「動態綁定」 。

對於 查找 虛函數表 的 作法, 是在 運行時 才決定具體要調用的 函數, 至關於 運行時 才 給 對象 綁定 函數, 因此叫 「後期綁定」   (我印象中好像是這麼叫的)  。

相關文章
相關標籤/搜索