引子
最近在工作中经常要使用到cmake,所有这里对零零散散的学习进行一个简单的总结。
首先,我们来看一个简单的命令。
g++ main.cpp -o a.out
该命令会调用编译器程序g++,来生成机器指令码,输出到a.out这个文件中(称为可执行文件)。
之后执行该命令,操作系统会读取刚刚生成的可执行文件,从而执行其中编译成的机器码。
但是,单文件编译虽然方便,但也有如下缺点:
- 所有的代码都堆在一起,不利于模块化和理解;
- 工程变大时,编译时间变得很长,改动一个地方就得全部重新编译。
因此,我们提出多文件编译的概念,文件之间通过符号声明相互引用。
g++ -c hello.cpp -o hello.og++ -c main.cpp -o main.o- 其中使用 -c 选项指定生成临时的对象文件 main.o,之后再根据一系列对象文件进行链接,得到最终的a.out
g++ hello.o main.o -o a.out
通过这种方式,当一个文件改变后,我们只需要将该文件进行重新编译即可。
为什么需要构建系统(Makefile)
- 文件越来越多,一个个调用g++编译链接会变得很麻烦;
- 于是,发明了 make 这个程序,你只需要写出不同文件之间的依赖关系,和生成这个文件的规则;
>make a.out,构建出a.out这个可执行文件;
优点:
- 当更新了 hello.cpp 时只会重新编译 hello.o,而不需要把 main.o 也重新编译一遍;
- 能够自动并行地发起对 hello.cpp 和 main.cpp 的编译,加快编译速度 (make -j);
- 用通配符批量生成构建规则,避免针对每个.cpp和.o重复写g++命令(%.o: %.cpp)。
缺点:
- make 在 Unix 类系统上是通用的,但在 Windows 则不然;
- 需要准确地指明每个项目之间的依赖关系,有头文件时特别头疼;
- make 的语法非常简单,不像 shell 或 python 可以做很多判断等;
- 不同的编译器有不同的 flag 规则,为 g++ 准备的参数可能对 MSCV 不使用。
构建系统的构建系统(CMake)
为了解决 make 的以上问题,跨平台的 CMake 应运而生
- 只需要写一份 CMakeLists.txt,他就能在调用时生成当前系统所支持的构建系统;
- CMake 可以自动检测源文件和头文件之间的依赖关系,导出到 Makefile里;
- CMake 具有相对高级的语法,内置的函数能够处理 configure, install 等常见需求;
- CMake 可以自动检测当前的编译器,需要添加哪些flag。比如 OpenMP,只需要在CMakeLists.txt 中指明 target_link_libraries(a.out OpenMP::OpenMP_CXX) 即可。
CMake的命令行调用
- 读取当前目录的 CMakeLists.txt,并在 build 文件夹下生成 build/Makefile;
- cmake -B build;
- 让 make 读取 build/Makefile,并开始构建 a.out(类比与 Windows 中的 exe 文件);
- cmake --build build
- 生成a.out:build/a.out
CMake库相关
为什么需要库(library)
- 有时候我们会有多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些共用的功能做成一个库,方便大家一起共享;
- 库中的函数可以被可执行文件调用,也可以被其他库文件调用;
- 库文件又分为静态库文件和动态库文件;
- 其中静态库相当于直接把代码插入到生成的可执行文件中,会导致体积变大,但是只需要一个文件即可运行;
- 而动态库则只在生成的可执行文件中生成”插桩“函数,当可执行文件被加载时会读取指令目录中的.dll文件,加载到内存中空闲的位置,并且替换成相应的“插桩”指向的地址为加载后的地址,这个过程称为重定向。这样以后函数被调用就会跳转到动态加载的地址去。
- Windows:可执行文件同目录,其次是环境变量%PATH%
- Linux:ELF格式可执行文件的RPATH,其次是/usr/lib等
CMake中的静态库和动态库
- CMake 除了 add_executable 可以生成可执行文件外,还可以通过 add_library 生成库文件;
- add_library 的语法与 add_executable 大致相同,除了他需要指定是动态库还是静态库;
- add_library(test STATIC source1.cpp source2.cpp) 生成静态库 libtest.a;
- add_library(test SHARED source1.cpp source2.cpp) 生成动态库 libtest.so
- 创建库以后,要在某个可执行文件中使用该库,只需要:
- target_link_libraries(myexec PUBLIC test)
CMake中的子模块
- 复杂的工程中,我们需要划分子模块,通常一个库一个目录,比如:
- 要在根目录使用他,可以用 CMake 的 add_subdirectory 添加子目录,子目录也包含一个 CMakeLists.txt,其中定义的库在 add_subdirectory 之后就可以在外面使用;
- 子目录的 CMakeLists.txt 里路径名(比如 hello.cpp)都是相对路径,这也是很方便地一点。
因为 hello.h 被移到了 hellolib 子文件夹里,因此 main.cpp 里也要改成:
- 如果要避免修改代码,我们可以通过 target_include_directories 指定 a.out 的头文件搜索目录:(其中第一个 hellolib 是库名,第二个是目录):
- 这样甚至可以用 <hello.h> 来引用这个头文件了,因为通过 target_include_directories 指定的路径会被视为与系统路径等价:
- 但是这样如果另一个 b.out 也需要用 hellolib 这个库,难道也得再指定一遍搜索路径吗?
- 不需要,其实我们只需要定义 hellolib 的头文件搜索路径,引用他的可执行文件 CMake 会自动添加这个路径:
- 这里用了 . 表示当前路径,因为子目录里的路径是相对路径,类似还有 .. 表示上一层目录;
- 此外,如果不希望让引用 hellolib 的可执行文件自动添加这个路径,把PUBLIC改成PRIVATE即可。这就是他们的用途:决定一个属性要不要在被 link 的时候传播。
目标的一些其他选项
-
除了头文件搜索目录外,还有这些选项,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_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) # 添加编译器命令行选项
第三方库
作为纯头文件引入
这里举几个例子:
- nothings/stb - 涵盖图像、声音、字体等,只需单头文件
- Tencent/rapidjson - 单纯的 JSON 库
- fmtlib/fmt - 格式化库,提供 std::format 的替代品
- gabime/spdlog - 能适配控制台,安卓等后端的日志库
只需要把他们的 include 目录或头文件下载下来,然后 include_directories(spdlog/include)即可。
缺点:
函数直接实现在头文件里,没有提前编译,从而需要重复编译同样内容,编译时间长。
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp)
target_include_directoires(a.out PUBLIC glm/include)
作为子模块引入
-
第二友好的方式则是作为 CMake 子模块引入,也就是通过 add_subdirectory;
-
方法就是把那个项目的源码放到你工程的根目录:
-
举几个例子:
- fmtlib/fmt - 格式化库
- google/googletest - 谷歌单元测试框架
- google/benchmark - 谷歌性能评估框架
- glfw/glfw - OpenGL 窗口和上下文管理
- abseil/abseil-cpp 旨在补充标准库没有的常用功能
- bombela/backward-cpp 实现了 C++ 的堆栈回溯便于调试
- libigl/libigl - 各种图形学算法大合集
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_subdirectory(fmt)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC fmt)
引用系统中预安装的第三方库
-
可以通过 find_package 命令寻找系统中的包/库
-
find_package(fmt REQUIRED) -
target_link_libraries(myexec PUBLIC fmt::fmt) -
为什么是 fmt::fmt 而不是简单的 fmt?
-
现代 CMake 认为一个包(package)可以提供多个库,又称为组件(components),比如TBB这个包,就包含了tbb,tbbmalloc,tbbmalloc_proxy这三个组件;
-
可以指定要用哪几个组件:
find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED) target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp)
find_package(fmt REQUIRED)
target_link_libraries(a.out PUBLIC fmt::fmt)
第三方库 - 常用 package 列表
- fmt::fmt
- spdlog::spdlog
- TBB::tbb
- OpenMP::OpenMP_CXX
不同的包之间常常有着依赖关系,而包管理器的作者为 find_package 编写的脚本 (例如/usr/lib/cmake/TBB/TBBConfig.cmake)能够自动查找所有依赖,并利用刚刚提到的 PUBLIC PRIVATE 正确处理依赖项,比如如果你引用了 OpenVDB::openvdb 那么 TBB::tbb 也会被自动引用。
其他的引用格式和文档参考: