開篇:預備知識—2

  • 2019 年 11 月 14 日
  • 筆記

版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://blog.csdn.net/Hacker_ZhiDian/article/details/103058927

前言

​ 在前一篇文章中我們大致介紹了 C語言的一些預備知識,對其中的某些常用知識點進行了一個概述。這篇文章中我們來通過實踐的形式來加深對之前知識點的理解。

程式的編譯過程

​ 我們在上篇文章中提到 C語言編譯器將一個源程式編譯成可執行程式大致需要經過預處理編譯彙編鏈接這四個過程。我們來藉助 GCC 編譯器來詳細看看這幾個過程。在開始之前確保你的電腦已經成功安裝了 GCC 編譯器。Linux 系統是自帶 GCC 的,如果你是 Windows 系統,則需要通過 MinGW 組件安裝 GCC,具體過程可以參考 這篇文章。完成之後如果你在命令行中執行 gcc -v 命令可以得到 GCC 的相關資訊證明 GCC 的相關程式組件安裝完成:

預處理

​ 預處理是用來處理 C語言中的 #include#define 等預處理指令的,每一個預處理指令有其對應的處理方式。比如遇見 #include 指令時將其包含的頭文件內容插入到該指令所在位置。我們可以通過執行 gcc -E 源文件路徑 -o 輸出文件路徑 指令來對 C語言源文件進行預處理操作。為了驗證這個過程,我們自定義兩個頭文件:custom1.hcustom2.hcustom1.h 內容如下:

int maxx(int, int, int);

我們在這個頭文件裡面聲明了一個函數,名為 maxx,這個函數的目標功能為求出 3 個數中的最大值

custom2.h 內容如下:

#include "custom1.h"  int minn(int, int , int);

在這個函數中我們通過 #include 指令將 custom1.h 頭文件包含了,同時還聲明了一個函數 minn,目的為求出 3 個數中的最小值。下面我們來寫一段簡單的源程式碼:

#include "custom1.h"    int main() {      return 0;  }

我們將其命名為 hello.c。然後在該文件同級目錄下鍵入指令 gcc -E hello.c -o hello.i

我們也可以通過 cpp 程式來單獨完成預處理這一過程:cpp hello.c -o hello.i。(這裡的 cppC Preprocessor 的縮寫)。我們可以在同級目錄下發現多了一個 hello.i 文件,這個文件依然是一個文本文件,我們可以用文本瀏覽器查看其文件內容:

可以看到,除了添加了部分注釋之外,#include 指令將 custom1.h 頭文件中的文本內容複製到 #include 指令所在的位置了。那麼當被包含的頭文件中還包含了其他頭文件時情況如何呢?我們拿上面的 custom2.h 頭文件舉例子,將 hello.c 的程式碼內容改為:

#include "custom2.h"    int main() {      return 0;  }

再執行同樣的 gcc 指令:gcc -E hello.c -o hello.i,得到的 hello.i 如下:

通過結果我們可以知道預處理指令是嵌套進行的,在處理上面的 custom2.h 頭文件時,發現其有 #include "custom1.h" 指令,於是繼續處理這個 #include 指令… 直到頭文件中沒有對應的預處理指令為止。

編譯

​ 經過了第一步預處理之後,我們就可以將預處理操作輸出的結果文件進行編譯了,GCC 指令為 gcc -S 預處理後的文件 -o 輸出文件路徑及文件名 。我們來編譯上文中得到的 hello.i 文件:gcc -S hello.i -o hello.s

我們也可以通過 ccl 程式來單獨完成編譯這一過程:ccl hello.i -o hello.s。此時得到的 hello.s 文件就為編譯後的彙編文件,裡面的程式碼為彙編程式碼:

彙編

​ 得到對應的彙編程式碼後,我們就可以通過執行彙編指令將對應的彙編程式碼轉換為二進位文件了,GCC 指令如下:gcc -C 彙編文件 -o 輸出文件路徑。我們來將上文中的彙編文件轉換為二進位文件,執行 gcc -c hello.s -o hello.o

