前置知识/背景
cpp文件的编译与链接
假设我们有个代码文件main.cpp,单个文件可以通过以下指令进行编译和运行:
# 编译
g++ main.cpp -o a.out
# 运行
./a.out
单文件编译虽然很方便,但也有以下缺点:
- 所有的代码都堆在一起,不利于模块化管理
- 单文件代码量很大时,编译时间很长,十分麻烦
因此,提出多文件编译的概念,文件之间通过 符号声明 相互引用:
# 分为m1模块和main主模块
# -c 编译生成临时的对象文件
g++ -c m1.cpp -o m1.o
g++ -c main.cpp -o main.o
# 对他们进行链接,得到最终可执行文件
g++ m1.o main.o -o a.out
这样做解决了单文件编译的缺点,但随着模块的增长,敲的指令也变多了,很麻烦、
Makefile
Makefile便诞生了,只需一个文件和一个指令即可完成上边的操作:
Makefile:
a.out: m1.o main.o
g++ m1.o main.o -o a.out
m1.o: m1.cpp
g++ -c m1.cpp -o m1.o
main.o: main.cpp
g++ -c main.cpp -o main.o
指令:
make
# 加快速度
make -j
但是也有一些不足:
make指令在Unix上通用,但Windows不是- 要准确指明每个项目间的依赖关系,规模大的时候(如
hit-oslab)会很头疼。 make语法简单,不能做很多判断。- 不同编译器的指令不一样。
库(library)
有时候会碰到多个可执行文件共用某些功能的情况,可以把这些共用的功能做成一个 库 ,方便共享。
库中的函数可以被可执行文件调用,也能被其它库文件调用。库文件分为 **静态库文件 **和 动态库文件(dll, so) 两种。静态库相当于直接把代码插到可执行文件中,会导致体积变大;动态库会在可执行文件相应位置生成“插桩”函数,执行时会先读取dll文件,将对应功能加载到内存空闲位置,执行到此处会跳转到加载后的地址(插桩在读取dll后会被替换)。
Windows中,先会在可执行文件所在目录查找dll,其次是环境变量%PATH%;Linux中,先在elf可执行文件的RPATH中查找,其次是/usrt/lib等。
CMake入门
为了解决make的以上问题,跨平台的CMake便诞生了。上边Makefile对应的CMakeLists.txt如下:
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_executable(a.out main.cpp m1.cpp)
这是CMakeLists.txt文件的基本格式,其中,第4行的意思是,a.out为输出的文件,后面的全是输入的文件。
CMake的命令行调用
读取当前目录的CMakeLists.txt,并在build文件夹下生成build/Makefile:
cmake -B build
让make读取build/Makefile,并开始构建a.out:
make -C build
# 也可以选择更跨平台的做法
make --build build
最后便可以执行build/a.out了。
CMake中的静态库与动态库
可以使用add_library生成库文件:
# 生成静态库 libtest.a/lib
add_library(test STATIC s1.cpp s2.cpp)
# 生成动态库 libtest.so/dll
add_library(test SHARED s1.cpp s2.cpp)
初学建议用静态库,动态库有坑,但他人提供的库大多数是作为动态库的。
创建库后,要在某个 可执行文件 中使用该库,需要:
# 为 myexec 链接刚刚制作的库 libtest.a
target_link_libraries(myexec PUBLIC test)
CMake中的子模块
在复杂的工程中,需要划分子模块,通常一个库一个目录。例如以下文件结构:
module-1
|- CMakeLists.txt
|- m1.cpp
|- m1.h
CMakeLists.txt
main.cpp
模块的CMakeLists.txt如下:
add_library(m1lib STATIC m1.cpp)
要想在根目录中使用子模块,CMakeLists.txt 可以这样写:
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
# 使用子模块m1lib
add_subdirectory(m1lib)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC m1lib)
此外,还得修改main.cpp中m1.h的路径。如果要避免修改代码,可以通过target_include_directories 指定a.out的头文件搜索目录:
# line 9
target_include_directories(a.out PUBLIC m1lib)
这样子指定的路径也被视为系统路径(可以用尖括号包裹起来)。
但是,以上做法违反了 不要重复自己(DRY) 原则,即如果还有其他可执行文件要使用m1模块,还得添加这一行。因此,可以直接将这句话添加到m1模块的CMakeLists.txt中:
# line 2
target_include_directories(m1lib PUBLIC .)
PUBLIC和PRIVATE决定一个属性要不要在被链接的时候传播。
目标(target)的一些其他选项
target_include_directories(myapp PUBLIC /usr/...):添加头文件搜索目录
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(myapps hello.cpp other.cpp):添加要编译的源文件
引入第三方库
第三方库的引入主要分为三类:纯头文件,作为子模块,系统预安装。
-
纯头文件库:操作起来最简单,只需把他们的
include目录或头文件下载下来,然后include_directories(xxxx/include)即可,例如:- 图形学相关的
stb_image库,只需添加一个宏定义STB_IMAGE_IMPLEMENTATION并引入一个头文件即可。 magic_enum库(实现枚举类的自反射,枚举名以字符串形式输出)glm库:模仿GLSL语法的数学矢量/矩阵库Tencent/rapidjson:JSON解析库,无STL内容range-v3:C++20的range受到他启发而成,但没他好用fmt:提供std::format的替代品,需要宏定义FMT_HEADER_ONLY.
缺点:函数直接实现在头文件里,没有提前编译,编译时间长。
- 图形学相关的
-
作为子模块:可以作为CMake子模块引入,即通过
add_subdirectoryfmtrange-v3glmabseil-cpp:补充标准库没有的常用功能backward-cpp:实现C++的堆栈回溯,便于调试googletest:谷歌单元测试框架benchmark:谷歌性能评估框架glfw:OpenGL窗口和上下文管理libigl:各种图形学算法大合集
缺点:可能会出现
菱形依赖,即某个模块被两个模块使用,发生重复定义。 -
系统预安装:可以通过
find_package命令寻找系统中的包/库:find_package(fmt REQUIRED)
target_link_libraries(myexec PUBLIC fmt::fmt)现代CMake认为一个包可以提供多个库,又称为组件,为了避免冲突,每个包都享有一个独立的名字空间。例如TBB这个包中有tbb,tbbmalloc,tbbmalloc_proxy这三个组件,可以指定要用那几个组件:
find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED)
target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)常用
package列表如下:fmt::fmtrange-v3::range-v3TBB::tbbOpenVDB::openvdbBoost::iostreamsEigen3::EigenOpenMP::OpenMP_CXX
不同的包之间常常有依赖关系,包管理器的作者会给
find_package编写脚本(xxxConfig.cmake),可以自动且正确处理依赖项。包的引用格式和文档可以参考FindBLAS.
包管理器
Linux可以用包管理器来安装第三方库(如apt,pacman)等,Windows没有自带的包管理器,可以用微软跨平台的vcpkg包管理器。
使用方法:下载vcpkg的源码,放到项目根目录,如图:
然后执行以下命令:
cd vcpkg
./bootstrap-vcpkg.bat
./vcpkg intergrate install
./vcpkg install fmt:x64-windows # 安装fmt库(默认最新,无法指定版本)
cd ..
cmake -B build -DCMAKE_TOOL_CHAIN_FILE="%CD%/vcpkg/scripts/buildsystems/vcpkg.cmake"

