作者 | 碼農翻身劉欣
鏈接 | https://dwz.cn/BEjV9FdX
面向對象可以說是各大語言一個重要的特性了,不過如果我們換個角度,在內存中看看對象的佈局,就會發現根本沒有什麼面向對象,只有面向過程。
讓我們從一個簡單的Shape類開始,這個類有兩個字段int x, int y, 它們在內存中是這麼存放的:
非常容易理解,對吧?
再來看一下繼承, class Circle繼承了Shape,增加了一個字段radius, Circle對象在內存中是這樣的:
這也沒什麼大不了的,但是這裡只是字段(x,y,radius), 如果Shape類有個方法:draw(),在內存中該怎麼放?
首先,不能把draw()方法都放在每個對象上,那樣就需要複製很多份,太浪費了。
我們可以把這個draw()方法在內存中生成一份, 然後在每個對象上增加一個指針,指向這個draw()方法就行了。
(三個Shape對象,都指向了同一個代碼)
但是這麼做也有問題, 如果Shape類又增加了一個方法 move() ,那每個對象都需要記錄move方法的指針:
如果方法很多,對象也很多,還是浪費!
很明顯,我們需要一箇中間層, 用這個中間層把所有函數指針都記下來。這個中間層就是所謂的虛函數表:
每個類,只要維持一個虛函數表就可以了。
每個對象,只要記錄一個虛函數表的地址就可以了。
當然,也可以在虛函數表中記錄一些關於這個類的相關信息,不是本文的重點,就不展開了。
為什麼叫做虛函數表呢?這個概念可能是從C++中來的,在C++中有個關鍵字virtual ,修飾一個函數的時候,這個函數就會變為虛函數,在調用時就具備了多態的行為。(注:在Java中,一個類的函數默認都是虛函數)
那多態到底是怎麼實現的呢?
非常簡單,只要把虛函數表給設置好就行了。假設子類Circle 也定義了一個move 函數,把父類Shape的move函數覆蓋了,在內存將會是這個樣子:
當你調用circle.draw()的時候,在虛函數表中找到的還是Shape類的draw()方法。
但是當調用circle.move()的時候,就會從Circle類的虛函數表中找到Circle.move(),而不是Shape.move(),
多態發生了!仔細看看上面這張圖,在內存中,三個方法和兩個對象是分開的,這裡沒有Class的概念,多態是通過虛函數表實現的。如果我們寫程序的時候,寫下這樣的函數Shape_draw(), Shape_move(), Circle_move(),再寫下Shape和Circle這樣的數據結構,然後把他們用虛函數表連接到一起。也就實現了面向對象了。
在內存中,“面向對象”已經褪去漂亮的包裝,退化成“面向過程”, 退化成那個最基本的公式:程序 = 數據結構 + 算法。
當然,在絕大部分情況下,程序員不需要手工地去實現這個虛函數表,這件事情應該交給機器去做。
對於C++,編譯器可以在編譯期間生成虛函數表。對於Java,編譯出的字節碼中是沒有的,只有invokevirtual這樣的指令,虛函數表是在類裝入虛擬機的時候創建的。
(完)
閱讀更多 程序猿168 的文章