我們也可以通過 as 程式單獨完成彙編這一過程:as hello.s -o hello.o。經過彙編過程之後得到的 hello.o 文件就是二進位文件了。如果我們用文本編輯器打開會得到一堆亂碼:

但是此時得到的 hello.o 文件還不能執行,如果想讓其可執行,我們還需要進行鏈接步驟。

鏈接

​ 我們已經通過上面的 彙編 步驟得到二進位文件了,為什麼還不能執行呢?因為我們上面的到的不是真正的可執行文件,其缺少一些必要的系統入口程式碼和庫實現文件。我們需要通過鏈接操作來添加必要的系統入口程式碼和程式中使用到的庫實現文件。啟動鏈接的指令為:gcc hello.o -o hello.exe。事實上,這也是 GCC 將源文件直接編譯為可執行文件的指令(gcc 源文件 -o 可執行文件輸出路徑)。

我們也可以通過 ld 程式單獨完成鏈接這一過程:ld hello.o -o hello.exe。我們現在得到了可執行程式了。通過命令行執行:

納尼,什麼也沒輸出嗎?對的,因為我們的源程式中的 main 方法什麼也沒有做,是一個空方法。好了,我們已經成功的通過 4 個編譯步驟將 C語言源程式 「變」 成了可執行文件。如果你已經很熟悉這個過程或者不想這麼麻煩,你也可以直接使用 gcc 源文件路徑 -o 可執行文件輸出路徑 命令直接將源程式編譯成可執行文件。

上面說到了鏈接過程需要加入必要的系統入口程式碼和庫實現文件,因為系統入口程式碼和各個作業系統有關,而 GCC 在鏈接過程中會幫我們添加。所以我們來著重討論一下後面的庫實現文件。比如說我們上面在 custom1.h 中聲明了一個 int maxx(int, int, int ) 函數用來求出 3 個數中的最大值。但是我們並沒有實現這個函數,即沒有為這個函數編寫對應的函數體。所以編譯器在鏈接過程中需要尋找對應函數的實現庫文件並將其加入調用了該函數的源程式編譯得到的 .o 文件中。在這裡鏈接器不需要尋找該函數的實現庫文件,因為我們在 hello.c 源程式文件中的 main 函數中並沒有調用這個 maxx 函數,所以此時鏈接過程只需要將之前的 hello.o 文件中加入必要的系統啟動程式碼後即可以生成可執行文件。這也是之前我們可以成功執行鏈接得到可執行文件的原因。那麼如果我們把 hello.c 的源程式碼改成如下呢:

#include <stdio.h>  #include "custom1.h"    int main() {      int maxValue = maxx(1, 2, 3);      printf("%dn", maxValue);      return 0;  }

即我們再 main 函數中調用了 custome1.h 頭文件中聲明的函數 maxx,編譯過程中會有什麼變化呢?你會發現你可以成功的執行編譯的前三個步驟:預處理編譯彙編,在將彙編過程產生的 .o 文件進行鏈接成可執行文件的時候會得到以下錯誤資訊:

這個問題相信很多小夥伴都遇見過,意為未定義指向 maxx 函數的引用。即鏈接器找不到指向 maxx 函數的實現。這時我們可以有兩種解決方法:

1、自己寫一個實現了 maxx 函數的源程式文件(.c),將其編譯成 .o 文件,並和 hello.o 一起執行鏈接。

2、找到一個實現了 maxx 函數的庫文件並和 hello.o 一起執行鏈接。

這兩種方法的核心都是需要補齊 maxx 函數的實現庫文件,並在鏈接過程中加入 hello.o 中。當然最簡單的方法就是直接在 hello.c 源文件中自己實現一個 maxx 函數。我們在這裡選擇第一種方法,即自己寫一個實現了 maxx 函數的源程式將其編譯後鏈接和 hello.o 一起進行鏈接過程。

創造和使用庫

​ 為了解決上面的問題,我們需要寫一個 C語言源文件並將其編譯成 .o 文件,我們在 custom1.h 的同級目錄下新建一個 custom1.c 文件

int maxx(int a, int b, int c) {  	return a > b ? (a > c ? a : c) : (b > c ? b : c);  }

