C 編譯過程

在開發 C++程式時,下一步是在執行程式之前編譯該程式。編譯是將用 C,C++等人類可讀語言編寫的程式轉換為中央處理單元直接理解的機器程式碼的過程。例如,如果你有一個名為 prog.cpp 的 C++原始碼檔案並執行了 compile 命令,

   g++ -Wall -ansi -o prog prog.cpp

從原始檔建立可執行檔案涉及 4 個主要階段。

  1. C++前處理器採用 C++原始碼檔案並處理標頭檔案(#include),巨集(#define)和其他預處理程式指令。

  2. 由 C++前處理器生成的擴充套件 C++原始碼檔案被編譯為平臺的組合語言。

  3. 編譯器生成的彙編程式碼被組裝到平臺的目的碼中。

  4. 彙編程式生成
    的目的碼檔案與用於生成庫或可執行檔案的任何庫函式的目的碼檔案連結在一起。

預處理

前處理器處理前處理器指令,如#include 和#define。它與 C++的語法無關,這就是必須謹慎使用的原因。

它一次在一個 C++原始檔上工作,將#include 指令替換為相應檔案的內容(通常只是宣告),替換巨集(#define),並根據#if 選擇不同的文字部分, #ifdef 和#ifndef 指令。

前處理器適用於預處理令牌流。巨集替換被定義為用其他標記替換標記(操作符##在有意義時允許合併兩個標記)。

在所有這些之後,前處理器產生單個輸出,該輸出是由上述變換產生的令牌流。它還新增了一些特殊的標記,告訴編譯器每行的來源,以便它可以使用它們來產生合理的錯誤訊息。

通過巧妙地使用#if 和#error 指令,可以在此階段產生一些錯誤。

通過使用下面的編譯器標誌,我們可以在預處理階段停止程序。

g++ -E prog.cpp

彙編

編譯步驟在前處理器的每個輸出上執行。編譯器解析純 C++原始碼(現在沒有任何前處理器指令)並將其轉換為彙編程式碼。然後呼叫底層後端(工具鏈中的彙編程式)將該程式碼組裝成機器程式碼,生成某種格式的實際二進位制檔案(ELF,COFF,a.out,…)。此目標檔案包含輸入中定義的符號的編譯程式碼(以二進位制形式)。目標檔案中的符號按名稱引用。

物件檔案可以引用未定義的符號。使用宣告時就是這種情況,並且沒有為它提供定義。編譯器不介意這一點,只要原始碼格式正確,編譯器就會愉快地生成目標檔案。

編譯器通常會讓你在此時停止編譯。這非常有用,因為有了它,你可以單獨編譯每個原始碼檔案。這提供的優點是,如果你只更改單個檔案,則無需重新編譯所有內容。

生成的目標檔案可以放在稱為靜態庫的特殊存檔中,以便以後重用。

正是在這個階段,報告了常規編譯器錯誤,如語法錯誤或失敗的過載解析度錯誤。

為了在編譯步驟之後停止程序,我們可以使用 -S 選項:

g++ -Wall -ansi -S prog.cpp

組裝

彙編程式建立目的碼。在 UNIX 系統上,你可能會看到帶有 .o 字尾(MSDOS 上的 .OBJ)的檔案,以指示目的碼檔案。在此階段,彙編程式將這些目標檔案從彙編程式碼轉換為機器級指令,並且建立的檔案是可重定位目的碼。因此,編譯階段生成可重定位目標程式,並且該程式可以在不同地方使用而無需再次編譯。

要在組裝步驟後停止該過程,可以使用 -c 選項:

g++ -Wall -ansi -c prog.cpp

連結

連結器是彙編程式生成的目標檔案的最終編譯輸出。此輸出可以是共享(或動態)庫(雖然名稱類似,但它們與前面提到的靜態庫沒有多少共同之處)或可執行檔案。

它通過用正確的地址替換對未定義符號的引用來連結所有目標檔案。這些符號中的每一個都可以在其他目標檔案或庫中定義。如果它們是在標準庫以外的庫中定義的,則需要告知連結器它們。

在此階段,最常見的錯誤是缺少定義或重複定義。前者意味著定義不存在(即它們沒有被寫入),或者它們所在的目標檔案或庫沒有被賦予連結器。後者是顯而易見的:在兩個不同的目標檔案或庫中定義了相同的符號。