1. 编译
在软件开发中,编译是将源代码转换为可执行代码或可执行库的过程。根据编译的对象和编译的方式,可以将编译分为以下几种类型
1.1 源代码编译
- 源代码编译(Source code compilation):源代码编译是将高级编程语言(如C、C++、Java等)的源代码转换为可执行代码的过程。
- 这是最常见的编译类型,通过将源代码发送给编译器,它将源代码翻译成机器语言或字节码,生成可执行文件或可执行库。
1.2 预编译
-
预编译(Precompilation):预编译是在编译过程的早期阶段对源代码进行处理的过程。预编译器通常会执行一些预处理操作,如宏展开、条件编译等,以生成经过处理的源代码。这样的预处理结果将用于后续的编译阶段。
-
何时需要预编译:
- 总是使用不经常改动的大型代码体。
- 程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项。在这种情况下,可以将所有包含文件预编译为一个“预编译头”。
1.2.1 条件编译
- gcc test.c
#include <stdio.h>
#define CHINESE //宏定义
int main(int argc, const char *argv[])
{
#ifdef CHINESE
printf("中文\n");//如果定义了CHINESE宏,此代码,参加编译
#else
printf("English\n");//如果没有定义CHINESE宏,此代码,参加编译
#endif
return 0;
}
- gcc -D CHINESE test.c —>在编译程序的时候,可以通过-D参数定义一个宏
#include <stdio.h>
//注意此处没有定义宏
int main(int argc, const char *argv[])
{
#ifdef CHINESE
printf("中文\n");//如果定义了CHINESE宏,此代码,参加编译
#else
printf("English\n");//如果没有定义CHINESE宏,此代码,参加编译
#endif
return 0;
}
- #if 0:不参与编译;#if 1 :参与编译
#if 1 —> 真
#endif#if 0 —>假
#endif
#include <stdio.h>
int main(int argc, const char *argv[])
{
#if 0
printf("中文\n");
#endif
printf("English\n");
return 0;
}
1.3 交叉编译
-
交叉编译(Cross-compilation):交叉编译是在一台计算机系统上使用一个编译器来生成在另一个不同的计算机系统上执行的目标代码。
-
这种编译方式常见于嵌入式开发,其中源代码在开发主机上编译成为适用于目标嵌入式设备的目标代码。
-
无论是自行安装PC上的编译器,还是下载其他平台(Android或者 iOS)的交叉工具编译链,它们都会提供以下几个工具:CC、AS、 AR、LD、NM、GDB。那么,这几个工具到底是做什么用的呢?下面就来逐一解释一下。
- CC:编译器,对C源文件进行编译处理,生成汇编文件。
- AS:将汇编文件生成目标文件(汇编文件使用的是指令助记符, AS将它翻译成机器码)。
- AR:打包器,用于库操作,可以通过该工具从一个库中删除或者增加目标代码模块。
- LD:链接器,为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者是可执行文件。
- GDB:调试工具,可以对运行过程中的程序进行代码调试工作。
- STRIP:以最终生成的可执行文件或者库文件作为输入,然后消除掉其中的源码。
- NM:查看静态库文件中的符号表。
- Objdump:查看静态库或者动态库的方法签名。
1.4 增量编译
- 增量编译(Incremental compilation):增量编译是一种优化技术,用于在修改代码后只重新编译被修改的部分,以加快编译过程。
- 编译器会跟踪源代码的变化,并仅编译发生更改的文件、模块或函数。
例子
1.5 即时编译
- 即时编译(Just-in-time compilation):即时编译是在程序运行时进行的一种编译方式。
- 它将程序的某些部分(通常是字节码或中间代码)动态地编译成机器代码,以提高执行效率。
- 即时编译常见于虚拟机(如Java虚拟机)和动态语言解释器(如Python解释器)等环境。
2. gcc
gcc 是一个跨平台的编译器
Windows中有很多编译器(keil eclipse … 内部使用的是gcc)
gcc 是编译程序的重要选项参数
2.1 编译指定生成的文件名称
gcc hello.c —> 默认生成产物:a.out 可执行文件
想要修改产物的名称为自己设定
gcc -o hello hello.c ---->hello 的生成依赖于hello.c
gcc hello.c -o hello ---->将编译后的产物改名为hello
2.2 gcc编译步骤
2.2.1 预处理
预处理阶段所做的事
- 将头文件的内容完全复制过来 <stdio.h> 944
- 将代码中所有的注释信息删除掉
- 将宏定义无脑替换
gcc -E -o hello.i hello.c —> -E想要停留在预处理阶段,-o 改名,hello.i 为产物
2.2.2 编译
编译阶段所做的事:将我们的c程序编译成汇编程序
编译过程停留在编译的阶段
gcc -S -o hello.s hello.c —> -S:想停留在编辑阶段,-o:改名,hello.s:产物
汇编程序就是以.s结尾的
2.2.3 汇编
汇编阶段所做的是:将汇编程序编译成二进制的机器码 0 1
将编译过程汇编的阶段
gcc -c -o hello.o hello.c —> -c:想要停留在汇编阶段,-o:改名,hello.o:产物
简写: gcc -c hello.c —> 默认生成的产物为hello.o
2.2.4 链接
链接阶段所做的事:将多个.c文件和库文件链接在一起,生成可以执行的程序—>a.out
gcc hello.c
gcc -o hello hello.c
- 一步编译,生成产物
gcc main.c add.c del.c //默认生成产物:a.out
gcc -o hh main.c add.c del.c
gcc main.c add.c del.c -o hh
- 逐个编译为.o,再生成产物
gcc -c main.c //默认生成main.o
gcc -c add.c //默认生成add.o
gcc -c del.c //默认生成del.o
//将所有的.o文件和库文件链接在一起,生成可执行程序
gcc -o hh main.o add.o del.o
2.3 程序编译保持类型
2.3.1 error:编译有error,不会生成可执行程序
2.3.2 warning:编译有warning,也可以生成可执行程序
-wall —> 显示所以警告,建议加-wall
gcc hello.c -wall
-w —> 忽略所有警告,建议不加此选项
gcc hello.c -w
2.4 gcc编译优化
gcc可以对我们写的不好的代码进行优化
#include <stdio.h>
int main(int argc,const char *argv[])
{
double i;
double result;
double temp; //double 双精度浮点数据计算的时候就耗时
for(i = 0;i <2000.0 * 2000.0 / 20 + 2; i = i + (5 - 4 ) / 1 )
{//循环结束条件2000.0*2000.0式子计算每次都耗时
temp = i / 1997 ; //此语句没有意义,=
result = i; //此语句应该放在循环外面
}
printf("result is %1f\n",result);
return 0;
}
time命令:测试程序运行的时间
time命令,可以使测试程序的运行的时间
time ./a.out —> 测试时间
//gcc 可以对上面的代码进行优化
gcc -o1 -o hello1 hello.c //-o1 一级优化
gcc -o2 -o hello2 hello.c //-o2 二级优化,linux程序员通常用o2
gcc -o3 -o hello3 hello.c //-o3 三级优化
time ./hello1
time ./hello2
time ./hello3
-o3 数越大优化级别越高,一般linux程序员用-o2优化
o1:目的都是在不影响编译速度的前提下,尽量采用一些优化算法降低代码大小和可执行代码的运行速度,开启如下的优化选项
o2:该优化选项会牺牲部分编译速度,除了执行-o1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,所以提高目标代码的运行速度
3. gdb
gdb(GNU Debuger): linux下的一个调试工具,用来调试程序,是GNU调试程序的一个主要工具
3.1 一般调试方法(gdb)
- 设置断电,程序运行会在断电处停下来,我们可以看变量的值
- 查看堆栈
3.2 GDB调试流程
第一步
- 生成一个可调试的文件
gcc -g -o hello hello.c
—> -g 参数:生成一个可调试的程序
第二步
- 加载调试程序
gdb hello
list // l 默认显示代码的前10行
break 9 //在第九行设置断点
info b //查看断点信息,查看设置了几个断点
clear 9 //清除第九行的断点
run //r 运行程序
print i //查看变量i,此时的值
next //n 向下执行一行代码
continue c //继续运行,运行到下一个断点
quit q //退出
backtrac //bt,查看堆栈,栈顶的函数为出错函数
4. Makefile
Makefile是一个文件,里面主要写了源代码的编译规则,然后通过make命令,来执行
- Makefile 是一个文件,通常名称为Makefile
- Makefile 通过make命令来执行
4.1 Makefile作用
gcc main.c add.c dek.c …
关于Makefile(主要是用于c文件的编译,如果文件比较多,用gcc是不现实的)
- 解决多个c文件的编译
- 提高编译的效率
4.2 编写Makefile
hello: hello.c #hello是目标也叫规则,hello的生成依赖与hello.c
gcc -o hello hello.c #执行hello规则的过程
clean: #clean规则
rm hello
rebuild: clean hello #rebuld规则,一个规则依赖与另外的规则,那么另外的规则会被执行
# : 后面都是依赖的
4.3 执行Makefile
make 在当前目录中找Makefile
make 空格 后面跟规则的名字 make hello
make 会默认执行第一个规则
1. 规则
make 是一个命令,专门执行Makefile,make命令会查找当前目录的makefile
2.Makefile 以 规则为单位
hello: hello.c #hello是目标也叫规则,hello的生成依赖与hello.c
gcc -o hello hello.c #执行hello规则的过程
// hello 是目标,通常是生成的文件名,hello也称为规则
// hello.c 是依赖文件
// hello的生成依赖于hello.c
4.4 多个文件编译
5. cmake
CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile 或者 project 文件,CMake 的配置文件取名为 CMakeLists.txt。也就是在 CMakeLists.txt 这个文件中写 cmake 代码。 一句话:cmake 就是将多个 cpp、hpp 文件组合构建为一个大工程的语言。
CMake是一个跨平台的开源构建工具,用于自动化项目的构建过程,CMake的优势在于它可以在不同的操作系统和开发环境中生成一致的构建过程,使得跨平台开发变得更加简单和可靠
5.1 编写CmakeLists.txt
在 linux 下使用 CMake 生成 Makefile 并编译的流程如下:
- 编写 CMake 配置文件 CMakeLists.txt 。
- 在 CMakeLists.txt 文件所在目录创建一个 build 文件夹,然后进入目录。(这一步可以省略,但是生成的中间文件不易清理)
- 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile(ccmake 和 cmake 的区别在于前者提供了一个交互式的界面)。其中, PATH 是 CMakeLists.txt 所在的目录。
- 使用 make 命令进行编译。
5.1.1 编译单目录下的单个源文件
文件目录:
//main.cpp
#include <iostream>
int main(int argc, char *argv[])
{
std::cout << "Hello CMake!" << std::endl;
return 0;
}
//CMakeLists.txt
#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
#声明一个cmake工程,工程名
#设置项目的名称,同时会自动生成 PROJECT_NAME 变量,使用 ${PROJECT_NAME} 即可访问到 hello_cmake。
project(hello_cmake)
# 生成可执行文件
#第一个参数是可执行文件名,第二个参数是要编译的源文件列表。这里将名为 main.cpp 的源文件编译成一个名称为 hello_cmake 的可执行文件
add_executable(hello_cmake main.cpp)
接着我们可以开始构建项目,构建的方法有以下两种:
- 内部构建:直接在源文件目录构建项目,会导致临时文件和源代码放在一起,不好清理。
- 外部构建:创建一个可以位于文件系统上任何位置的构建文件夹。 所有临时构建和目标文件都位于此目录中,以保持源代码树的整洁。
这里以外部构建为例,此时我们需要新建一个构建文件夹 build,并在该目录下运行 cmake 命令进行构建:
mkdir build
文件目录:
cd build
#在build目录下构建
cmake ..
#使用cmake生成的makefile编译得到可执行文件
make
#执行可执行文件
./hello_cmake
5.1.2 编译单目录下的多个源文件
文件目录:
此时 CMakeLists.txt 如下:
#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
#声明一个cmake工程,工程名
project(hello_cmake)
# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 指定生成目标
add_executable(${PROJECT_NAME} ${DIR_SRCS})
在本示例中,为了避免一个个将所有源文件输入,使用了aux_source_directory 命令。
- aux_source_directory :第一个参数是目录的路径,第二个参数是变量名。当我们使用这个命令时,就会将指定目录下的所有源文件保存到指定的变量名中。
如果不想使用这种方法,而是向一条条枚举每个变量,可以使用 set 来手动将源文件保存到变量名中:
#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
#声明一个cmake工程,工程名
project(hello_cmake)
set(DIR_SRCS test.cpp main.cpp)
# 指定生成目标
add_executable(${PROJECT_NAME} ${DIR_SRCS})
编译方法同上
5.1.3 多个目录下的多个源文件
文件目录如下:
本示例在多个目录下有着多个源文件。在这种情况下,我们需要在每个目录中都编写一个 CMakeLists.txt。这里为了方便,我们可以将test 里的文件编译为一个静态库再由 main 函数调用。
首先看看 test 目录下的 CMakeLists.txt,这里主要做的事是将当前目录下的文件编译为一个静态库:
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)
# 指定生成 MathFunctions 链接库
add_library (TestFunctions ${DIR_LIB_SRCS})
- add_library:用于从某些源文件创建一个库,默认生成在构建文件夹。第一个参数为库名(不需要 lib 前缀,会自动添加),第二个参数用于指定 SHARED(动态库),STATIC(静态库)(如果不写,则通过全局的BUILD_SHARED_LIBS 的 FALSE 或 TRUE 来指定)。第三个参数即为源文件列表。
接着看看根目录的 CMakeLists.txt:
#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
#声明一个cmake工程,工程名
project(hello_cmake)
# 查找目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)
# 添加 test 子目录
add_subdirectory(test)
# 指定生成目标
add_executable(${PROJECT_NAME} ${DIR_SRCS})
# 添加链接库
target_link_libraries(hello_cmake TestFunctions)
- add_subdirectory:用于表示该项目包含一个子目录,此时会去处理子目录下的 CMakeLists.txt 与源文件。
- target_link_libraries:该命令用于指明可执行文件 Demo 需要链接 MathFunctions 库。第一个参数为可执行文件名,第二个参数为访问权限(PUBLIC、PRIVATE、INTERFACE,默认为 PUBLIC),第三个参数为库名(这两个参数可以为多个)。
如果 test 目录下没有CMakeLists.txt,我们可以这样写
目录结构:
CMakeLists.txt文件内容,如下:
#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
#声明一个cmake工程,工程名
project(hello_cmake)
#用来向工程添加多个指定头文件的搜索路径,路径之间用空格分隔
include_directories (test)
# 查找目录下的所有源文件
# 并将名称保存到 SRC_LIST 变量
aux_source_directory(test SRC_LIST)
# 指定生成目标
add_executable(main main.cpp ${SRC_LIST})
5.1.4 项目级的组织结构
正规一点来说,一般会把源文件放到src目录下,把头文件放入到include文件下,生成的对象文件放入到build目录下,最终输出的可执行程序文件会放到bin目录下,这样整个结构更加清晰。
目录结构:
test.h
#ifndef _TEST_FUNC1_H_
#define _TEST_FUNC1_H_
void func(int data);
#endif
test.cpp
#include <iostream>
#include "test.h"
void func(int data)
{
std::cout << "data is " << data << std::endl;
}
main.cpp
#include <iostream>
#include "test.h"
int main(int argc, char *argv[])
{
func(100);
return 0;
}
CMakeLists.txt
#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.22.1)
#声明一个cmake工程,工程名
project(skia_test)
#EXECUTABLE_OUTPUT_PATH 是一个 CMake 变量,用于指定可执行文件的输出路径。
#通过设置这个变量,可以将生成的可执行文件定向到指定的目录中,而不是默认的构建目录。
#${PROJECT_SOURCE_DIR} 是指 CMakeLists.txt 文件所在的目录
#用来设置可执行文件的输出路径
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 查找当前目录下的所有源文件
# 并将名称保存到 SRC_LIST 变量
aux_source_directory (src SRC_LIST)
# 添加引用的头文件
include_directories (include)
# 生成可执行文件
add_executable (main ${SRC_LIST})
编译
cd build
cmake ..
make
cd ../bin
./main