文章

09.C和C++编译

09.C和C++编译

C/C++ 编译

程序的生命周期

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404260824187.png)

编译过程与编译器

编译过程

编译过程是指编写的源代码通过编译器进行编译,最后生成 cpu 所能识别的二进制形式存在的源代码的过程。而编译器则是指能够使源代码编译生成二进制形式的工具,根据平台不同,工具也不同,如 window 是 XXX.exe 可执行程序,unix 系统侧不定后缀名,系统根据文件的头部信息来判断。

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404260824752.png)

编译器和链接器介绍

.cpp 文件编译成 *.obj 目标文件:一个 cpp 文件编译成一个 obj 文件。

常用的 C++ 编译器:

GCC (GNU Compiler Collection)

GCC 是 “GNU Compiler Collection”(GNU 编译器集合)的缩写,它包含了处理多种编程语言的编译器,如 C(gcc)、C++(g++)、Objective-C、Fortran、Ada、Go,甚至更多。

gcc 和 g++ 是这个编译器集合中的两个具体命令,分别用于编译 C 和 C++ 程序。它们的主要区别在于默认的语言和链接库:

  1. 默认语言:
    • gcc 默认编译 C 程序代码。
    • g++ 默认编译 C++ 程序代码。
  2. 默认链接的库:
    • 当使用 gcc 来编译时,它不会默认链接 C++ 标准库,如果你的代码使用了 C++ 库,你必须显式指定。
    • g++ 会链接 C++ 标准库,可以正确处理 C++ 程序的各种依赖,比如对模板的支持和异常处理等。
  3. 文件扩展名处理:
    • 尽管 gcc 可以编译 .cpp 文件或者 g++ 可以编译 .c 文件(通过显式指定),默认情况下它们会根据文件的扩展名推断使用哪种语言。
    • gcc 会把 .c 文件视为 C 程序代码,而 g++ 会把 .cpp 文件视为 C++ 程序代码。
  4. 编译产物的差异:
    • 使用 gcc 编译 C++ 代码时,你可能需要手动添加链接器标志,如 -lstdc++,以链接 C++ 标准库。
    • 使用 g++ 编译则不需要上述附加步骤,因为它自动处理了 C++ 相关的标准库链接。

在内部,gcc 和 g++ 实际上都是调用相同的编译器后端,只是它们的前端(解析源代码和生成中间代码的部分)针对不同的语言设置了不同的默认行为。

GCC/G++ 编译器执行过程

gcc/g++ 编译器能把一个源文件生成一个执行文件,这是因为该编译器是集成了各种程序(预处理器、汇编器等),这个过程中的工作如下:

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidianobsidian202404260833941.png)

示例:test.c 

1
2
3
4
5
#include <stdio.h>
int main() {
    printf("hello world\n");
    return 0;
}

