系列文章目錄
????????歡迎讀者訂閱《計算機底層原理》、《從JVM看Java》系列文章、能夠幫助到大家就是對我最大的鼓勵!
文章目錄
-
目錄
系列文章目錄
文章目錄
前言
一、編譯器做了什么?
1.詞法分析
2.語法分析
3.語義分析
4.中間代碼生成
5.優化
6.目標代碼生成
拓展:匯編
7.符號表生成
8.錯誤處理
9.生成可執行文件
二、基于編譯理解繼承和多態
1.再談動靜態綁定
?2.虛方法表與運行時類型標識
總結
前言
? ? ? ?這篇文章我重點要講解的是有關動靜態綁定的具體概念以及多態實現的底層原理、因為這其中涉及到了編譯器的工作原理、所以我在這篇文章當中花費了較長的篇幅、來為大家講解編譯器的具體工作內容,我相信一個優秀的程序員對自己程序當中的編譯的過程應該是很熟悉的。
一、編譯器做了什么?
1.詞法分析
????????詞法分析是編譯過程當中的第一個階段,也被稱為是掃描或者詞法掃描,它的主要目的是將源代碼進行分割變成一系列的詞法單元,每一個詞法單元都代表源代碼當中的一個基本元素,這些基本的元素包括關鍵字、標識符、運算符、常量等,而我們的詞法分析這一過程是由我們的詞法分析器來完成的。
識別詞法單元????????
? ? ? ?詞法分析器會識別源代碼當中的詞法單元,每個詞法單元都具有特定的語法和含義、例如我們的關鍵字int、double、或者循環語句、分支語句、if、else,包括運算符加減乘除、或者常量,都會被詞法分析器識別成為一個詞法單元。去除空白和注釋
? ? ? ?這個其實很好理解、詞法分析器通常會忽略掉空白字符像空格、制表符、注釋等無用字符,因為他們對程序的語法結構沒有貢獻、這有助于減小后續階段的處理復雜性。生成詞法單元流
? ? ? ?在經過了識別詞法單元和去除空白字符和注釋以后,詞法分析器就會將經過以上這些操作的源代碼生成詞法單元流,詞法單元流當中當中包含了他的類型和相應的屬性信息。為了方便大家理解我為大家寫了一個詞法單元流,注意這只是抽象的表示,方便大家都理解。
?
詞法單元流 詞法單元類型 例子 關鍵字 if、else、while 標識符 variable_name 運算符 +、-、*、/ 數字 123、756 字符串 "Hello World" 分號 ; 括號 ( ,),{ } 等號 = 逗號 , 冒號 : 注釋 //This is a comment 這些只是示例、大家理解即可
錯誤檢測? ? ? ??
? ? ? ?詞法分析也負責將源代碼當中的一些簡單的語法錯誤進行檢測、例如非法字符、或者字符串未閉合、這有助于提前發現一些常見的編碼錯誤。詞法分析通常使用有限狀態自動機或者正則表達式來描述詞法單元的模式、并且使用這些模式來識別和提取詞法單元、整個過程為后續的語法分析和語義分析提供了一個清晰的輸入、幫助編譯器理解程序的基本結構、這里要注意、編譯器所做的每一個操作都是為了方便后續的操作進展順利。
這里為大家展示一下詞法單元流是什么樣子的
?源代碼: int main() {if (x > 0) {return 1;} else {return 0;} }詞法單元流: [關鍵字:int] [標識符:main] [(] [)] [{][關鍵字:if] [(] [標識符:x] [運算符:>] [數字:0] [)] [{][關鍵字:return] [數字:1] [;][}] [關鍵字:else] [{][關鍵字:return] [數字:0] [;][}] [}]
2.語法分析
語法規則檢查? ? ? ??
? ? 首先在這里我要先為大家講解一個概念叫做文法和產生式,首先什么是文法呢?文法是一種廣泛的概念、用于描述語言之間的結構規則、它包括了一組規則、這些規則定義了語言的合法結構和形式、文法可以包括多種類型的規則例如產生式、終結符、非終結符、語法規則等等,這其中就涉及到了編譯原理的概念、在這里就不為大家詳細地敘述了。
? ? ?那么產生式呢只是文法當中的一種語言規則、它是文法的組成部分、產生式描述了如何通過替換一些符號來產生另一些符號串。- 構建語法樹
? ? ?語法分析器會根據產生式(語法規則)遍歷詞法單元流來形成語法樹,如下圖:程序 -> 類聲明 類聲明 -> class 標識符 { 成員聲明列表 } 成員聲明列表 -> 成員聲明 成員聲明列表 | ε 成員聲明 -> 變量聲明 | 方法聲明 變量聲明 -> 類型 標識符 ; 方法聲明 -> 返回類型 標識符 ( 參數列表 ) { 方法體 } 參數列表 -> 參數 參數列表 | ε 參數 -> 類型 標識符 類型 -> int | float | boolean
上面就是我為大家展示的一個產生式,他是用來規范代碼的一種規則。
?[class, Main, {, public, static, void, main, (, String, [, ], args, ), {, int, a, =, 5, ;, }, }]
這個是詞法單元流,語法分析器遍歷詞法單元流,并且一句產生式形成語法樹。
?Program└── ClassDeclaration├── Identifier: Main├── MethodDeclaration: public static void main(String[] args)├── VariableDeclaration: int a = 5
語法分析樹的格式大致如上圖。
- 錯誤檢測
? ? ?當語法分析器在對詞法單元流進行遍歷的時候就會進行錯誤檢測,如果在這個過程當中遇到了無法匹配的單元序列或者違反了語法規則的部分的時候,語法分析器就會做出如下操作。
? ? ?1. 錯誤報告:一旦發現語法的錯誤,語法分析器就會生成錯誤報告、其中會包含錯誤的位置、類型、以及可能的修復建議、例如我們在使用各種集成開發環境編寫代碼的時候,我們寫了一些違反語法規則的代碼的時候,編譯器就會在這些代碼的下面顯示紅線、來提示我們編譯不通過、并且會提供可能的修復意見、這有助于開發者快速定位并且修改問題。
? ? ?2. 錯誤恢復:有些語法分析器在發現錯誤以后會嘗試進行錯誤恢復、以便繼續分析源代碼并且檢測可能的后續錯誤、錯誤恢復的方法可以包括插入或者刪除一些詞法單元,是的語法分析可以繼續進行。
? ? ?3. 提供多個錯誤:語法分析器在遇到問題的時候不會在發現一個錯誤之后立即停止檢測、而是會繼續檢測以發現更多的錯誤、這樣可以提高開發者修復代碼的效率、因為他們可以一次性地解決多個問題。
- 優化準備
? ? ?注意這里的優化,只是語法分析器在工作時的副產品,并不是他的主要職責、這些優化主要是針對詞法單元流當中一些冗余的括號或者一些無用的語句進行刪除。代碼在運算 2 * (3 + 4)的時候語法樹的邏輯表達 乘法表達式 (*)/ \數字(2) 加法表達式 (+)/ \數字(3) 數字(4)
? ? ?我們進行語法樹的構建之后在邏輯上我們就可以這樣表示語法樹,這只是簡單的一個操作,實際語法樹的構成及其復雜,這里展示只是為了方便大家理解。
3.語義分析
類型檢查:這個不僅是語義分析當中的重點更是一個我們在進行變成的時候的一個重要知識點、希望大家能夠徹底掌握這些知識(提醒:這些知識的掌握一定要依靠我們對于計算機底層的理解、千萬不要死記硬背)!
? ? ??1)類型定義:首先編程語言通常由一組基本類型(整數、浮點數、字符等)以及用戶定義的符合類型(結構體、類等)編譯器需要了解這些類型及其規定。
? ? ? 2)類型推斷:在某些情況下,編譯器可以通過上下文推斷變量或者表達式的類型、例如對于賦值語句 ‘x = 5’,編譯器可以推斷變量x的類型為整數。
? ? ? 3)類型匹配:編譯器檢查表達式當中的操作符和操作數的類型是否匹配,例如加法操作通常要求兩側的操作數是相同的類型。? ? ? ?
? ? ? 4)強制類型轉換:在某些情況下、編譯器可能允許或者要求進行顯示的類型轉換以確保表達式的類型匹配。
? ? ? 5)函數調用檢查:對于函數的調用,編譯器會檢查函數的參數類型和返回類型是否與函數的聲明相匹配。
? ? ? 6)數組和指針檢查:對于數組和指針、編譯器會檢查索引操作的合法性、以及指針的指向是否正確。例如我們在使用數組的時候經常會有這樣的操作array[5]? = 5;如果這個數組一共只有5個元素,下表只到4,那么這樣的操作無疑是違法的,再例如ptr ->或者ptr. 操作的時候就需要進行這樣的指針檢查。
? ? ? 7)用戶定義類型檢查:如果編程語言支持用戶定義的類型(如類或者結構體)編譯器會檢查對這些類型的使用是否規范。
? ? ? 8)生成錯誤報告:如果在這個時候編譯器發現了類型的錯誤、就會生成相關的錯誤信息、知識具體的錯誤位置和類型不匹配的原因。符號解析
? ? ?符號解析主要負責對程序當中的符號(變量、函數、類型)進行解析和處理、確保這些處于不同位置的符號能夠得到正確的處理和使用、同時還會處理作用域的問題、例如這個變量的使用是否超過了他的作用域、方法內的局部變量放到方法外去使用等等操作。接下來我為大家講解符號解析的詳細內容。
? ? ?1)符號表的使用:這里的符號表為大家買一個伏筆、各位讀者先不要糾結這個符號表是什么東西,我們只需要記住這個符號表是從詞法分析階段就開始填充,在整個過程當中不斷進行完善的這個一個表單即可。符號表是一個數據結構、用于存儲程序當中的符號及其相關信息、例如變量的類型、地址、作用域等、在符號解析階段、編譯器會使用符號表來查找、驗證和更新符號的信息。也就是說每一個符號(變量、函數、類型)都會存儲在這個符號表當中、編譯器在進行語義分析的時候就會使用符號表來進行查找和解析。
? ? ?2)變量和常量解析:對于變量和常量,符號解析器會檢查他們是否已經進行了聲明,檢驗變量的使用是否符合其聲明類型、以及是否進行了正確的初始化。
? ? ?3)函數解析:對于函數、符號解析器會檢查函數的聲明和定義、確保函數的參數類型和返回值類型與調用時相匹配、還會處理函數的作用域問題、確保函數內正確引用外部的變量。
? ? ?4)類型解析:符號解析器也負責處理類型信息、他會檢查類型的合法性、比如確保使用的類型是必須是已經定義過的類型、在一些靜態類型語言當中,符號解析也包括進行類型推斷和類型檢查。
? ? ?5)作用域管理:確保變量在聲明的作用域當中是可見的,并且符號解析器還會處理作用域的嵌套的情況,這包括處理作用域、函數作用域等。
? ? ?6)錯誤檢測和報告:如果符號解析器這個時候發現了符號引用的錯誤、例如變量未聲明、類型不匹配等、它會生成錯誤信息并且報告給程序員、幫助調試和修復代碼。語義錯誤檢測和報告
? ? ?語義錯誤檢測和報告是編譯器或解釋器在語義分析階段的一個關鍵任務、語義錯誤指的是源代碼在語法上是合法的但是語義上存在問題,說白了就是編譯不會報錯,但是一運行就崩,或者導致程序在運行時產生的不正確的行為。
? ? ?1)檢測范圍:語義錯誤通常涉及更高層次的語言結構、而不是基本的語法錯誤、這包括變量的使用、表達式的計算、函數調用和復雜的語言特性。
? ? ?2)類型不匹配:一種常見的錯誤時類型不匹配、即在表達式當中使用了不兼容的數據類型、例如將一個字符串賦值給了一個整形變量、或者試圖對不同類型的變量進行算數運算等等都屬于類型不匹配的語義錯誤問題。
? ? ?3)未聲明的變量:使用未聲明的變量或者標識符時另一個常見的語法錯誤、編譯器或者解釋器在語法上可能允許變量的使用、但是如果該變量沒有聲明那么就會報錯誤報告。
? ? ?4)作用域問題:語義錯誤還可能涉及變量的作用域、例如用在某個作用域內重新聲明已經存在的變量、或者在作用域外使用未被聲明的變量等。因為作用域外已經聲明過的變量他的生命周期還沒有結束就再一次聲明就會引發作用域沖突。
? ? ?5)函數調用錯誤:在函數調用的時候、參數的變量或者類型與函數的聲明不匹配會導致語義錯誤、同樣對非函數類型的對象進行函數調用也是一種錯誤。
? ? ?6)數組和指針錯誤:對于數組越界、對空指針進行擦歐總等問題也屬于語義錯誤的范疇。
? ? ?7)不合理的操作:嘗試對不支持的操作進行操作、例如對非數值類型的變量執行算數運算、例如對一個字符串進行加減乘除運算顯然是不合理的、這就可能導致語義錯誤。
? ? ?8)錯誤報告:當編譯器檢測到語義錯誤的時候,它會產生相應的錯誤信息、還包括錯誤的類型、位置和可能的修復建議。這有助于程序員和糾正潛在的問題。
4.中間代碼生成
源代碼:x = 5 y = 10if x > y:z = x + y else:z = x - y生成的語法樹:Program/ \ Assign IfStatement| / \x > =/ \ / \x y z +/ \x y生成的中間表示(簡化的三地址碼):1. x = 5 2. y = 10 3. if x > y goto 6 4. z = x + y 5. goto 8 6. z = x - y
? ? ? ? 中間代碼我已經給大家展示出來了,下面我來解釋一下為什么編譯器要將語法樹轉換成為中間代碼來表示。
? ? ? ? 可移植性:首先中間代碼是獨立于硬件平臺和目標指令集的抽象表示形式,上面的代碼大家也看到了,這也就意味著編譯器可以在不同的平臺上使用相同的中間代碼,從而提高了程序的可移植性。
? ? ? ? 便于分析:中間代碼提供了一個更簡單、更結構化的表示形式、便于編譯器分析程序的控制流、數據流等特性,這種簡化使得編譯器能夠更容易地實施復雜的優化算法。
? ? ? ? 我之前提到過編譯當中的每一步操作都是在為后面的操作打基礎,每一步都是在盡可能地優化代碼為后續的操作和目標代碼的生成提供一個更靈活的基礎。
5.優化
? ? ? ? 編譯器當中的優化通常是指修改程序的中間表示(中間代碼)以提高程序的性能、減少資源消耗或者改進其他方面的目標、優化是編譯過程當中的一個重要步驟、他的目的是為了生成更有效、更緊湊的目標代碼、以便在運行的時候高效地執行。下面我為大家列舉一下編譯器當中的常見的優化操作。
? ? ? ? 1)常量折疊:表達式當中的常量經過編譯之后會全部被折疊得到一個常量結果,例如3 + 5 = 8,這里會把運算過程全部省略直接保留 8 這個結果。
? ? ? ? 2)死代碼消除:死代碼就是程序當中永遠都不會執行的代碼,以減少程序的大小和提高執行效率。public static void main(String[] args){int count = 10;if(count > 5){、、、}else{、、、} }
????????3) 復制傳播:將變量的值替換為其在程序當中的實際值,這有助于減少不必要的變量賦值操作。
?復制傳播之前 x = 5 y = x + 10 z = x * 2復制傳播之后 x = 5 y = 15 z = x * 2
????????這樣子就可以減少一次對x的不必要的賦值操作。
? ? ? ? 4)循環優化:針對循環結構進行優化、例如循環展開、循環合并、循環變量替換等等、以減少循環的開銷。
?循環展開 for (int i = 0; i < N; i += 2) {result[i / 2] = array[i] + array[i + 1]; }for (int i = 0; i < N; i += 4) {result[i / 2] = array[i] + array[i + 1];result[i / 2 + 1] = array[i + 2] + array[i + 3]; }循環合并 for (int i = 0; i < N; ++i) {array1[i] = array1[i] + constant; }for (int i = 0; i < N; ++i) {array2[i] = array2[i] + constant; }for (int i = 0; i < N; ++i) {array1[i] = array1[i] + constant;array2[i] = array2[i] + constant; }
? ? ? ? 5)內聯函數:將函數調用替換為函數的實際代碼以減少函數調用的開銷,因為當我們提到函數的調用時,有一些開銷與之相關、比如保存和恢復現場、跳轉到函數體等等、內聯函數是一種優化技術、它通過將函數調用處用函數體的實際代碼替換來減少這些開銷。我在這里為大家補充一下關于函數調用的相關知識(提示:這里涉及到C語言當中的知識、不感興趣的可以直接劃走。)
? ? ? ? 1.保存和恢復現場:當一個函數被調用的時候、當前函數的執行狀態、包括他的寄存器的值、棧指針等都需要保存起來、以便在函數執行完畢之后能夠正確地恢復到調用點、這涉及到寄存器的值保存到棧上、以及在函數返回的時候從棧上恢復這些值。因為我們在不管是C語言還是Java或者是別的語言,我們在直接調用函數或者使用對象調用函數都是通過函數名也就是函數的引用來調用函數的,而不是直接調用我們定義的函數體,那么這里就涉及到通過函數引用調用函數需要跳轉到函數體、那么既然要涉及到跳轉也就必須提前保存和恢復現場。
? ? ? ? 2.跳轉到函數體:程序執行需要跳轉到被調用函數的代碼段、這通常涉及到一條跳轉指令、將程序的控制流從調用點轉移到函數體的入口點。
? ? ? ? 3.參數傳遞:將參數傳遞給函數也是開銷的一部分,參數通過寄存器或者壓棧出棧等方式傳遞給函數(這個過程是通過CPU和主存、系統總線來完成的,感興趣的小伙伴可以去看我《計算機底層原理專欄》的文章)。
? ? ? ? 4.棧操作:函數的調用通常會涉及到對棧的一些操作、如果將返回地址推入棧中、分配局部變量的空間等。
? ? ? ? 5.返回地址管理:在函數調用的時候、需要將調用點地址作為返回地址保存、以便在函數執行完成后能夠正確返回到調用點。
? ? ? ? 好了現在我們回歸正題:我來為大家解釋一下內聯函數,這個是C語言當中的概念,有興趣的小伙伴可以去了解一下。
? ? ? ? 首先我先使用代碼的形式為大家展示一下內聯函數。
?int add(int a, int b) {return a + b; }inline int add(int a, int b) {return a + b; }
? ? ? ? 我這里為大家提供了一個簡單的函數、用于計算兩個數的和,如果我們在代碼當中多次調用這個函數、會有一些額外的開銷涉及到函數調用的操作(這些操作我在上文當中也已經提到過了),那么這個時候我們為了節省開銷就可以使用內聯函數對這些函數進行函數體展開,什么意思呢?就是字面意思,在函數調用的地方將函數的函數體展開,避免頻繁多次地執行函數調用操作,我們的關鍵字inline會直接告訴編譯器在函數調用點將函數體直接插入、而不是進行常規的函數調用,因此如果我們在代碼當中多次調用add函數,編譯器會盡量地將函數體插入到每個調用點、從而減少函數調用的開銷、當然我這里還要提一句、我們這里的inline關鍵字對于編譯器來說也僅僅是一個建議、不代表我們只要使用這個關鍵字編譯器就一定會將這個函數展開,具體是否展開還是要由編譯器決定的。
? ? ? ? 這樣的優化有助于提高程序的執行效率、尤其是對于短小并且頻繁調用的函數、因為他們的開銷較高、然而需要注意的是內聯函數的過度使用可能導致代碼膨脹,。增加程序的大小、因此、在進行內聯優化的時候需要權衡代碼大小和執行效率。否則好好的程序,因為內聯函數多次展開,會變得極其冗余。
? ? ? ? 6)常量傳播:這個很好理解比如說我定義一個常量。
?#include <iosream> using namespace std; int main(){String str = "Hello World";return 0; }
? ? ? ? 之后程序在用到這個str的時候,就會直接將其轉換為"Hello World",這就是常量傳播。
? ? ? ? 7)最后優化的環節還涉及到很多的內容,例如數組和指針優化、代碼塊合并、指令調度、寄存器分配等等操作,這不是我們今天所講的重點,我就不為大家詳細展開講解了。
6.目標代碼生成
? ? ? ? 首先我在這里問各位一句:什么是目標代碼?我可以很明確地告訴大家目標代碼就是要在目標機器上面執行的代碼就叫做目標代碼,因為我們運行的程序可能在不同的硬件平臺上,所以我們的目標機器是不一樣的所以目標代碼也是不一樣的。
? ? ? ?接下來我為大家解釋一下,為什么需要生成目標代碼,其實這個我剛才已經提到過了,
? ? ? ? 1.可執行性:目標代碼是機器上可以直接執行的代碼形式、與高級源代碼相比更接近計算機硬件的語言、通過目標代碼生成、編譯器將源代碼翻譯成為機器能夠理解和執行的指令、使程序可以在特定的硬件平臺上運行。
? ? ? ? 2.性能優化:目標代碼生成階段可以應用各種優化技術、提高程序的執行效率、這些優化包括指令調度、寄存器分配、循環展開等等、以確保生成的機器代碼在運行的時候能夠更快地執行。? ? ? ? 3.硬件適配性:不同的計算機體系結構和處理器有不同的指令集和架構、目標代碼生成確保編譯后的程序能夠在目標機器上面正確運行、充分利用該機器的特性和性能。
? ? ? ? 4.代碼生成抽象層次:編譯器通過目標代碼生成實現了源代碼到底層硬件的抽象層次轉換,這使得程序員可以使用高級語言編寫程序、而不必擔心底層硬件的細節、同時確保程序在不同的平臺上的可移植性。
? ? ? ? 下面我為大家詳細地講解一下,目標代碼生成的過程(注意:這里不是重點,如果想進一步深入了解編譯的過程的話可以直接去看匯編,如果沒有興趣了解的話,請直接跳過!)
? ? ? ? 1.中間代碼準備:在前面的編譯階段、編譯器會生成中間代碼,這個中間代碼是抽象的中間表示、與機器指令沒有半毛錢關系、它可以是三地址碼、抽象語法樹或者其他中間表現形式、這些中間代碼捕捉了源代碼的語義信息、但是與具體的機器架構無關。
????????2.選擇目標指令集:在目標代碼生成階段之前、編譯器首先要選擇目標機器的指令集架構。即目標機器上支持的指令集、不同的硬件平臺有不同的指令集、因此編譯器需要根據目標機器的特性來選擇相應的指令集。
? ? ? ? 3.寄存器分配:編譯器決定如何分配中間代碼當中的變量到目標機器的寄存器、這包括選擇哪些寄存器存儲變量、以及何時將變量存儲到內存當中去,至于為什么這么干呢?因為CPU對于寄存器的訪問速度遠遠快于CPU訪問內存的速度、所以對于頻繁使用的變量一定要放到寄存器當中去,否則CPU開銷會很大,關于寄存器的具體知識我下文當中會講到。
? ? ? ? 4.指令選擇:對于每個中間代碼操作、編譯器選擇相應的目標機器指令、這可能涉及到將高級操作(如加法、乘法)映射到目標機器的具體指令、同時考慮寄存器的使用和目標機器的特性。? ? ? ? 5.地址計算:如果程序涉及到數組、結構體符合等數據結構、編譯器需要生成代碼來計算這些數據的地址、這可能包括對數組的索引的計算、結構體成員的偏移量計算等等,因為涉及到將中間代碼轉換成為目標代碼,那么轉換后的代碼總歸是要放到內存當中的,這些數據在內存當中是有地址的,所以需要進行地址計算。
? ? ? ? 6.代碼優化:優化可能大家都要猜到了、常量折疊、死代碼消除、循環展開以提高生成代碼的效率和性能。
? ? ? ? 7.生成目標代碼:最后編譯器生成目標機器代碼、并將其寫入輸出文件或者存儲在內存當中、這里面會涉及到生成匯編代碼、調用匯編器將其轉換為機器碼或者直接生成二進制碼(注意紅色的那句話,調用匯編器生成機器碼或者直接生成二進制碼,這里的或者是一個伏筆,我在后面的匯編當中會講到)
? ? ? ??
拓展:匯編
這部分內容雖然是拓展內容,但對于真正理解編譯卻至關重要
? ? ? ?首先我要來解釋一下我上面埋下的那個伏筆。我們必須知道從計算機的角度來看,匯編代碼在程序執行的過程當中是可有可無的,這也就是為什么我將匯編這部分的知識作為拓展知識來進行講解,因為計算機最終執行的是目標代碼,這是由編譯器或者匯編器生成的,直接由計算機硬件執行的機器碼。
? ? ? ? 匯編代碼的存在主要是為了人類程序員而設計的、以便容易理解、調試和優化程序、對于計算機硬件而言、它更關注目標代碼、因為它是能夠直接執行的二進制指令。從計算機的角度來看、匯編代碼可以提供更好的可讀性和調試性、使得程序在開發的過程當中更容易理解和干預程序的執行。
? ? ? ? 所以這也就是為什么我在上面提到了那個或者、因為匯編器將目標代碼翻譯成為匯編代碼這一步并不是必須的。匯編代碼本身是基于目標代碼、為了方便程序員調試所翻譯的機器語言。
????????在這里的中間代碼和優化之間的步驟我還要給大家講解一個非常重要的知識、那就是匯編。
? ? ? ? 這里我要提一句、各位不要把中間代碼和匯編代碼混為一談、中間代碼是編譯過程當中的一個階段、它將源代碼翻譯成為一種抽象的表示形式、這個中間表示形式通常更接近機器語言、但仍然比匯編語言更抽象,具體的中間代碼的表現形式我已經給大家在上文當中呈現出來了。
? ? ? ? 生成的中間代碼可以進一步翻譯成為匯編代碼,這是編譯器的下一個階段、匯編代碼更接近計算機體系結構的語言、是機器語言的一種低級表示。
? ? ? ? 所以我們可以認為中間代碼的生成結果最終會被轉化為匯編代碼、但中間代碼本身不是匯編代碼、中間代碼就是為了在編譯過程當中為了方便處理和優化的一種抽象表示。
? ? ? ? 接下來我為大家列舉一下中間代碼翻譯成為匯編代碼都有哪些過程,(注意:這里涉及到了計算機系統當中最底層的知識,如果對這方面不了解的小伙伴可以直接跳過。)
? ? ? ? 1.寄存器分配:????????編譯器將中間代碼的臨時變量和值會映射到物理寄存器或者棧上,這是為了在生成匯編代碼的時候能夠有效利用計算機的寄存器。編譯器進行寄存器分配的原因主要時為了優化程序的性能,那么為什么要進行寄存器分配呢?
? ? ? ? 寄存器是高速存儲器:????????相比于訪問主存或者緩存,訪問寄存器速度更快,因為寄存器當中含有更多的硅元素(至于為什么硅元素更快這個就不探討了啊,反正他不盡快而且貴,所以數量有限),將頻繁使用的變量或者值放入寄存器當中,否則的話總是訪問內存這對于CPU開銷太大了,而且耗時,所以將這些常用的數據加載如寄存器當中,可以減少對內存的讀寫操作。
? ? ? ? 2.指令選擇:? ? ? ? 指令選擇是編譯器當中的一個關鍵階段,它將中間代碼翻譯成目標機器的匯編代碼、這個階段涉及到高級語言中的抽象操作、映射到底層硬件的具體指令集。以下我為大家列出指令選擇的一些關鍵步驟。
? ? ? ? 操作符映射:編譯器需要將中間代碼中的操作符映射到目標機器的匯編指令,例如將中間代碼當中的加法操作映射到目標機器匯編代碼的加法指令。
? ? ? ? 尋找最佳指令序列:不同的機器指令集可能有不同的指令來執行相似的操作、編譯器需要在性能和代碼大小之間找到平衡,選擇最適合的目標機器指令序列,就例如剛才所說的加法指令、能夠執行加法運算的指令有很多、那么編譯器應該選擇什么樣的指令呢?那么這個時候就涉及到尋找最佳指令序列了,既要找到能夠完成加法操作的又要高效。
? ? ? ? 寄存器分配:在指令選擇階段、編譯器還需要考慮寄存器的使用情況、它決定哪些數據應該存儲到寄存器當中、以及合適將數據從內存當中加載到寄存器當中,以保證高效執行。
? ? ? ? 處理復雜操作:有時候、高級語言當中的一條簡單語句可能需要多條底層指令來實現、例如一條高級語言當中的循環操作可能需要轉化為條件分支、跳轉和遞增指令的序列。
? ? ? ? 優化:指令選擇階段也是進行一些優化的時機、編譯器可以通過選擇適當的指令序列、或者通過一些代數優化、來提高生成的匯編代碼的效率。????????
? ? ? ? 3.地址計算:
????????地址計算是編譯器當中的一個重要任務、它涉及將高級語言中的變量和內存地址映射到底層的匯編指令當中,以下是地址計算的一些關鍵步驟:
? ? ? ? 變量到內存地址的映射:編譯器首先確定高級語言中的每個變量在目標機器當中的具體位置、這通常包括局部變量、全局變量和參數。每個變量都有一個相對于某個參考點(例如函數棧幀或者數據段)的偏移量。
? ? ? ? 尋找基址:在地址計算當中、尋找基址是關鍵的一步、基址是一個相對于參考點的地址、它用于計算其他變量的地址、例如函數的棧幀指針可以作為基址(就是棧頂地址也叫做棧頂指針),用于訪問局部變量和函數參數。
? ? ? ? 計算偏移量:一旦有了基地址,編譯器可以計算變量相對于基址的偏移量、這個偏移量是根據變量在數據結構當中的位置和大小來計算的。? ? ? ? 生成匯編指令:編譯器根據計算出的地址信息生成對應的匯編指令(這個很好理解,如果我都不知道這條語句在哪里的話,我怎么在這個地址處生成相應的匯編指令呢?)、這些指令包括加載指令(load)和存儲 (store)操作、用于將數據從內存當中加載到寄存器當中獲獎寄存器當中的數據存儲到內存當中。(這里明確一個概念數據從內存到寄存器叫做加載,寄存器返回內存叫存儲、其實意思都一樣只是專業的叫法不一樣)
? ? ? ? 處理復雜數據流:在高級語言當中,數據結構可以包含復雜的嵌套和指針引用,編譯器需要處理這些復雜情況,確保計算正確地址、這可能涉及到遞歸地計算嵌套結構的地址。
? ? ? ? 考慮優化:在地址計算階段、編譯器會考慮一些優化策略、例如通過寄存器間接尋址來減少內存訪問、或者通過使用常量折疊等方式來簡化地址。
? ? ? ??
? ? ? ? 4.控制流轉移:
????????首先我要先說明一下這個控制流轉移具體是一個什么樣的東西或者說是什么過程。
? ? ? ? ? ? ? ? 我們要知道控制流轉移的存在是因為程序的執行并不是一條線性的執行流,這里面涉及到了中斷處理或者跳轉等其他操作,程序當中包含了條件分支、循環分支和函數調用結構。控制流轉移允許程序在執行時根據條件或者需要跳轉到不同的代碼段,實現了程序的靈活性和功能性。? ? ? ? 條件執行:控制流轉移允許程序在滿足或者不滿足某個條件時執行不同的代碼塊、知識的程序能夠根據輸入、狀態或者其他條件做出不同的決策、提高了程序的靈活性。
? ? ? ? 循環結構:控制流轉移時是心啊循環結構的關鍵、循環程序允許程序多次執行相同的代碼塊,而不需要顯示重復相同的指令,通過控制流轉移程序能夠回到程序最開始的地方、實現迭代進行。
? ? ? ? 函數調用和返回:控制流轉移用于在程序中調用函數和返回函數、函數調用時、控制流跳轉到函數的入口;函數返回時控制流回到調用點,繼續執行后續的指令。
? ? ? ? 異常處理:控制流轉移用于處理異常情況、當程序發生錯誤或者異常的時候、控制流可以被轉移到相應的異常處理代碼、以采取適當的措施。
? ? ? ? 代碼結構組織:控制流轉移有助于組織代碼的結構、通過合理的控制流轉移、程序員可以實現清晰的邏輯結構、提高代碼的可讀性和可維護性。
? ? ? ? 好的接下來我為大家具體的演示一下,編譯器是怎么進行控制流轉移的。
? ? ? ? 條件語句的轉譯:對于高級語言中的條件語句(if - else)編譯器會生成對應的判斷指令、例如在匯編當中可以使用條件跳轉指令(如 ‘JZ’ ‘JNZ’)來實現,根據條件的成立與否跳轉到不同的代碼塊當中,
????????if (condition) {// code block A } else {// code block B }匯編偽代碼 ; Assuming condition is in register R1 CMP R1, 0 ; Compare condition with 0 JZ code_block_B ; Jump if zero (condition is false) ; code block A JMP end_of_if ; Jump over code block B if condition is true code_block_B: ; code block B end_of_if:
? ? ? ?這里的代碼大家理解就好,控制流轉移就為大家介紹到這里。
????????5.數據傳送:
????????數據傳送階段是編譯器將高級語言中的數據操作(如賦值語句)翻譯成為相應的匯編指令、確保數據正確地從一個位置傳送到另一個位置。數據傳送階段就是編譯器將高級語言當中的數據操作(如賦值語句)翻譯成底層的匯編指令的過程、該階段主要涉及將數據從一個位置傳送到另一個位置、這包括寄存器、內存或者其他數據區域。數據傳送無非就是數據在計算機當中的流動、從寄存器到另一個寄存器或者到內存,只要確保他們能夠正確賦值即可。這里沒有什么特別難理解的,就為大家介紹到這里了。
? ? ? ? 6.優化:
????????優化是編譯器在生成匯編代碼階段進行的重要工作、旨在提高程序的執行效率、優化涉及到對生成的匯編代碼進行改進、以減少執行時間、降低內存消耗等等、優化涉及到對生成的匯編代碼進行改進、以減少執行時間、降低內存消耗、以下是一些常見的優化技術。
? ? ? ? 寄存器優化:編譯器嘗試最大限度地使用寄存器、減少對內存的訪問,這里面包括寄存器的分配和重用(寄存器的分配具體的內容我在上文當中已經講過了,這里就不提了)。以減少數據在寄存器和內存之間的傳輸。
? ? ? ? 常量折疊:編譯器嘗試將常量表達式在編譯的時候進行計算這樣就可以減少運行時的開銷。? ? ? ? 循環展開:如果循環次數較少的話,例如只循環4次循環體直接被展開為4次,以減少循環控制的開銷,這可以提高程序運行的并行性,加快循環的執行。
? ? ? ? 條件分支優化:預測分支的方向以減少分支錯誤的影響,也有可能會進行條件移動等優化。? ? ? ? 死代碼消除:編譯器刪除不會被執行的代碼、以減少可執行文件的大小、并提高執行效率。
? ? ? ?
7.符號表生成
? ? ? ? 這部分的內容是重中之重,雖然我這篇文章花費了大量的篇幅去講解編譯器,但是不要忘了我們這篇文章的主題是基于編譯器理解繼承和多態,加油啊鐵鐵們,鋪墊知識很快就要結束了,千萬別走神!
????????當編譯器在處理源代碼的時候、符號表是一個非常關鍵的數據結構、用于存儲程序當中的標識符(如變量名、函數名等)以及這些標識符相關聯的信息、不過各位一定要注意,雖然我的文章到了這里才開始分析符號表的生成,但是我還是要提醒大家,符號表其實詞法分析的過程就已經開始了,每一個階段都會進行不斷地填充并且完善、知道可執行文件生成才會停下來,這一點我們務必要牢記!
? ? ? ? 現在我為大家詳細講解符號表生成的具體過程。
? ? ? ? 1.初始化符號表:
? ? ? ? 在編譯器的符號表生成階段開始的時候、會初始化一個空的符號表數據結構、這通常是一個哈希表、樹形結構或者其他適合快速檢索的數據結構。通常編譯器會使用哈希表來進行初始化。? ? ? ? 哈希表的鍵值就是標識符的名稱、值是包含信息的數據結構、在初始化階段、這個表是空的、隨著編譯進行,會不斷地向其中添加新的條目。
? ? ? ? 2.·識別標識符:
? ? ? ? 編譯器通過詞法分析階段從源代碼當中識別出詞法單元、這可能包括變量名、函數名、類型名等。詞法分析的具體過程我已經在上文當中講到了,這里就不再進行過多的贅述了。
? ? ? ? 3.處理標識符屬性:
? ? ? ? 對于每個識別到的標識符、編譯器收集相關的屬性信息、如標識符的名稱、類型、作用域等。如果已經存在于符號表當中、可能需要更新已有的條目、如果不存在、則創建一個新的符號表條目。
? ? ? ? 對于每個識別到的標識符、編譯器收集相關的屬性信息、如標識符的名稱、類型、作用域等。如果已經存在符號表中、可能需要更新已有的條目;如果不存在,則創建一個新的符號表。
? ? ? ? 編譯器處理標識符屬性的時候、它必須對每個識別到的標識符進行處理、并且更新或者創建符號表當中的相應條目、以下是一個詳細的步驟。
? ? ? ? 1)識別標識符并收集屬性信息:對于每一個標識符,編譯器要收集相關的屬性信息,這可能包括。
? ? ? ? 名稱:標識符的名字。
? ? ? ? 類型:標識符的數據類型、例如整數、字符串、浮點數等等。
? ? ? ? 作用域:標識符所在的作用域 、例如全局作用域、函數作用域等等。
? ? ? ? 地址:在內存中的地址或者偏移量、用于訪問標識符的存儲位置。
? ? ? ? 大小:標識符所占用的內存大小、對于數組或者結構體等符合類型很重要。
? ? ? ? 值:標識符的當前值、對于常量或者變量而言。
? ? ? ? 其他屬性:可能的其他屬性、如是否是常量是否被初始化等等。
? ? ? ? 2)檢查符號表:這個時候編譯器需要檢查是否已經存在了相同名稱的標識符。
? ? ? ? 如果存在:
? ? ? ? ? ? ? ? 更新信息:如果已經存在了相同名稱的標識符,編譯器更新符號表中該條目的信息、確保它反映源代碼當中的最新屬性。例如可能需要更新類型、作用域、地址大小等信息。
? ? ? ? ? ? ? ? 錯誤檢查:進行必要的錯誤檢查、例如用重復定義的變量或者函數。
? ? ? ? 如果不存在:
? ? ? ? ? ? ? ? 創建新條目:如果符號表當中沒有找到相同的名稱的標識符、編譯器創建一個新的符號表條目,并且將收集到的屬性信息插入到表中。
? ? ? ? 3)繼續分析:編譯器繼續分析源代碼、處理下一個標識符或者語句。
? ? ? ? 這個過程確保符號表保持更新、反映源代碼當中標識符的最新狀態。符號表的正確性對于后續的語義分析、代碼生成和優化階段都至關重要。
? ? ? ? 4.作用域處理:編譯器會跟蹤程序的作用域、并且在符號表當中記錄標識符作用域信息、這有助于處理同名但在不同作用域的標識符。我們應該都知道、作用域就是程序中變量、函數和其他標識符的可見性和訪問范圍。作用域處理對于確保程序中的標識符不會產生沖突或者混淆非常重要、接下來我為大家詳細介紹一下這個環節。
? ? ? ? 1)全局作用域:全局作用域是整個程序的最外層作用域、其中定義的變量和函數在整個程序當中都是可見的。全局作用域由編譯器負責跟蹤和管理。
? ? ? ? 2)局部作用域:局部作用域是在函數或者某個代碼塊內部定義的作用域。在局部作用域中定義的變量通常只在該作用域內可見、每當定義一個新的函數或者代碼的時候、編譯器會創建一個新的作用域。
? ? ? ? 3)作用域鏈:作用域鏈是指在嵌套的作用域結構中,一個標識符查找的路徑。在某個作用域內引用一個標識符時、編譯器會按照作用域鏈逐級查找、直到找到匹配的標識符或者達到全局作用域。這有助于處理同名但在不同作用域的標識符。(這個概念大家理解就好,具體實現涉及到復雜的算法,這里就不詳細解釋了,但是作用域鏈這個概念大家一定要清楚)
? ? ? ? 4)納入符號表:編譯器會將標識符的作用域等信息記錄入符號表以方便管理。
? ? ? ? 5)作用域解析規則:編譯器需要遵循一定的作用域解析規則,確保在不同作用域當中使用相同名稱的標識符時不會發生沖突。一般來說內存作用域的標識符會覆蓋外層作用域的同名標識符。作用域處理是編譯器確保程序中標識符正確可見性和訪問性的關鍵步驟。
? ? ? ? 5.類型處理:對于變量、函數等標識符、編譯器會確定其類型、并在符號表當中記錄這些信息、這對后續的類型檢查和中間代碼生成是至關重要的。類型處理是編譯過程當中確定標識符的數據類型并在符號表當中記錄相關信息、這一過程對于確保程序的類型安全、驚醒類型檢查以及生成有效的中間代碼都只管重要。
????????6.鏈接處理(可選):如果編程語言支持模塊化編程、編譯器可能需要處理符號的鏈接信息、以確保在不同的模塊之間正確引用標識符。
? ? ? ? 7.錯誤處理:編譯器在處理標識符的過程當中可能會遇到一些錯誤、例如重復定義、未聲明等在這一階段、他需要識別并且報告這些錯誤。
? ? ? ? 8.最終符號表:符號表生成階段結束的時候、編譯器會得到一個最終的符號表、其中包含了源代碼當中所有的標識符信息。
? ? ? ? 符號表生成是編譯器前端的關鍵部分、它為整個編譯階段提供了必要的信息,符號表的生成貫穿了整個編譯的過程,它確保了源代碼當中的標識符能夠被正確地識別、管理和使用。
8.錯誤處理
? ? ? ? 負責檢測、報告和處理源代碼當中的錯誤,主要包括錯誤檢測、錯誤報告、錯誤定位、錯誤恢復、錯誤編碼、警告處理等等信息、這個不是我們要研究的重點。
?拓展:
? ? ? ? 在生成可執行文件之前、編譯當中還有一個重要的任務就是鏈接,這個過程是由鏈接器來完成的。那么鏈接器的目的是什么呢?明確地告訴大家,鏈接器的任務就是將所有的編譯工作整合在一起,在這個過程當中符號引用被解析、地址被重定位、各個目標文件和庫全部都被整合在一起,變成一個可以在特定平臺上執行的程序。說白了就是將生成的目標文件組合成一個可執行文件,就這么簡單。所以如果把我們上文當中講到過的有關編譯的知識串聯起來的話很簡單就是以下這些過程。
詞法分析 -> 語法分析 -> 語義分析 -> 中間代碼 -> 優化 -> 目標文件 -> 鏈接器 -> 可執行文件。符號表貫穿了整個過程。
? ? ? ? 在這里我就不對鏈接這個過程做詳細地介紹了,否則這篇文章戰線拉得太長了😂,在農歷年前我會將整個編譯階段的知識點,重新整合成一套完整的知識體系,到那個時候我會詳細介紹鏈接的整個過程,這里對鏈接就暫時先略過了。
9.生成可執行文件
? ? ? ? 最終的階段是將鏈接后的代碼生成一個可執行文件、這個文件包含了程序的完整機器代碼、可以由操作系統加載和執行,這個文件經過最后的優化會生成調試信息、以便程序員在程序出現錯誤的時候進行調試,這些信息包括源代碼行號、變量名稱等等。總體而言、可執行文件生成是編譯過程的最后一步、將所有的編譯、優化和鏈接工作整合在一起、生成可以在特定的平臺上執行的程序。
總結:
? ? ? ? 到這里,編譯器的初步知識我就介紹完了,洋洋灑灑已經寫了一萬五千多字了,下面就要進入我們的正題——從編譯器的角度看多態,前面的都只是鋪墊?。我將用我自己的方法帶領大家從虛方法表和運行時類型標識,帶大家深度理解多態實現的底層邏輯。
二、基于編譯理解繼承和多態
1.細談動靜態綁定
? ? ? ? 1.糾正一個誤區:
? ? ? ? ? ? ? ? 我相信很多小伙伴在第一次聽到動態綁定和靜態綁定這個概念的時候應該是學習多態的時候,這個時候老師們總是會拿出重載和重寫來舉例子,然后就會很敷衍地告訴我們靜態綁定是在編譯的時候確定方法調用目標的機制、而動態綁定是在運行時確定方法調用目標的機制。好了就到了這里以后,我們聽到了靜態綁定和動態綁定的概念,所以從此之后我們認識了靜態綁定和動態綁定,我們就形成了一個刻板的印象,好像多態之所以是多態就是因為他采用的是動態綁定,而動態綁定好像成了多態的標簽、靜態綁定好像也就成為了重載的標簽,從此以后我們的刻板印象也就和重載和多態牢牢地綁定到了一起。當然我沒有說這個概念就是錯的,大家可以看一下我是怎么分析的。
? ? ? ? 2.明確一個概念:
? ? ? ? ? ? ? ? 我剛才也提到了,靜態綁定是在編譯的時候就能夠確定方法調用的目標(就是編譯器就知道要調用哪個方法),而動態綁定是在運行時能夠確定方法調用的目標,好!我問一個問題,各位準備接招!請問!!!什么時編譯時什么是運行時?可能有些小伙伴要懵圈了,不對呀,平常這個代碼我寫完之后直接Ctrl + F5 或者 Ctrl + shift + \ 直接運行出結果了呀?我哪知道什么是運行時什么是編譯時呢?這就是我為什么在這篇文章的前面長篇大論的講了半天編譯,就為這里鋪墊,我可以明確地告訴大家,生成可執行文件及其之前時編譯時,可執行文件加載到內存當中之后進行解析的過程叫做運行時,這個過程都是由編譯器來完成的,一定要記住了各位鐵鐵們,運行時和編譯時的這個概念是相對于編譯器來說的。可不是相對于程序員來說的。
? ? ? ? 3.靜態綁定的使用場景:
? ? ? ? 1)靜態方法調用:靜態方法是與類相關聯的而不是與對象相關聯的、因此他們通常通過類名來進行調用,具有靜態綁定的特性。
class Example {static void staticMethod() {System.out.println("Static method");} }Example.staticMethod(); // 靜態綁定,編譯時確定調用的目標
? ? ? ? 2)final 方法:final 方法是無法被子類重寫的方法,因此他們在編譯的時候就能夠確定方法要調用的目標。
class Example {final void finalMethod() {System.out.println("Final method");} }
? ? ? ? 4.動態綁定的使用場景
? ? ? ? 這里可以明確地告訴大家之所以采用多態往往都是與方法的重寫有關、因為方法需要重寫,所以導致編譯器在編譯的時候無法確定具體執行哪一個方法,也就是說編譯器無法再生成可執行文件之前確定具體調用哪個方法,必須講可執行文件加載到內存之后,JVM利用虛方法表、運行時類型標識和符號表動態地確認,才可以知道具體要調用的是哪個方法,所以我們可以在這里具體地明確一個概念
? ? ? ? 1)重寫方法調用:大多數情況下,實例方法的調用涉及到動態綁定,因為方法的具體調用目標是在運行時基于對象的實際類型動態確認的。
class Animal {void makeSound() {System.out.println("Animal makes a sound");} }class Cat extends Animal {void makeSound() {System.out.println("Cat meows");} }Animal myCat = new Cat(); myCat.makeSound(); // 動態綁定,根據實際類型確定調用目標
? ? ? ? 2)抽象方法和接口方法:在抽象類和接口中定義的為實現方法,它們子類或實現類中被具體實現時涉及到動態綁定。
abstract class Animal {abstract void makeSound(); }class Cat extends Animal {void makeSound() {System.out.println("Cat meows");} }
? ? ? ? 靜態綁定和動態綁定的根本區別就在于靜態綁定在編譯時就能夠確定調用目標、而動態綁定在運行時才能夠確定調用目標,適用于需要在運行時基于實際類型進行靈活調整的場景。
? ? ? ? 好了到了這里我只是介紹了一些場景,我現在又要問?動靜態綁定當中的綁定是指什么意思,是誰和誰進行綁定呢?
? ? ? ? 我在這里可以非常負責人地告訴大家這里的綁定指的是我們在調用方法的時候的方法名和具體的方法實現之間的一種綁定,注意了這個概念看似好像很好理解,能把這一點說清楚的人真不多,我問過很多學編程的學習學妹們,一說起動靜態綁定都知道,能夠在編譯時確定方法調用目標的就是靜態綁定,反之亦然。但是當我問起那具體是誰和誰進行綁定呢?很多人都回答不上來。? ? ? ? 所以我在這里為大家明確了這個概念,而且要想徹底掌握編程當中這些核心的思想、學會計算機底層的知識是必要的,包括編譯原理、我這篇文章當中第一部分的內容希望大家好好消化,為我后面講到多態的具體實現的時候,打好基礎。
?2.虛方法表與多態的關聯
? ? ? ? 虛方法表(VTable)是一種用于實現動態綁定的機制,每個類都有一個對應的虛方法表、虛方法表是一個存放類當中所有虛方法地址的表格,每個對象實例都包含一個指向這個虛方法表的指針(Java當中是引用)。
? ? ? ? 當我們在調用虛方法的時候、實際上是通過虛方法表來查找并且調用相應的方法,這使得在代碼運行的時候能夠動態地確定調用的具體方法,實現了動態綁定。
? ? ? ? 1.什么是虛方法
? ? ? ? 在Java當中虛方法是一種支持動態綁定的方法,具體來說虛方法是指非靜態的實例方法,并且沒有被聲明為static 或者 static。那么具體哪些方法被稱為是虛方法呢?我為大家列舉一些。
? ? ? ? 非靜態方法:虛方法必須要是實例方法、不可以是類方法(靜態方法)。
? ? ? ? 可繼承:虛方法可以被子類繼承。
? ? ? ? 可覆寫:子類可以使用@Override 注解來覆寫方法的虛方法。
? ? ? ? 動態綁定:虛方法支持動態綁定,即在運行時根據對象的實際類型來確定要調用的方法。
? ? ? ?大家應該也發現了,我在論述多態的時候正好是反著來的,多態為什么能夠實現,因為它能夠實現動態綁定因此實現了多態、那么動態綁定又為什么會發生呢?因為有著虛方法表,虛方法表的調用是動態綁定實現的基礎,那么虛方法表又是為什么會調用呢?因為只有以上三種(非靜態、可繼承、可重寫)的方法才能夠調用虛方法表。所以只有這三種方法才可以實現多態。我相信當我說到這里的時候,大家就可以明白了。
????????2.虛方法表如何建立
? ? ? ? 當一個類加載到Java當中的虛擬機當中的時候、會經歷以下這些步驟:
? ? ? ? 加載類:JVM講類的文件加載到內存當中、這一步通過JRE當中的類加載器來實現,類的加載器會解析類的結構信息其中包括類的字段、方法信息等。(這部分內容我在《從JVM看Java》系列當中的第一篇文章當中又詳細講解,感興趣的小伙伴可以去看一下。)
? ? ? ? 準備階段:JVM為類的靜態變量分配內存、并且初始化為默認值、同時、也會為類中的方法建立符號引用(這個引用就是方法名,這里插一句嘴,學過C語言的小伙伴應該知道函數名就是函數的入口地址,當通過方法名來調用函數的時候其實使用了函數的地址,其實這里也一樣的,首先通過對象名調用方法名,這里調用的就是方法的引用,然后對象引用又通過方法當中的this參數也就是this引用來對方法當中的各種變量進行操作,最終得出結果,詳細的過程我的上一篇文章當中已經詳細講過了,這里就不再廢話了)
? ? ? ? 解析階段:JVM會將符號引用替換為直接引用,即將方法的符號引用轉化為實際內存當中方法的地址。
? ? ? ? 創建虛方法表:在內存當中為類的每個方法創建了一個虛方法表的頭目、虛方法表的每個條目包含方法的實際地址。
? ? ? ? 對于非靜態、非私有、非final的實例方法:由于這些方法它們可能會被子類重寫、所以他們這些方法的虛方法表當中存放的都是當前對象的實際類型所對應的方法的地址,這將會運行在運行時根據對象的實際類型來調用正確的方法。
? ? ? ?
package Yangon;public class Animal {public void Print(){System.out.println("Hello World!");} } class Dog extends Animal{@Overridepublic void Print(){System.out.println("Hello Animal!");}public static void main(String[] args) {Animal animal = new Dog();} }
? ? ? ? 大家請看上面這段代碼,Dog類型的虛方法表當中的Print方法會存放Dog類自己的Print
方法,雖然animal對象通過向上轉型變成了Animal類型的對象,但是他的虛方法表當中仍然會存放Dog類自己的虛方法,但是編譯器在編譯階段只知道animal這個對象是Animal類型的,并不知道它是Dog類型的,這一點只有在運行的時候通過運行時類型標識和虛方法表當中的虛方法調用這個過程,編譯器才恍然大悟,原來animal對象并不是Animal類型的而是Dog類型的,這個過程就是動態綁定,這是多態實現的基礎。
? ? ? ? 當我說到這里的時候想必大家也應該理解多態真正的含義了。? ? ??
? ??????對于靜態方法、私有方法、final方法等:他們屬于靜態綁定、不參與動態綁定、虛方法表中存儲自身的地址。不參與動態綁定。虛方法調用的時候調用的還是自己。
? ? ? ? 設置對象的虛方法表指針:每一個對象實例在創建的時候都包含一個指向自己類的虛方法表引用,這個引用存儲在堆當中,虛方法表同類一起存放在方法區當中,JVM可以通過對象的虛方法表引用找到對應類的虛方法表、根據對應的虛方法表索引或者名稱在虛方法表中查找實際方法的地址、然后調用該方法。
? ? ? ? 動態綁定:當調用對象的虛方法的時候、JVM通過對象的虛方法表引用找到虛方法表當中對應的虛方法、根據方法的索引或者名稱在虛方法表中查找實際方法的地址、然后調用該方法。
? ? ? ? 好了虛方法表的內容我就給大家介紹到這里,相信大家已經有了一個初步的認識,接下來我要為大家介紹另外一個非常重要的知識點,運行時類型標識。
????????
總結
? ? ? ? 到這里這篇文章就算是徹底結束了,從12月1日開始一直到今天12月9日,這篇文章是我對于編譯器的初步認識、后續我還會深入學習《編譯原理》我會繼續為大家講解關于編譯器的更多知識,這篇文章結尾我為大家詳細地講解了多態的底層實現原理,我相信很多編程初學者對于這部分的內容其實了解的都不夠深入、這部分的知識我之前也很迷茫、他的定義每一個字我都能夠看懂但是連起來看就是不明白是什么意思、我在瀏覽了很多的視頻查閱了很多的資料、才對這部分的內容有了一個粗略的知識、利用這將近10天的時間整理出來展示給大家。很多老師對這部分的講解實在是不夠透徹、我堅持寫博客一方面鞏固我自己所、另一方面希望能夠幫助到正在迷茫的各位。
? ? ? ? 我相信各位程序員朋友今年都很迷茫、今年的就業形勢不容樂觀、很多小伙伴可能也聽說Java不行了、計算機要沒落了之類的話、但是我想說如果給我一次重來的機會我仍然會選擇計算機這條路、我本人是2024屆應屆生今年的10月份拿到的offer,我大學期間是主修C++的,包括實習崗位也是C++開發崗位、由于工作需要我決定開始轉換語言,我從今年的11月13日開始學習Java,到現在將近一個月的時間了,雖說我是Java的初學者,但是我在大學期間已經系統地學習過《計算機組成原理》、《計算機網絡》、《操作系統》、《C語言》、《數據結構》等計算機基礎課程、所以我這次在學習Java的時候要比最開始學習C++的時候快得多。
? ? ? ? 如果大家有關于學習或者找工作上不明白的問題可以隨時私信我,能夠幫助到大家就是對我最好的鼓勵!諸君共勉。