【編譯、鏈接與構建詳解】Makefile 與 CMakeLists 的作用
- 前言
- 源代碼(.c、.cpp)
- 編譯
- 編譯的本質
- 編輯的結果
- 編譯器(GCC、G++、NVCC 等)
- 目標文件(`.o`)
- 什么是 `.o` 目標文件
- 為什么單個 `.o` 目標文件不能直接執行?
- 鏈接
- 鏈接的本質
- 如果需要鏈接的 `.o` 文件很多且雜亂怎么辦?
- 庫文件(.a、.so)
- 靜態庫(`.a`)
- 動態庫(`.so`)
- 構建
- 構建的步驟
- 自動化構建
- 構建工具與構建規則(Make、Makefile)
- 構建配置工具與構建配置文件(CMake、CMakeLists)
前言
在大型項目中,通常會使用 C 或 C++ 語言進行開發,而編譯、鏈接、構建等概念,以及相關工具如 GCC、G++、NVCC、CMake、Make、Makefile 等,是每個開發者都無法繞過的重要內容。這些概念雖然至關重要,但往往容易混淆。因此,本文將簡明扼要地介紹這些概念,幫助讀者更深入地理解編譯、鏈接與構建過程。
源代碼(.c、.cpp)
我們編寫的代碼通常以 .c
或 .cpp
文件的形式存在,這些源代碼文件是人類可讀的,但計算機無法直接理解和執行。
計算機只能理解二進制的機器語言,因此,需要通過編譯將源代碼轉換為機器可以識別的格式,這就涉及到編譯的相關知識。
編譯
編譯的本質
程序員編寫的 .c
和 .cpp
源代碼是人類可讀的,但計算機無法直接理解。為了讓計算機執行這些代碼,需要將其轉換為計算機能夠識別的二進制語言,這個過程就是編譯的本質。
編輯的結果
編譯的結果是目標文件(.o
文件),它包含了經過翻譯但尚未完整鏈接的二進制代碼(機器可以理解的語言)。理解目標文件的作用,有助于更深入地掌握編譯過程。
編譯器(GCC、G++、NVCC 等)
編譯器是將源代碼翻譯為計算機能夠理解的目標文件的“翻譯官”。它負責將人類編寫的 .c
、.cpp
、.cu
等源代碼轉化為機器可執行的二進制代碼。常見的編譯器包括:
- GCC(GNU Compiler Collection):主要用于編譯 C 語言的源代碼(
.c
文件)。 - G++:是 GCC 的 C++ 編譯器,用于編譯 C++ 語言的源代碼(
.cpp
文件)。 - NVCC:NVIDIA CUDA 編譯器,用于編譯并行計算的 CUDA 程序(
.cu
文件)。
這些編譯器各自對應不同類型的源代碼文件,執行代碼翻譯的任務。
目標文件(.o
)
什么是 .o
目標文件
.c
和 .cpp
源代碼經過編譯后,會生成 .o
目標文件。目標文件是計算機可以理解的二進制代碼,意味著源代碼已經被翻譯成機器語言,程序的執行又向前邁進了一步。
為什么單個 .o
目標文件不能直接執行?
目標文件(.o
)本身并不能直接運行,因為它只是編譯后的中間產物,尚未構成完整的可執行程序。通常,一個程序由多個 .c
或 .cpp
文件組成,而 main
函數往往位于其中的一個文件中,負責調用其他模塊的函數。
可以將程序比作一輛汽車:main
函數相當于車架,而各個 .o
文件代表輪子、方向盤、控制臺等組件。單獨的 .o
文件只是一個零件,只有經過鏈接,將所有模塊正確拼接在一起,才能形成最終可運行的程序。
要將這些獨立的目標文件整合成一個可執行程序,就涉及到鏈接的過程。
鏈接
鏈接的本質
當所有 .c
或 .cpp
代碼經過編譯后,都會生成 .o
目標文件。這些 .o
文件雖然已經被翻譯成機器可以識別的語言,但它們彼此獨立,尚無法直接運行。
鏈接就是組裝:
可以將 .o
文件比作汽車的零部件:單獨的目標文件就像輪子、方向盤、發動機等組件,只有經過鏈接,將這些部件正確組裝起來,才能形成一個完整的可執行程序(拼成一個可以跑的汽車)。鏈接的本質,就是將多個 .o
目標文件整合在一起,最終拼接成可以運行的可執行文件。
通常,在所有被鏈接的 .o
目標文件中,只有一個包含 main
主函數,它相當于汽車的車架,而其他 .o
文件則封裝了各種功能模塊(如發動機、剎車系統、座椅等)。鏈接的過程,就是將這個帶有 main
入口的*“車架”與其他“零部件”*拼接在一起,使其成為一個完整可運行的程序。
如果需要鏈接的 .o
文件很多且雜亂怎么辦?
在大型項目中,編譯過程中會生成大量 .o
目標文件。如果直接鏈接所有 .o
文件,不僅會導致項目結構混亂,還會增加管理和分發的難度。
為了解決這個問題,通常會將多個 .o
文件打包成庫文件,即 .so
(動態/共享庫) 和 .a
(靜態庫)。這些庫文件可以幫助我們更高效地組織、管理和復用代碼,使項目結構更加清晰,鏈接過程也更加簡潔。
庫文件(.a、.so)
為了更方便地管理大量的 .o
目標文件,引入了庫文件的概念。庫文件可以看作是多個 .o
文件的集合,用于提高代碼的組織性和復用性。然而,庫文件分為兩種類型:
.a
(靜態庫).so
(動態庫/共享庫)
二者雖然都是 .o
文件的集合,但在使用方式上存在明顯區別。
靜態庫(.a
)
靜態庫(.a
)本質上是多個 .o
目標文件的打包集合,在編譯時會被直接鏈接到可執行文件中。這種方式可以提高代碼復用性,并減少每次編譯時重復編寫相同代碼的工作量。
然而,靜態庫是固定的,如果庫中的 .o
文件對應的源代碼發生修改,就需要重新編譯修改的部分并更新靜態庫文件,然后再重新鏈接生成新的可執行文件。這意味著每次庫文件更新后,所有依賴該庫的程序都必須重新編譯和鏈接。
動態庫(.so
)
動態庫(.so
,共享庫)與靜態庫類似,也是多個 .o
目標文件的集合,但它不會在編譯時直接嵌入可執行文件,而是在程序運行時被加載。這種方式減少了可執行文件的體積,并允許多個程序共享同一個庫,從而提高資源利用率。
此外,動態庫是靈活的,如果庫中的 .o
文件對應的源代碼發生修改,只需重新編譯動態庫文件,無需重新編譯和鏈接所有依賴它的程序。程序在運行時會自動加載最新版本的動態庫,因此更新更加便捷。
構建
構建是將源代碼轉化為可執行文件的完整過程,通常包括以下幾個主要步驟:
構建的步驟
-
清理
在進行新一輪構建之前,需要先清理掉之前構建的產物,比如刪除舊的目標文件.o
、可執行文件和庫文件等。這一步確保構建環境干凈,避免舊文件影響新一輪構建。 -
管理依賴
項目可能依賴外部庫或資源,這時候需要下載、安裝并管理這些依賴,確保它們的版本正確且可用。 -
編譯
這一階段將源代碼文件(.c
或.cpp
)編譯成目標文件(.o
)。編譯過程將人類可讀的代碼轉化為機器可以理解的中間產物。 -
鏈接
鏈接過程將多個目標文件(.o
文件)和庫文件(.a
或.so
文件)整合、拼接成一個完整的可執行文件,最終生成可以運行的程序。 -
其他
在某些構建過程中,還可能涉及其他步驟,如單元測試、部署、打包等,這些步驟根據項目需求可能會有所不同。
自動化構建
如上所述,構建過程包含多個環節,涉及到源代碼的編譯、目標文件的生成、依賴的管理等操作。為了實現高效且自動化的構建,通常會使用構建工具來控制這一過程。
最常見的構建工具是 Make,它通過 Makefile 文件定義的構建規則和依賴關系,從而自動化管理和執行構建步驟。接下來,我們將介紹 Make 工具及其 Makefile 構建規則的相關內容。
構建工具與構建規則(Make、Makefile)
Make 是一種常用的構建工具,它根據 Makefile 中定義的 構建規則 來自動化構建過程。Makefile 中指定了構建目標、依賴關系、編譯器選項、可執行文件名等具體的構建命令。
然而,Make 的一些局限性也較為明顯:它的語法較為底層,可讀性較差,并且對多平臺兼容性較弱(例如,它通常只適用于 UNIX 系統,而在其他平臺的構建可能會遇到困難)。
為了克服這些問題,CMake 應運而生。CMake 是一個更高級的構建配置工具,它通過平臺無關的配置文件生成適用于不同平臺的構建文件(例如 Makefile 或 Visual Studio 工程文件),從而解決了多平臺兼容性和可讀性差的問題。
構建配置工具與構建配置文件(CMake、CMakeLists)
正如前文所述,CMake 是一個高級的構建配置工具,它通過讀取 CMakeLists.txt 文件中定義的構建配置來生成適應不同平臺的構建規則。CMake 會根據 CMakeLists.txt 中的內容,自動生成對應的構建文件(例如 Makefile 或 Visual Studio 工程文件),然后調用相應的構建工具(如 make
)來執行構建過程。
相較于 Make,CMake 提供了更高層次的抽象,具有更強的實用性和更好的跨平臺兼容性。因此,CMake 在大型項目中得到了更廣泛的應用,尤其是在需要支持多平臺構建時,CMake 和 CMakeLists.txt 文件成為了主流的選擇。