天天看點

10年大牛帶你從C++代碼的執行過程看編譯器支援面向對象語言

從C++代碼的執行過程看編譯器支援面向對象語言

大家都知道,Java語言作為面向對象程式設計語言中的後來者,吸收了其他進階語言的特點,特别是吸收、借鑒了C++的很多特性。JVM作為位元組碼執行器,在對位元組碼進行編譯和解釋時也借鑒了C++編譯器的實作。與面向過程的語言不同,面向對象的語言有三大特點:封裝、繼承和多态。下面從一個具體的執行個體出發,看一下編譯器是如何支援這三大特點的。C++的代碼示例如下:

struct CPoint{

double xAxis;

double yAxis;

};

class CShape {

private:

double xAxis;

double yAxis;

public:

void setCenter(double xAxis, double yAxis) {

this->xAxis = xAxis;

this->yAxis = yAxis;

}

void setCenter(CPoint point) {

this->xAxis = point.xAxis;

this->yAxis = point.yAxis;

}

virtual string getType() {

string s("Unknown");

return s;

}

};

class CCircle : CShape {

private:

double radius;

public:

virtual string getType() {return string("Circle");}

void setRadius(double radius) {

this->radius = radius;

}

};

注意:C++的文法非常複雜,有靜态成員函數、多繼承、虛繼承、模闆等。這裡隻是為了簡單示範編譯器如何處理面向對象語言,是以僅僅包含了單繼承、函數的重載和重寫。

封裝支援

封裝是面向對象方法的重要原則——把對象的屬性和行為(資料操作)結合為一個獨立的整體,并盡可能地隐藏對象的内部實作細節,外部隻能通過對象的公有成員函數通路對象。編譯器對于封裝的處理相對來說比較簡單,隻要确定好怎麼處理成員函數和成員變量就能正确地處理類。

編譯器對于成員函數的處理方法是把成員函數轉化成類似于C語言中的普通函數,轉化之後編譯器就能像編譯C語言的函數一樣編譯成員函數。轉化的規則也非常簡單,就是為成員函數增加一個額外的參數。例如我們前面提到的CShape類中有一個成員函數void setCenter(double xAxis, doubleyAxis),編譯器首先對這個函數進行轉化,然後再進行編譯。轉化後的函數形式為void setCenter(CShape * const this, double xAxis, doubleyAxis),這就解決了成員函數的編譯問題。

注意:這也是在面向對象語言的成員函數中可以通過this指針通路對象成員變量的原因。因為每一個this指針實際上指向一個具體的對象,這個對象是成員函數的隐式參數之一。

編譯器對成員變量的處理非常簡單,直接按照對象的記憶體布局産生對象即可。比如CPoint類執行個體化的對象布局如圖1-7所示。

10年大牛帶你從C++代碼的執行過程看編譯器支援面向對象語言

圖1-7 簡單對象的記憶體布局

另外需要提到的是,編譯器按照對象的成員變量組織對象的記憶體布局,在這個過程中并不關心對象成員變量的修飾符(如private、protected和public)。也就是說,當記憶體布局組織好以後,編譯器無法控制記憶體的通路,那麼private的成員變量可以通過“某些特殊”手段被非本類的成員函數通路。成員變量和成員函數的修飾符的通路規則是編譯器在編譯過程進行處理,不涉及程式運作時。

因為CShape中存在虛函數,是以編譯器在執行個體化對象的時候會增加一個額外指針的空間用于存儲虛函數表的位址。虛函數表中存放的是函數的位址,這個指針的目的是支援多态,下面會詳細介紹。CShape類執行個體化的對象布局如圖1-8所示。

10年大牛帶你從C++代碼的執行過程看編譯器支援面向對象語言

圖1-8 包含虛函數對象的記憶體布局

注意:vptr的位置和編譯器實作有關,有些編譯器将vptr放在對象布局的起始位置,有些則将vptr放在對象記憶體布局的最後。

繼承支援