下面我們需要對其進行預處理、編譯、彙編三個步驟得到對應的 .o 文件,為了簡單,這裡直接使用命令 gcc -c custom1.c -o custom1.o 命令將源程式文件其直接編譯成 .o 文件:

現在我們已經得到了實現了 maxx 函數的 .o 文件 custom1.o。我們重新啟動鏈接,將 hello.ocustom1.o 文件鏈接成一個可執行文件,執行命令: gcc hello.o custom1.o -o hello.exe:

可以看到,這次鏈接沒有報錯,我們來執行得到的可執行文件(hello.exe)看看:

Ok,完成。其實在這個過程中我們就已經創建了一個我們自己的庫:custom1.o,裡面實現了一個函數:maxx 可以求出三個整數中值最大的數。使用到這個庫的程式可以說成是這個庫的庫依賴程式。

​ 思考一下,如果每次鏈接一個庫都需要將其加入 GCC 的命令行參數中,比如上面如果我們還需要鏈接一個名為 custom2.o 的庫,那麼我們就會寫成 gcc hello.o custom1.o custom2.o -o hello.exe。那麼假設我們有一個非常大的程式,其需要鏈接很多個 .o 文件,那麼我們總不能每編譯一次就寫一次這麼長的命令行吧。此時我們可以藉助 ar 工具(ar 為 archive 的縮寫,它並不是一個編譯工具,而是一個文件打包工具)將多個 .o 文件打包成一個大的庫文件。命令為:ar -rcs 生成的庫文件路徑 xx1.o xx2.o xx3.o ...

我們來試驗一下,在 custom2.h 同級目錄下(建議將當前創建的所有文件都放在同一目錄)。創建一個新的 custom2.c 文件,來實現 minn 函數:

int minn(int a, int b, int c) {      return a < b ? (a < c ? a : c) : (b < c ? b : c);  }

同樣的通過 gcc -c custom2.c -o custom.o 將其編譯為 .o 文件。這時我們就有了兩個 .o 文件:custom1.ocustom2.o。我們來通過 ar 程式打包:

完成之後我們就有了一個大的庫文件,這其實就是我們創建的庫文件,裡面包含了 custom1.hcustom2.h 頭文件中的聲明的函數實現。既然我們在上面 custom2.o 中實現了 minn 函數,那麼我們就可以在主程式的 main 函數中調用 minn 函數了。修改主程式(hello.c)程式碼如下:

#include <stdio.h>  // 這裡因為 custom2.h 已經 #include "custom1.h" 了,所以這裡直接包含 custom2.h 即可  #include "custom2.h"    int main() {      int maxValue = maxx(1, 2, 3);      int minValue = minn(1, 2, 3);      printf("max value = %dn", maxValue);      printf("min value = %dn", minValue);      return 0;  }

我們直接通過命令:gcc hello.c libcustom.a -o hello.exe 即可完成整個編譯鏈接等過程,在鏈接過程中會自動將 libcustom.a 庫文件和彙編得到的 hello.o 進行鏈接,得到可執行文件,不過這個過程被隱藏了,我們看不到。整個操作結果如下:

成功!到這裡我們已經成功的創建了並使用了我們自己的庫。不知道小夥伴有沒有好奇,在上面我將 custom1.ocustom2.o 打包成一個庫的時候為什麼要將打包後的庫文件命名為 libcustom.a。這不是偶然,我們將在下面的小節中討論這個問題。

動態庫和靜態庫

​ 在上面我們已經成功的創建並使用了我們自己的庫(libcustom.a)。為什麼我要將庫文件命名為 libcustom.a 呢?這其實和庫文件的種類和命名標準有關。庫文件種類分為兩種:動態鏈接庫和靜態鏈接庫。

動態鏈接庫

