CMake基础

CMake基础

一、什么是编译器

编译器,是一个根据源代码生成机器码的程序

image-20230317150938743
g++ main.cpp -o a.out

该命令会调用编译器程序g++,让他读取main.cpp中的字符串(称为源码),并根据C++标准生成相应的机器指令码,输出到a.out这个文件中,(称为可执行文件)

./a.out

之后执行该命令,操作系统会读取刚刚生成的可执行文件,从而执行其中编译成机器码,调用系统提供的printf函数,并在终端显示出Hello, world

image-20230317150949955

二、多文件编译与链接

单文件编译虽然方便,但也有如下缺点:

1.所有的代码都堆在一起,不利于模块化和理解

2.工程变大时,编译时间变得很长,改动一个地方就得全部重新编译

因此,我们提出多文件编译的概念,文件之间通过符号声明相互引用

image-20230317151256956

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

文件越来越多时,一个个调用g++编译链接会变得很麻烦

于是,发明了 make 这个程序,你只需写出不同文件之间的依赖关系,和生成各文件的规则

image-20230317151542545
make a.out

敲下这个命令,就可以构建出 a.out 这个可执行文件了

和直接用一个脚本写出完整的构建过程相比,make 指明依赖关系的好处:

1.当更新了hello.cpp时只会重新编译hello.o,而不需要把main.o也重新编译一遍

2.能够自动并行地发起对hello.cpp和main.cpp的编译,加快编译速度(make -j)

3.可以用通配符批量生成构建规则,避免针对每个.cpp和.o重复写 g++ 命令(%.o: %.cpp)

但坏处也很明显:

1.make 在 Unix 类系统上是通用的,但在 Windows 则不行

2.需要准确地指明每个项目之间的依赖关系,有头文件时特别头疼

3.make 的语法非常简单,不像 shell 或 python 可以做很多判断等

4.不同的编译器有不同的 flag 规则,为 g++ 准备的参数可能对 MSVC 不适用

四、构建系统的构建系统CMake

为了解决 make 的以上问题,跨平台的 CMake 应运而生:

  1. 只需要写一份 CMakeLists.txt,他就能够在调用时生成当前系统所支持的构建系统
  2. CMake 可以自动检测源文件和头文件之间的依赖关系,导出到 Makefile 里
  3. CMake 具有相对高级的语法,内置的函数能够处理 configure,install 等常见需求
  4. 不同的编译器有不同的 flag 规则,为 g++ 准备的参数可能对 MSVC 不适用
  5. CMake 可以自动检测当前的编译器,需要添加哪些 flag。比如 OpenMP,只需要在 CMakeLists.txt 中指明 target_link_libraries(a.out OpenMP::OpenMP_CXX) 即可

image-20230317152111419

五、CMake的使用

1.CMake的命令行调用

读取当前目录的 CMakeLists.txt,并在 build 文件夹下生成 build/Makefile:

cmake -B build

让 make 读取 build/Makefile,并开始构建 a.out:

make -C build

以下命令和上一个等价,但更跨平台:

cmake --build build

执行生成的 a.out:

build/a.out

image-20230317152521460

2.为什么需要库

有时候我们会有多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些共用的功能做成一个,方便大家一起共享

库中的函数可以被可执行文件调用,也可以被其他库文件调用

  • 库文件又分为静态库文件动态库文件
  1. 其中静态库相当于直接把代码插入到生成的可执行文件中,会导致体积变大,同样的对库文件进行编译,但生成的可执行文件,不依赖库文件即可运行
  2. 而动态库则只在生成的可执行文件中生成“插桩”函数(汇编语言中的jump,指定跳转的位置),当可执行文件被加载时会读取指定目录中的.dll文件,加载到内存中空闲的位置,并且替换相应的“插桩”指向的地址为加载后的地址,这个过程称为重定向,这样以后函数被调用就会跳转到动态加载的地址去

image-20230317153348367

链接库文件查找位置顺序:

Windows:可执行文件同目录,其次是环境变量%PATH%

Linux:ELF格式可执行文件的RPATH,其次是/usr/lib等

3.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

image-20230317153952172

注意:

  1. 动态库有很多坑,特别是 Windows 环境下,初学者自己创建库时,建议使用静态库
  2. 但是他人提供的库,大多是作为动态库的,我们之后会讨论如何使用他人的库
  3. windows中静态库是以.lib 为后缀的文件,动态库是以.dll 为后缀的文件
  4. linux中静态库是以.a 为后缀的文件,动态库是以.so为后缀的文件

创建库以后,要在某个可执行文件中使用该库,只需要:

target_link_libraries(myexec PUBLIC test) # 为 myexec 链接刚刚制作的库 libtest.a

其中 PUBLIC 的含义稍后会说明(CMake 中有很多这样的大写修饰符)

4.CMake中的子模块

复杂的工程中,我们需要划分子模块,通常一个库一个目录,比如:

这里我们把 hellolib 库的东西移到 hellolib 文件夹下了,里面的 CMakeLists.txt 定义了 hellolib 的生成规则

要在根目录使用他,可以用 CMake 的 add_subdirectory 添加子目录,子目录也包含一个 CMakeLists.txt,其中定义的库在 add_subdirectory 之后就可以在外面使用

子目录的 CMakeLists.txt 里路径名(比如 hello.cpp)都是相对路径,这也是很方便的一点

image-20230317154833854

5.子模块头文件的处理

因为 hello.h 被移到了 hellolib 子文件夹里,因此 main.cpp 里也要改成:

image-20230317155309289

