【编译、链接与构建详解】Makefile 与 CMakeLists 的作用
- 前言
- 源代码(.c、.cpp)
- 编译
- 目标文件(`.o`)
-
- [什么是 `.o` 目标文件](#什么是
.o
目标文件) - [为什么单个 `.o` 目标文件不能直接执行?](#为什么单个
.o
目标文件不能直接执行?)
- [什么是 `.o` 目标文件](#什么是
- 链接
-
- 链接的本质
- [如果需要链接的 `.o` 文件很多且杂乱怎么办?](#如果需要链接的
.o
文件很多且杂乱怎么办?)
- 库文件(.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 文件成为了主流的选择。