​ 動態鏈接庫即為動態載入的,在鏈接時不將整個庫文件鏈入可執行程式中,只是將庫文件的資訊放入可執行文件中。在可執行程式運行時如果需要使用該動態鏈接庫中的某個模組或者函數時再進行動態載入。這樣的話可以減少可執行程式文件的大小。在 Linux 下動態鏈接庫的文件後綴名為 .so。在 Windows 下為 .dll。我們可以通過 GCC 來創建動態鏈接庫文件,為了方便,這裡直接使用上文中得到的兩個 .o 文件(custom1.ocustom2.o)進行操作。我們通過命令:gcc custom1.o custom2.o -shared -o libcustom.dll。因為我的 PC 為 Windows 系統,因此這裡將生成的動態鏈接庫文件後綴名設置為 .dll,如果是 Linux 系統,那麼文件後綴名改為 .so。我們也可以直接使用 custom1.ccustom2.c 兩個源程式文件來創建動態鏈接庫:gcc custom1.c custom2.c -shared -o libcustom.dll。不過這樣需要將兩個源程式文件重新編譯成 .o 文件再創建對應的鏈接庫,因此相比直接使用 .o 文件來說速度會更慢。我們來看看操作結果:

成功。這裡我們用 GCC 編譯時用到了 -L-l 參數,關於 GCC 的常用編譯參數我們下個小節再進行討論。上面提到過:使用動態庫鏈接到的可執行程式是在程式運行並使用到對應庫中的數據時被載入,即為運行時載入。也就是說雖然我們通過動態庫鏈接得到了可執行程式。我們也不能將對應的動態庫刪除,否則當程式運行時找不到要載入的動態鏈接庫就會報錯。這裡我有意刪除了生成的 libcustom.dll 動態庫文件,運行結果如下:

如果小夥伴對 Windows 系統接觸的多的話,我相信你一定遇見過這種類型的錯誤。遇見這種錯誤時通常重新安裝程式可以解決。但其本質原因還是因為丟失了某些程式運行時必須的動態鏈接庫文件導致的。

我們再來此時看看生成的 hello.exe 的文件大小:

這裡我們先暫且記下,待會和使用靜態鏈接庫生成的可執行文件進行一個對比。

好了,這裡我們成功的創建並使用了動態鏈接庫。這個動態鏈接庫不僅可以給我們用,還可以提供給運行在其他相同作業系統的程式中使用。這就是庫文件存在的最大價值:共享性。我們在實現一些功能時,可以先查找並使用已經存在的庫文件,一方面減少我們自己的工作量,另一方面可以保證程式碼品質(庫文件被多人使用過,其正確性已經被各種數據場景驗證過)。

靜態鏈接庫

​ 靜態鏈接庫的作用和動態鏈接庫一樣,都是用來共享,減輕工作量和提升程式碼品質。不過在機制上有所不同。上問提到:使用動態鏈接庫文件時並不是將整個庫文件鏈入可執行程式文件中,而是在可執行文件中存入動態鏈接庫文件的相關資訊,以供程式在運行過程中在需要時進行動態鏈接庫文件的載入。而對於靜態鏈接庫來說,其在鏈接過程中就將整個庫文件鏈入可執行程式文件中,這樣程式在運行時就無需動態載入庫文件。也就是說生成的程式就是一個完整的可執行程式,無需依賴外部庫文件。但是生成的可執行文件大小肯定比使用動態鏈接庫更大。我們其實在上面已經生成過靜態鏈接庫文件了,聰明的小夥伴已經猜到了,沒錯,就是在上面通過 ar 工具生成的 libcustom.a。靜態鏈接庫文件的後綴名在 Windows 和 Linux 系統中一樣,都是 .a。我們可以藉助 ar 工具將多個已經編譯好的 .o 文件打包成一個靜態鏈接庫文件。因為 ar 是一個打包工具,不具備編譯功能,因為我們必須提供已經編譯好的文件讓其進行打包。這裡我們直接將上面已經編譯好的 custom1.ocustom2.o 文件打包成靜態鏈接庫文件:ar -rsc libcustom.a custom1.o custom2.o。操作結果如下:

我們來看看此時生成的可執行文件的大小:

比使用動態鏈接庫生成的可執行文件大幾百位元組左右。這是因為鏈接的靜態庫比較小,差距不是特別明顯,當鏈接大型庫文件時,這兩種類型對應生成的可執行文件的大小差距就很明顯了。同時,因為使用的是靜態鏈接庫。因此即使刪除了 libcustom.a 庫文件後程式依然可以正常執行。這裡不做演示了,小夥伴們可以自行嘗試。

