C++项目编译基本原理和入门实践
C++编译基本过程
上图展示了一个源代码文件通过编译器生成一个可执行文件的大致过程。基本包含源文件的预处理,编译,汇编和最终链接目标文件生成可执行文件。本篇文章就是逐步介绍从源代码生成最终可执行文件的过程。在知道每一步的原理之后,介绍在编写大型项目时,如果通过cmake工具来帮助我们构建项目最终的可执行文件。
graph TD
A[源文件test.c] -->|gcc -E test.c -o test.i| B(预处理后文件test.i)
B -->|gcc -S test.c -o test.s| C[汇编文件test.s全部为汇编指令]
C --> |as test.s -o test.o| D[目标文件test.o全部为二进制机器指令]
D --> |ld test.o -o test.out| F[可执行文件test.out]
预处理 预处理的本质是进行内容的插入和替换,主要包含以下几项工作。
- 将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换
- 处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些
- 处理#include,将#include指向的文件插入到该行处
- 删除所有注释
- 添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行
- 保留#pragma编译器指令,因为编译器需要使用它们。
编译
例如以下这个函数。
int task() {
int a = 0;
while(a > 2){
//do something
}
return 1;
}
未经优化的汇编代码为
task():
pushrbp
movrbp, rsp
mov[rbp-4], 0
nop
.L2:
cmp[rbp-4], 2
jg.L2
moveax, 1
poprbp
ret
而经过优化之后汇编代码变为:
task():
moveax, 1
ret
可以看到,编译器将没必要的汇编指令进行了优化,生成了全新的代码。这从侧面也提醒了我们,在大型工业项目中,要谨慎使用编译器的优化功能,尽量在开发环境中应用与生产环境相同的编译器和相同的优化等级进行测试,防止因为编译器优化导致的bug。
汇编 【C++项目编译基本原理和入门实践】汇编的主要功能就是将汇编指令根据CPU所支持的指令集体系结构(Instruction-Set Architecture) 生成二进制指令。
目前的CPU指令集主要可以归类为RISC(精简指令集计算机)和CISC(复杂指令集计算机)。其中前者的代表为ARM架构,后者的代表为Intel架构。不过目前这两种指令集也在互相吸收对方的特点,两种指令集的边界也在逐渐模糊。
链接 链接的本质是将源代码中使用的外部变量,外部类,外部函数的可执行代码融合到最终生成的目标文件中。
在Linux中目标文件可以分为3类:
- 可重定位目标文件:简单来说就是静态库,或者是单独的目标文件。在例子中我们将test.cpp编译得到的test.o就是单独的目标文件,而静态库就是一些目标文件的集合。
- 可执行目标文件。
- 共享目标文件:简单来说就是动态库。
头文件和库之间的关系
在工作中我们时常面临着在代码中插入别人已经写好的库,例如OpenCV,rapidjson等等。一般来说我们都会包含这些库的头文件并链接这些库。对于链接这一任务来说,头文件是调用函数的代码和提供函数的库之间沟通的桥梁。
库也是通过源代码编译生成的。既然在源代码cpp文件中就可以完成函数的声明和定义,我们为什么还要写头文件呢?
头文件对于库源代码来说,起到了声明的作用,库源代码按照头文件的声明,定义相应的函数。当库源代码编译为二进制库时,头文件就成为了这个库的说明书,当有其他程序需要调用库里面定义的函数,就可以按照库提供的头文件来编写程序,而在编译程序时,只需要将这个库的头文件加入到包含路径中,并链接该库即可。
以一个例子理解g++/gcc命令 以https://gitee.com/hlinbit/cpp...为例,我们的目标是生成main.cpp对应的可执行文件。
.
├── build_lib.sh
├── build.sh
├── include
│├── add.h
│└── sort.h
├── library
│├── libadd.a
│└── libsort.a
├── main.cpp
└── src
├── add.cpp
└── sort.cpp
其中最外层的两个脚本分别用来编译依赖库(build_lib.sh)和编译可执行文件(build.sh)。接下来我们通过编译脚本中的命令来理解C++程序的编译过程。
其中build_lib.sh脚本中包含两条命令分别是将src/目录下的sort.cpp和add.cpp编译为依赖库libsort.a和libadd.a。
g++ -c -I ./include -o ./library/libadd.a ./src/add.cpp
g++ -c -I ./include -o ./library/libsort.a ./src/sort.cpp
# -c 代表编译目标为静态库,不加编译器会检测main函数,没有main函数会报错。
# -I 代表include路径,将头文件所在的文件夹加入到包含路径中。
# -o 指定结果文件路径和名称。
# 最后的文件名为源代码文件。
运行完build_lib.sh,main.cpp的依赖库被输出到library中。接下来就可以运行build.sh完成可执行文件的生成。
g++ -o main -I ./include -L ./library -lsort -ladd main.cpp
# -L 后面的参数指明了链接的库所在的文件夹。
# -l 后面紧跟着需要链接的库名,动态库和静态库皆可。需要注意的是-ladd指的是链接libadd.a,而不是add.a。
# 最后跟的是可执行文件对应的源代码文件。
推荐阅读
- opencv|opencv C++模板匹配的简单实现
- C语言学习|第十一届蓝桥杯省赛 大学B组 C/C++ 第一场
- 17|17 关山松 第二课作业#公众号项目# D20
- RxJava|RxJava 在Android项目中的使用(一)
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- 靠QQ月入上万灰色暴利偏门的项目
- spring|spring boot项目启动websocket
- c++基础概念笔记
- vuex|vuex 基础结构
- 区块链开发平台(以太坊)