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

大數據架構師 發佈 2022-11-15T17:19:43.502954+00:00

大家都知道,Java語言作為面向對象程式語言中的後來者,吸收了其他高級語言的特點,特別是吸收、借鑑了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所示。

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

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

注意:vptr的位置和編譯器實現有關,有些編譯器將vptr放在對象布局的起始位置,有些則將vptr放在對象內存布局的最後。

繼承支持

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

編譯器對於繼承的實現也不複雜。還是從兩個方面考慮,繼承對於成員函數的處理並不影響,也無關成員函數是不是虛函數。對於成員變量的處理,編譯器需要把父類的成員變量全部複製到子類中。在上例中,CCircle繼承於CShape,CCircle類實例化的對象布局如圖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所示。

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

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

另外,在圖1-10和圖1-11中都指出虛函數表(vtbl)位於數據段中,這樣設計主要是因為使用該數據時只需要讀權限,而不需要執行權限。但這並不意味著虛函數表會動態地變化,實際上虛函數表在編譯時唯一確定,在程序執行過程中並不會變化。

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

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

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