開篇:預備知識---2

前言

​ 在前一篇文章中我們大致介紹了 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("%d\n", 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 = %d\n", maxValue);
    printf("min value = %d\n", 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 = %d\n", maxValue);
    printf("min value = %d\n", 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 相關用法已經有了一個不錯的瞭解,這也爲之後的內容打下了基礎。這篇文章就到這裏了,我們下篇文章再見~

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

謝謝觀看。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章