CMake
编译器是一个根据源代码生成机器码的程序
单文件编译
main.cpp
#include <iostream>
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
g++ main.cpp -o a.out
这个命令会调用g++编译器,让他读取main.cpp中的源码,并根据C++标准生成相应的机器指令码,输出到a.out中,a.out就是可执行文件
./a.out
这个命令是让操作系统读取刚生成的可执行文件,从而执行其中编译成的机器码,调用对应的函数并且输出hello world
多文件编译和链接
像上面的单个文件编译虽然方便,但是有如下缺点:
- 所有的代码都堆在一起,不利于模块化和理解
- 工程代码变大时,编译时间变的过长,改动一个地方就得全部重新编译。
因此,我们提出了多文件编译的概念,文件之间通过符号声明相互引用
g++ -c hello.cpp -o hello.o
g++ -c main.cpp -o main.o
其中使用-c选项是指定生成临时的对象文件main.o,之后在根据一系列的对象文件得到最终的a.out
g++ hello.o main.o -o a.out
为什么需要构建系统(Makefile)
a.out: hello.o main.o
g++ hello.o main.o -o a.out
hello.o: hello.cpp
g++ -c hello.cpp -o hello.o
main.o: main.cpp
g++ -c main.cpp -o main.o
文件越来越多时,一个一个调用g++编译连接会变得很麻烦。于是发明了make这个程序,只要写出不同文件之间的依赖关系,和生成各个文件的规则
make a.out
上述命令可以直接构建出a.out这个可执行文件。和直接使用g++相比,make指明了依赖关系的好处是:
- 当更新了hello.cpp的时候,只会重新编译hello.o而不需要把main.o也重新编译一遍、
- 能够自动并行的发起对hello.cpp和main.cpp进行编译,加快编译速度
- 用通配符批量生成文件规则,避免对每个cpp和o文件重复写g++命令(%.o : %.cpp)
但是坏处也很明显:
- make需要再unix类系统上是通用的,但是在windows系统上不然
- 需要准确的指明每个项目之间的依赖关系,有头文件时候会特别头疼
- make语法简单,不像shell或者python一样可以做很多判断
- 不同的编译器有不同的flag规则,为g++准备的参数可能对msvc不适用
cmake构建系统
为了解决make的问题,跨平台的cmake应运而生
- 只需要一份CMakeLists.txt,他就能够在调用时生成当前系统所支持的构建系统
- cmake可以自动检测源文件和头文件年之间的依赖关系,导出到makefile里
- cmake具有相对高级的语法,内置的函数能够处理configure,install等常见需求
- cmake可以自动检测当前的编译器,需要添加那些flag
cmake的命令和调用
cmake_minimum_required(VERSION 3.8)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp hello.cpp)
读取当前目录的CMakeLists.txt文件,并且在build文件夹下生成Makefile
cmake -B build
让make读取makefile并且开始构建a.out
cmake -C build
以上命令和上一个等价,但是更跨平台
cmake --build build
执行生成的a.out
./a.out
为什么需要库
- 有时候我们需要多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些共有的功能做成一个库,方便大家使用
- 库中的函数可以被可执行文件调用,也可以被其它库调用
- 库文件又分为静态库文件和动态库文件
- 其中静态库相当于直接把代码插入到生成的可执行文件中,会导致体积变大,但是只需要一个文件即可执行
- 而动态库则只在生成的可执行文件中生成插桩函数,当可执行文件被加载时会读取指定的目录中的dll文件,加载到内存中空闲的位置,并且替换相应的插桩指向的地址为加载后的地址,这个过程称为重定向。这样以后函数被调用就会跳转到动态加载的地址去。
cmake中的静态库和动态库
-
cmake除了add_executable生成可执行文件之外,还可以通过add_library生成库文件
-
add_library的语法与add_executable大致相同,除了他需要指定的动态库还是静态库
add_library(test STATIC source.cpp)
add_library(test SHARED source.cpp)
- 动态库有很多坑,特别是windows下,初学者自己创建库时候建议使用静态库
- 但是他人提供的库大多是动态库,我们之后还要讨论如何使用他人的库
- 创建库以后要在某个可执行文件中使用该库只需要:
target_link_library(a.out PUBLIC test)
cmake中的子模块
- 复杂的工程中,我们需要划分子模块,通常一个库一个目录
- 我们把hellolib的库的文件移动到hellolib文件夹下,里面的CMakeLists.txt定义了hellolib的生成规则
- 要在根目录使用,可以在cmake的add_subdirectory添加子目录,子目录也包含一个CMakeLists,其中定义的库在add_subdirectory中就可以在外面使用
- 子目录的CMakeLists.txt里面的路径都是相对路径,这也是很方便的一点
如果想使用子目录中的头文件,可以使用target_include_directories(a.out PUBLIC hellolib)
这样就可以直接使用头文件名称来索引子目录的头文件了,而不需要输入较多的路径去索引子目录中的头文件。并且我们可以使用<>来索引子目录的头文件。
如果我们不想再外部工程中写该代码,则可以在库中写target_include_directories(a.out PUBLIC .)
其中PUBLIC的作用是表示一个属性在被link的时候要不要向外传播,如果需要则使用PUBLIC,如果不需要则使用PRIVATE
目标的其他命令
除了头文件搜索目录以外,还有一些选项,PUBLIC和PRIVATE对他们是同理的:
target_include_directories(myapp PUBLIC /usr/include/eigen3) # 添加头文件搜索目录
target_link_libraries(myapp PUBLIC hellolib) # 添加要连接的库
target_add_definitions(myapp PUBLIC MY_MACRO=1) # 添加一个宏定义
target_add_definitions(myapp PUBLIC -DMY_MACRO=1) # 添加一个宏定义
target_compile_options(myapp PUBLIC -fopenmp) # 添加编译器命令行选项
target_sources(myapp PUBLIC hello.cpp other.cpp) # 添加要编译的源文件
通过一下命令可以将选项添加到接下来的所有目标中去(不推荐使用):
include_directories(/opt/cuda/include)
link_directories(/opt/cuda)
add_definitions(MY_MACRO=1)
add_compile_options(-fopenmp)
第三方库 - 作为头文件引入
有时候我们不满足于C++标准库的功能,难免会使用一些第三方库。最友好的一类库莫过于纯头文件库了
- nothing/std
- magic_enum
- glm
- rapidjson
- range-v3
- fmt
- spdlog
上面的库只需要将头文件下载下来然后include进项目即可。缺点是函数直接在头文件里面,没有提前编译,从而需要重复编译同样的内容,编译时间长
引用系统中预安装的第三方库
可以通过find_package命令寻找系统中的第三方库
find_package(fmt REQUIRED)
target_link_libraries(myapp PUBLIC fmt::fmt)
这里为什么是fmt::fmt,而不是fmt
现代CMake认为一个包可以提供多个库,又称为组件。比如TBB这个包,就包含tbb、tbbmalloc、tbbmalloc_proxy这三个组件
因此为了避免冲突,每个包都向右一个独立的命名空间,以::分割
你可以指定使用哪几个组件:
find_package(TBB REQUIRED COMPONENTS tbb tbballoc REQUIRED)
tark_link_libraries(myapp PUBLIC TBB::tbb TBB::tbbmalloc)