在控制台执行命令 (-o:为指定输出文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ls
test.c
$ gcc -E test.c -o test.i
$ ls
test.c    test.i
$ gcc -S test.i -o test.s
$ ls
test.c    test.i    test.s
$ gcc -c test.s -o test.o
$ ls
test.c    test.i    test.o    test.s
$ gcc test.o -o test
$ ls
test    test.c    test.i    test.o    test.s
$ ./test 
hello world
$
GCC/G++ 重要参数
  • 通过 gcc --help 命令查看更多。
  • -E 只激活预处理,这个不生成文件, 你需要把它重定向到一个输出文件里面。
  • -S 只激活预处理和编译,就是指把文件编译成为汇编代码。
  • -c 只激活预处理,编译,和汇编,也就是他只把程序做成 obj 文件
  • -o 制定目标名称, 默认的时候, gcc 编译出来的文件是 a.out。
  • -fPIC 生成与位置无关代码。
  • -shared 生成动态库,使用例子:

    gcc -fPIC -shared test.c -o libTest.so

  • -include file 包含某个代码,简单来说,就是便以某个文件,需要另一个文件的时候,就可以用它设定,功能就相当于在代码中使用 #include<filename>。例子:

    gcc test.c -include xxx.h

  • -Dmacro 相当于 C 语言中的 #define macro
  • -Dmacro=defn 相当于 C 语言中的 #define macro=defn
  • -Umacro 相当于 C 语言中的 #undef macro
  • -undef 取消对任何非标准宏的定义
  • -Idir 在你是用 #include "file" 的时候, gcc/g++ 会先在当前目录查找你所制定的头文件, 如果没有找到, 他回到默认的头文件目录找, 如果使用 -I 制定了目录,他会先在你所制定的目录查找, 然后再按常规的顺序去找。 对于 #include, gcc/g++ 会到 -I 制定的目录查找, 查找不到, 然后将到系统的默认的头文件目录查找 。
  • -I- 就是取消前一个参数的功能, 所以一般在 -Idir 之后使用。
  • -idirafter dir 在 -I 的目录里面查找失败, 讲到这个目录里面查找。
  • -iprefix prefix 、-iwithprefix dir 一般一起使用, 当 -I 的目录查找失败, 会到 prefix+dir 下查找
  • -M 生成文件关联的信息。包含目标文件所依赖的所有源代码你可以用 gcc -M test.c 来测试一下,很简单。
  • -MM 和上面的那个一样,但是它将忽略由 #include 造成的依赖关系。   
  • -MD 和 -M 相同,但是输出将导入到.d 的文件里面   
  • -MMD 和 -MM 相同,但是输出将导入到 .d 的文件里面。
  • -Wa,option 此选项传递 option 给汇编程序; 如果 option 中间有逗号, 就将 option 分成多个选项, 然 后传递给会汇编程序。
  • -Wl.option 此选项传递 option 给连接程序; 如果 option 中间有逗号, 就将 option 分成多个选项, 然 后传递给会连接程序。
  • -llibrary 指编译的时候使用的库,library 是指动态库或静态库的名称。如:liblibrary.a 或 liblibrary.so,系统会自动加上 lib 前缀和.a(.so) 后缀。
  • -Ldir 指编译的时候,搜索库的路径。比如你自己的库,可以用它指定目录,不然编译器将只在标准库的目录找。这个 dir 就是目录的名称。
  • -O0 、-O1 、-O2 、-O3 编译器的优化选项的 4 个级别,-O0 表示没有优化, -O1 为默认值,-O3 优化级别最高。
  • -g 只是编译器,在编译的时候,产生调试信息。   
  • -gstabs 此选项以 stabs 格式声称调试信息, 但是不包括 gdb 调试信息。   
  • -gstabs+ 此选项以 stabs 格式声称调试信息, 并且包含仅供 gdb 使用的额外调试信息。   
  • -ggdb 此选项将尽可能的生成 gdb 的可以使用的调试信息。
  • -static 此选项将禁止使用动态库,所以,编译出来的东西,一般都很大,也不需要什么动态连接库,就可以运行。
  • -share 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库。

Clang

  • Clang 是一个 C、C++、Objective-C 和 Objective-C++ 编程语言的编译器前端,使用 LLVM 作为它的后端。
  • 它提供了一套完整、工业级的工具链,对开发者非常友好,尤其在诊断(错误和警告)方面。

Microsoft Visual C++

  • Visual C++ 是 Microsoft Visual Studio 的一部分,Visual Studio 属于 IDE,它包含 C++ 编译器以及许多其他的开发工具。
  • Visual C++ 编译器在 Windows 上广泛应用于商业和个人项目。

其他编译器

  • MinGW (Minimalist GNU for Windows):MinGW 提供了一套完整的开源编程工具集,包括 GCC,是 Windows 系统上的 GCC 实现。
  • Cygwin:Cygwin 是一个旨在将类 Unix 的环境带到 Windows 的项目,它包括 GCC 作为其集合中的 C/C++ 编译器。
  • Intel C++ Compiler:Intel C++ Compiler 提供优化的性能,特别适用于 Intel 处理器。虽然它不是完全免费,但对于一些特定使用场景,如学术研究,Intel 提供免费版本。
  • Code:: Blocks with GCCCode::Blocks 是一个开源的 C++ IDE,它带有 MinGW 或可以自行配置的 GCC。

链接器

在 C++ 开发中,连接器(也称链接器或 Linker)是用来将编译后的目标文件(Object files),即扩展名通常为 .o 或 .obj 的文件,以及库(Libraries)合并为单个可执行文件的工具。以下是一些常用的 C++ 链接器:

  • GNU Linker (ld): GNU ld 是 GNU 项目的标准链接器,通常和 GCC 编译器一起使用。
  • LLD: 这是 LLVM 项目的链接器,旨在成为跨平台链接器,提供更快的链接速度。
  • Microsoft Linker (link. Exe): 这是 Microsoft Visual Studio 提供的链接器,用于 Windows 平台的 C++ 程序。
  • Gold Linker: 一个专门为 ELF(Executable and Linkable Format)文件格式设计的链接器,目标是提供比标准 GNU Linker 更快的性能。
  • Mold: 一种现代的并发链接器,开发者声称它是比现有链接器(如 ld 和 gold)更快的链接器。

链接器通常与编译器捆绑使用,不需要单独安装。例如,当你在使用 GCC 编译 C++ 程序时,GNU linker 会自动被调用来链接编译后的目标文件。同样,如果你用的是 Visual Studio,则其内置的 link.exe 会被用来链接你的程序。

编译错误以 C 开头,链接错误以 LNK 开头

静态库和动态库

库基本概念

什么是库?

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a.lib)和动态库(.so.dll)。

由于 Windows 和 Linux 的平台不同(主要是编译器、汇编器和连接器的不同),因此二者库的二进制是不兼容的。

库分类

所谓静态、动态是指链接。

动态库与静态库统称为函数库,根据系统不一样,后缀名标识也不一定,如图:

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404260842148.png)

一个程序编译成可执行程序的步骤:

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidianobsidian202404122347255.png)

静态库

静态库: 之所以成为【静态库】,是因为在链接阶段,会将汇编生成的目标文件 .o 与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。 试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟 .o 文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

优点::静态库节省时间,不需要再进行动态链接,需要调用的代码直接就在代码内部。