如果要避免修改代码,我们可以通过 target_include_directories 指定a.out 的头文件搜索目录:(其中第一个 hellolib 是库名,第二个是目录)

这样甚至可以用 <hello.h> 来引用这个头文件了,因为通过 target_include_directories 指定的路径会被视为与系统路径等价

image-20230317155653646

6.多文件引用库的头文件处理

但是这样如果另一个 b.out 也需要用 hellolib 这个库,难道也得再指定一遍搜索路径吗?

image-20230317160402196
  1. 其实我们只需要定义 hellolib 的头文件搜索路径,引用他的可执行文件 CMake 会自动添加这个路径
  2. 这里用了 . 表示当前路径,因为子目录里的路径是相对路径,类似还有 … 表示上一层目录
  3. 此外,如果不希望让引用 hellolib 的可执行文件自动添加这个路径,把 PUBLIC 改成 PRIVATE 即可,这就是他们的用途:决定一个属性要不要在被 link 的时候传播

7.目标的一些其他选项

除了头文件搜索目录以外,还有这些选项,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)     # 与 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)       # 添加编译器命令行选项

注:没有target_前缀的选项会让项目所有文件都添加选项的属性

tips:add_definitions(NOMINMAX) 可以在C++定义变量为min/max不受到std中的min/max影响

8.第三方库的引入

  • 纯头文件引入:

有时候我们不满足于 C++ 标准库的功能,难免会用到一些第三方库。

最友好的一类库莫过于纯头文件库了,这里是一些好用的 header-only 库:

1.nothings/stb - 大名鼎鼎的 stb_image 系列,涵盖图像,声音,字体等,只需单头文件!
2.Neargye/magic_enum - 枚举类型的反射,如枚举转字符串等(实现方式很巧妙)
3.g-truc/glm - 模仿 GLSL 语法的数学矢量/矩阵库(附带一些常用函数,随机数生成等)
4.Tencent/rapidjson - 单纯的 JSON 库,甚至没依赖 STL(可定制性高,工程美学经典)
5.ericniebler/range-v3 - C++20 ranges 库就是受到他启发(完全是头文件组成)
6.fmtlib/fmt - 格式化库,提供 std::format 的替代品(需要 -DFMT_HEADER_ONLY)
7.gabime/spdlog - 能适配控制台,安卓等多后端的日志库(和 fmt 冲突!)

只需要把他们的 include 目录或头文件下载下来,然后 include_directories(spdlog/include) 即可

缺点:函数直接实现在头文件里,没有提前编译,从而需要重复编译同样内容,编译时间长

  • 作为子模块引入:

第二友好的方式则是作为 CMake 子模块引入,也就是通过 add_subdirectory

方法就是把那个项目(以fmt为例)的源码放到你工程的根目录:

image-20230317161534556

这些库能够很好地支持作为子模块引入:

1.fmtlib/fmt - 格式化库,提供 std::format 的替代品
2.gabime/spdlog - 能适配控制台,安卓等多后端的日志库
3.ericniebler/range-v3 - C++20 ranges 库就是受到他启发
4.g-truc/glm - 模仿 GLSL 语法的数学矢量/矩阵库
5.abseil/abseil-cpp - 旨在补充标准库没有的常用功能
6.bombela/backward-cpp - 实现了 C++ 的堆栈回溯便于调试
7.google/googletest - 谷歌单元测试框架
8.google/benchmark - 谷歌性能评估框架
9.glfw/glfw - OpenGL 窗口和上下文管理
10.libigl/libigl - 各种图形学算法大合集
  • 引用系统中预安装的第三方库:

可以通过 find_package 命令寻找系统中的包/库:

find_package(fmt REQUIRED)
target_link_libraries(myexec PUBLIC fmt::fmt)
image-20230317161945845
  • 为什么是 fmt::fmt 而不是简单的 fmt?

现代 CMake 认为一个 (package) 可以提供多个,又称组件 (components),比如 TBB 这个包,就包含了 tbb, tbbmalloc, tbbmalloc_proxy 这三个组件

因此为避免冲突,每个包都享有一个独立的名字空间,以 :: 的分割(和 C++ 还挺像的)

你可以指定要用哪几个组件:

find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED)
target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)

第三方库 - 常用 package 列表:

1.fmt::fmt
2.spdlog::spdlog
3.range-v3::range-v3
4.TBB::tbb
5.OpenVDB::openvdb
6.Boost::iostreams
7.Eigen3::Eigen
8.OpenMP::OpenMP_CXX

不同的包之间常常有着依赖关系,而包管理器的作者为 find_package 编写的脚本(例如/usr/lib/cmake/TBB/TBBConfig.cmake)能够自动查找所有依赖,并利用刚刚提到的 PUBLIC PRIVATE 正确处理依赖项,比如如果你引用了 OpenVDB::openvdb 那么 TBB::tbb 也会被自动引用

其他包的引用格式和文档参考:https://cmake.org/cmake/help/latest/module/FindBLAS.html

安装第三方库 - 包管理器:

Linux 可以用系统自带的包管理器(如 apt)安装 C++ 包

Windows 则没有自带的包管理器。因此可以用跨平台的 vcpkg:https://github.com/microsoft/vcpkg

/usr/lib/cmake/TBB/TBBConfig.cmake)能够自动查找所有依赖,并利用刚刚提到的 PUBLIC PRIVATE 正确处理依赖项,比如如果你引用了 OpenVDB::openvdb 那么 TBB::tbb 也会被自动引用

其他包的引用格式和文档参考:https://cmake.org/cmake/help/latest/module/FindBLAS.html

注:内容来源-https://space.bilibili.com/263032155

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coca1cole

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值