我們在上面生成動態鏈接庫和靜態鏈接庫文件時,採用的文件名都是以 lib***.a / lib***.dll 的形式,即為以 lib 前綴開頭。這是因為 GCC 在進行鏈接庫文件查詢時會自動會為參數指定的庫文件名加上前綴和對應的後綴,比如上面我們採用了命令行 gcc hello.c -L"." -lcustom -o hello.exe,這裡通過 -L 參數來指定額外鏈接庫所在的目錄,這裡指定為 "." 即為當前源程式文件所在的目錄(相對目錄);再通過 -l 參數指定要載入的庫文件,這裡指定的庫文件為 custom。GCC 自動補全前綴後的庫文件名為 libcustom。那麼後綴名如何確定呢?GCC 優先使用動態鏈接庫,也就是說當鏈接庫文件夾中存在動態鏈接庫文件的時候,使用動態鏈接庫文件進行鏈接操作,此時確定的庫文件名為 libcustom.dll(Windows 系統)或者 libcustom.so(Linux 系統),當鏈接庫文件夾中不存在動態鏈接庫文件時,才使用靜態庫文件,你也可以在編譯命令中加入 -static 參數來禁止 GCC 使用動態庫進行鏈接,這時 GCC 只會使用靜態庫來進行鏈接,生成可執行文件也會更大。因為在這裡對應目錄下沒有動態鏈接庫文件(libcustom.dll),只有靜態鏈接庫文件(libcustom.a),因此在這裡確定的庫文件名為 libcustom.a。整條命令的含義為將 hello.c 源程式編譯為可執行程式文件 hello.exe,在鏈接過程中將 hello.c 文件所在目錄下的 libcustom.a 文件作為需要額外鏈接的靜態庫文件。 在我們之後在進行創建鏈接庫文件時應該遵循這種命名規則,這樣我們創建的庫文件才具有通用性。

GCC 常用編譯參數