繼承是面向對象最顯著的一個特性,繼承是從已有的類中派生出新的類,稱為子類。子類繼承父類的資料屬性和行為,并能根據自己的需求擴充出新的行為,提高了代碼的複用性。

編譯器對于繼承的實作也不複雜。還是從兩個方面考慮,繼承對于成員函數的處理并不影響,也無關成員函數是不是虛函數。對于成員變量的處理,編譯器需要把父類的成員變量全部複制到子類中。在上例中,CCircle繼承于CShape,CCircle類執行個體化的對象布局如圖1-9所示。

10年大牛帶你從C++代碼的執行過程看編譯器支援面向對象語言

圖1-9 對象繼承後的記憶體布局

C++中還支援多繼承,如果多個父類都定義了虛函數,即對象布局可能都需要一個vptr,大多數編譯器會将多個vptr合并成一個。當然這也與編譯器的實作有關,由于這些内容涉及C++編譯器的實作細節,且與本書内容關系不密切,是以不再進一步介紹,有興趣的讀者可以參考其他書籍。

多态支援

多态指的是一個接口多種實作,同一接口調用可以根據對象調用不同的實作,産生不同的執行結果。多态有兩種形式,一種是靜态多态,另一種是動态多态。

靜态多态也稱為函數重載(overlap)。在早期的C語言中,每個函數的名字都不相同,是以可以直接通過函數名唯一地确定函數。例如,在CShape中有兩個函數名字相同的setCenter,是以不能通過函數名來唯一地确定函數。編譯器采用的方法是對函數名進行編碼(稱為name mangling),編碼的規則不同,編譯器的實作也不同,原則是把函數名、參數個數、參數類型等資訊編碼成唯一的一個函數名(也稱為函數的簽名)。在Linux中對上述檔案進行編譯,然後可以通過nm指令檢視編譯後的函數簽名。可以得到兩個不同的函數簽名,分别為:

_ZN6CShape9setCenterE6CPoint,對應成員函數setCenter(CPointpoint)。

_ZN6CShape9setCenterEdd,對應成員函數setCenter(double xAxis,double yAxis)。

關于Name Mangling的具體編碼規則,可以參考其他書籍或文章。

動态多态也稱為函數重寫(override),該機制主要通過虛函數實作。

編譯器對于虛函數的實作主要通過增加虛函數指針和虛函數表的方式來實作。編譯器會在資料段中增加一個資料空間,稱為虛函數表,虛函數表中存放的是編譯後函數的位址,同時在類的構造函數中把執行個體化對象的虛指針指向虛函數表。CShape示例化對象的布局如圖1-10所示。

10年大牛帶你從C++代碼的執行過程看編譯器支援面向對象語言

圖1-10 CShape示例化對象布局

CCircle示例化對象的布局如圖1-11所示。

10年大牛帶你從C++代碼的執行過程看編譯器支援面向對象語言

圖1-11 CCirle示例化對象布局

從編譯器的角度來看,當CCircle重寫了CShape的虛函數(此處為getType),編譯器會在CCircle對應的虛函數表中修改函數的位址,此函數的位址為CCircle中函數的位址。若CCircle僅僅繼承CShape的虛函數,但并沒有重寫,則CCircle的虛函數表中函數的位址仍然指向CShape中函數的位址。

另外,在圖1-10和圖1-11中都指出虛函數表(vtbl)位于資料段中,這樣設計主要是因為使用該資料時隻需要讀權限,而不需要執行權限。但這并不意味着虛函數表會動态地變化,實際上虛函數表在編譯時唯一确定,在程式執行過程中并不會變化。

編譯器支援封裝、繼承和多态的特性以後,也會按照與C語言一樣的方式生成可執行檔案,并且也按照對應的調用約定支援函數調用。

本文給大家講解的内容是Java虛拟機和垃圾回收基礎知識:從C++代碼的執行過程看編譯器支援面向對象語言

  1. 下篇文章給大家講解的内容是Java虛拟機和垃圾回收基礎知識: Java代碼執行過程簡介
  2. 覺得文章不錯的朋友可以轉發此文關注小編;
  3. 感謝大家的支援!

繼續閱讀