Linux 下使用 ar 工具、Windows 下 vs 使用 lib.exe ,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。一般创建静态库的步骤如图所示:

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidianobsidian202404122352358.png)

动态库

为什么还需要动态库?

  • 空间浪费是静态库的一个问题。
![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404122355234.png)
  • 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库 liba.lib 更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新

![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404122358429.png)

动态库特点总结:

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单。
  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。

Window 与 Linux 执行文件格式不同,在创建动态库的时候有一些差异。

  • 在 Windows 系统下的执行文件格式是 PE 格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有 _declspec(dllexport)关键字
  • Linux 下 gcc 编译的执行文件默认是 ELF 格式,不需要初始化入口,亦不需要函数做特别的声明, 编写比较方便。

与创建静态库不同的是,不需要打包工具(arlib.exe),直接使用编译器即可创建动态库。

动态库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。带来好处的同时,也会有问题!如经典的 DLL Hell 问题,关于如何规避动态库管理问题,可以自行查找相关资料。

Linux 下库的种类?

linux 下的库有两种:静态库共享库(动态库);二者的不同点在于代码被载入的时刻不同。

Linux 静态库

静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。

静态用 .a 为后缀, 例如:libhello.a

Linux 动态库

共享库 (动态库) 的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。 动态通常用 .so 为后缀, 例如:libhello.so

共享库 (动态库) 的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。

为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,例如: libhello.so.1.0,由于程序连接默认以 .so 为文件后缀名。所以为了使用这些库,通常使用建立符号连接的方式。

1
ln -s libhello.so.1 libhello.so

C++ 静态库和动态库引入

Visual Studio 2022 引入

静态库 glfw 引入

  • 下载拿到 静态库文件.lib头文件.h [DownloadGLFW](https://www.glfw.org/download.html)
![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404130025248.png)
  • 将下载好的静态库文件 glfw.libglfw.h 按照想要的路径放置到解决方案里,例如 【解决方案】\Dependencies\glfw
![image.png500](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404130026787.png)
  • VS2022 开发工具里打开应用程序项目 属性(右键打开),找到 C++ → 常规 → 附加包含目录,添加头文件所在基础路径 $(SolutionDir)\Dependencies\glfw\;。注意第一是顶部要选择全部配置和全部平台,如果只选择了其中一种配置和平台可能会报错。其次是不要删除原本附加包含目录里已有的路径,只使用分号 ; 隔开,除非你确定那些路径已经不需要;
![image.png1000](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404130030211.png)
  • 接着上一步,重新在 属性里找链接器 → 常规 → 附加库目录,添加静态库文件所在基础路径:$(SolutionDir)\Dependencies\glfw\;
![image.png900](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404130033520.png)
  • 之后找到 链接器》输入》附加依赖项,添加静态库剩余路径 glfw3.lib,基础路径和剩余路径合起来才是静态库的完整路径 $(SolutionDir)\Dependencies\glfw\glfw3.lib;
![image.png900](https://raw.githubusercontent.com/hacket/ObsidianOSS/master/obsidian202404130034346.png)
  • 在需要使用该库的项目文件代码里引入头文件,路径根据第 2 步基础路径所决定,如 #include <glfw3.h> 指向的是 $(SolutionDir)\Dependencies\glfw\glfw3.h
  • 现在可以开始调用静态库里的方法了,代码示例:
1
2
3
4
5
6
#include <iostream>
#include <glfw3.h>
int main() {
  int a = glfwInit();
  std::cout << a << std::endl;
}
  • 注意下载的是 X86 还是 X64, 就运行什么平台,成功后控制台输出:1

动态库 glfw 引入

基本同静态库的引入:

  1. 将下载好的动态库 glfw.dll、glfw3dll.lib(动态库专用的链接文件)和 glfw.h 放到依赖下 $(SolutionDir)\Dependencies\glfw\
  2. 在 VS 开发工具里打开应用程序项目属性,找到 C++》常规》附加包含目录,添加头文件所在基础路径 $(SolutionDir)\Dependencies\glfw\;
  3. 接着上一步,重新在属性里找链接器》常规》附加库目录,添加动态库文件所在基础路径 $(SolutionDir)\Dependencies\glfw\;
  4. 之后找到链接器》输入》附加依赖项,添加静态库剩余路径 glfw3dll.lib,基础路径和剩余路径合起来才是静态库的完整路径 $(SolutionDir)\Dependencies\glfw\glfw3dll.lib
  5. 将动态库 glfw.dll 放到应用程序所生成的 exe 文件的旁边(否则点击 exe 直接运行时会报错找不到 glfw.dll 库;
  6. 在需要使用该库的项目文件代码里引入头文件,路径根据第 2 步基础路径所决定,如 #include <glfw3.h> 指向的是 $(SolutionDir)\Dependencies\glfw\glfw3.h
  7. 现在可以开始调用动态库里的方法了,代码示例:
1
2
3
4
5
6
#include <iostream>
#include <glfw3.h>
int main() {
  int a = glfwInit();
  std::cout << a << std::endl;
}

C++ 中创建与使用库 (VisualStudio 多项目)

CmakeList 引入

本文由作者按照 CC BY 4.0 进行授权