​ 我們先簡單總結一下GCC 編譯 C語言程式的過程:先進行預處理,查找源文件中包含(#include)的頭文件和其他文件,找到之後進行內容處理和替換,在預處理指令全部處理完成後進行編譯,將 C語言程式碼編譯成彙編程式碼然後進行彙編。彙編過程將彙編程式碼變成二進位文件,最後經過鏈接處理生成可執行文件。在 Linux 系統下,GCC 在預處理時默認會在 /usr/include 文件夾中搜索使用到的頭文件,在鏈接時會在 /usr/lib 文件夾中搜索要鏈接的庫文件,Windows 下為 MinGW 安裝目錄的 includelib 子目錄下。我們在上面已經用過了 -L-l 參數,分別用來指定 GCC 在鏈接過程中需要額外鏈接的庫文件目錄和鏈接的庫名。這是在鏈接階段使用到的參數,我們還可以通過 -I 參數指定 GCC 在預處理過程中需要額外搜索的頭文件目錄路徑。假設我們有以下程式碼:

#include <stdio.h>  #include "sub-header.h"    int main() {      return 0;  }

我們將其命名為 header_test.c。可以看到我們包含了一個新的頭文件,那麼我們新建一個空的 .h 文件,名為 sub-header.h,為了測試,我們將其放在 header_test.c 所在目錄的的子目錄 sub-header 下:

然後用 GCC 對其進行編譯:

這裡報錯了,說沒有 sub-header.h 文件。這個很好理解解決這個問題可以有兩個方法:

1、將上面 header_test.c#include "sub-header.h" 程式碼改為 #include "./sub-header/sub-header.h",即使用相對路徑將 test_header.h 正確的路徑包含進程式碼中。

2、將 sub-header.h 文件放在 GCC 默認會搜索的頭文件目錄中,Linux 下為 usr/include,Windows 下為 MinGW 安裝目錄的 include 子目錄下。

3、將 sub-header 的相對路徑 / 絕對路徑通過 -I 參數加入 GCC 編譯命令中,使 GCC 將 sub-header 目錄作為頭文件搜索目錄之一。

這裡我們採用第 3 種方式,將編譯命令改為 gcc header_test.c -o header_test.exe -I"./sub-header"

OK,成功編譯沒有報錯,因為在 header_test.cmain 函數中我們什麼都沒有做,因此這裡運行 header_test.exe 時沒有任何輸出。

除了這幾個參數之外。GCC 還有非常多的編譯參數,這裡列舉幾個:

-isysroot xxx:將 xxx 作為頭文件搜索的邏輯根目錄,和 --sysroot 參數類似,不過只作用於頭文件。

--sysroot=xxx:將 xxx 作為邏輯根目錄。編譯器默認會在 /usr/include/usr/lib 中搜索頭文件和庫,使用這個選項後將在 xxx/usr/includexxx/usr/lib 目錄中搜索。如果使用這個選項的同時又使用了 -isysroot 選項,則此選項僅作用於庫文件的搜索路徑,而 -isysroot 選項將作用於頭文件的搜索路徑。

-std=xxx:選擇編譯時採用的 C語言標準,不同的標準支援的語法和 API 會略有不同。比如 -std=c89 指名是用 c89 標準編譯程式。-std=gnu99 使用 ISO C99 標準進行編譯,同時加上一些 GNU 擴展。

-Ox:編譯時採用的優化等級,有 O0, O1, O2, O3 4 個選項。默認為 O1(偷偷告訴你,O2O3 等級的編譯優化支援尾遞歸)。

-m32-m64:生成適用於 32 位 / 64 位機器字長電腦的可執行程式文件。

關於其他的 GCC 編譯參數,可以參考 GCC 使用幫助

make 和 makefile

​ 當我們在編譯大型程式的時候,一次性要編譯多個文件,此時我們的 GCC 編譯命令會很長,所以每一次編譯的時候都去寫一遍 GCC 編譯命令是一件非常低效的事。因此 GCC 中提供了 make 工具(和 ar 類似,是一個工具類程式)讓我們可以更方便快捷的進行大型程式編譯。

要在項目中使用 make 工具,我們需要在項目文件夾下編寫 makefile 文件,在執行 make 程式的時候,它會到當前工作目錄下尋找 makefile 文件並讀取其中的內容並按照 makefile 的內容來執行對應的操作。我們來實踐一下,新建一個文件夾,名為 make-test,把這個文件夾作為新的工程目錄。然後我們將上面書寫過的 hello.c, custom1.h, custom1.c, custom2.h, custom2.c 文件複製進入 make-test 文件夾中。這幾個文件的內容如下。

hello.c:

#include <stdio.h>  // 這裡因為 custom2.h 已經 #include "custom1.h" 了,所以這裡直接包含 custom2.h 即可  #include "custom2.h"    int main() {      int maxValue = maxx(1, 2, 3);      int minValue = minn(1, 2, 3);      printf("max value = %dn", maxValue);      printf("min value = %dn", minValue);      return 0;  }

custom1.h:

int maxx(int, int , int );

custom1.c:

int maxx(int a, int b, int c) {  	return a > b ? (a > c ? a : c) : (b > c ? b : c);  }

custom2.h:

#include "custom1.h"    int minn(int , int , int );

custom2.c:

int minn(int a, int b, int c) {      return a < b ? (a < c ? a : c) : (b < c ? b : c);  }

最後,我們在 make-test 目錄下新建一個名為 makefile 的文件並使用文本編輯器打開它。我們在 makefile 文件中輸入如下內容並保存:

hello.exe: custom1.o custom2.o  		gcc hello.c custom1.o custom2.o -o hello.exe  custom1.o:  		gcc -c custom1.c -o custom1.o  custom2.o:  		gcc -c custom2.c -o custom2.o

最後我們在 make-test 工作目錄下命令行中執行 make 命令,這裡需要注意的是在 Windows 系統下 MinGW 中 make 程式被命名為了 ming32-make.exe 但是功能還是不變的,如果你不習慣的話可以在 MinGW 安裝目錄下的 bin 子目錄下找到這個程式文件並將其重命名為 make.exe 即可。這裡我採用的是原文件名:

可以看到,通過 makemakefile 我們成功的得到了可執行程式文件。下面我們來探討一下 makefile 文件的寫法。

我們可以將 makefile 內容看做是以任務為最小單位驅動的。每一個任務的格式為:

任務名: 依賴的任務1 依賴的任務2 依賴的任務3 ... 依賴的任務n  		完成這個任務需要執行的命令

其中,「完成任務需要執行的命令」 部分需要和首部空開一個以 8 個英文字元寬度為單位的 tab 或者兩個以 4 個英文字元寬度為單位的 tab,不能用視覺上寬度相同的多個空格來代替 tab

我們按照上面的規律來比對一下之前寫的 makefile 文件的內容。首先是 hello.exe 任務,可以看到這個任務依賴兩個任務:custom1.o 任務和 custom2.o 任務。意味著在執行 hello.exe 任務之前要確保 custom1.o 任務和 custom2.o 任務已經執行完畢。同時,完成這個 hello.exe 任務需要執行的命令為 gcc hello.c custom1.o custom2.o -o hello.exe,即編譯 hello.c 源文件和鏈接 custom1.ocustom2.o 兩個已經編譯的二進位文件最終得到一個名為 hello.exe 的可執行文件。 接下來是 custom1.o 任務,這個任務不依賴任何其他任務,通過將 custom1.c 源文件編譯為 custom1.o 文件即可完成。後面的 custom2.o 任務也是類似。

我們在使用 make 工具的時候,如果 make 命令後面不接任何參數,意味著執行當前工作目錄下 makefile 文件定義的第一個任務,當執行某個任務時,make 會自動計算任務依賴關係的順序並按照任務對其他任務的依賴性從小到大依次執行任務。比如上面書寫的 makefile 在執行 hello.exe 任務之前,會先執行 custom1.o 任務再執行 custom2.o 任務,最後執行 hello.exe 任務(這使我想起了拓撲排序)。另一方面,make 將任務名當成這個任務生成的目標文件名,如果發現當前目錄中文件名和當前任務名相同的文件已經存在,則不會再執行這個任務,不管源程式碼文件有沒有更新。我們也可以單獨執行某個任務,在 make 命令後面加入任務名即可,比如在上面我需要單獨執行 custom2.o 任務,在命令行中執行 make custom2.o 即可。

有了 make 工具之後,我們就可以通過編寫 makefile 文件來更加靈活的控制程式的編譯了,比如當程式的某些源碼文件發生更改了之後,我們只需要對這部分源程式生成的可執行文件重新編譯即可,無需重新編譯整個工程的程式程式碼。這對編譯大型的程式是十分便利的。

最後,更正一個網路上存在的錯誤結論:gcc 只能編譯 C語言不能編譯 C++語言,g++ 可以編譯 C++ 語言。這個結論看似正確,因為你在使用 gcc 編譯 C++ 源文件的時候會得到這樣的報錯資訊,而是用 g++ 的時候卻可以成功編譯得到可執行程式並運行:

其實 gccg++ 都可以編譯 C++語言程式,只不過 gcc 默認不能鏈接 C++ 的鏈接庫文件而已。 我們可以通過給 gcc 命令後面通過 -L-l 參數鏈接需要的 C++庫文件即可解決這個問題:

成功!這就說明在上面使用 gcc 編譯 C++ 源文件的報錯是發生在鏈接過程中的,並不是編譯的前三個階段(預處理、編譯、彙編)中的。這裡不做演示了,有興趣的小夥伴可以自行嘗試。

好了,在這篇文章中我們從實踐的角度著重介紹了 C語言程式編譯的流程和 GCC 的相關用法,在最後我們介紹了一下關於 make 工具的用法和 makefile 文件的書寫規則。相信到這裡你對 GCC 相關用法已經有了一個不錯的了解,這也為之後的內容打下了基礎。這篇文章就到這裡了,我們下篇文章再見~

如果覺得本文對你有幫助,請不要吝嗇你的贊,如果覺得文章內容有什麼問題,請多多指點。

謝謝觀看。。。