学C++从Cmake学起
文章目录
1.构建系统make
2.构建系统CMake
- 为了解决make的以上问题,跨平台的Cmake应运而生
- 只需要写一份CmakeLists.txt,它就能在调用时生成当前系统所支持的构建系统
- CMake可以自动检测源文件和头文件之间的依赖关系,导出到Makefile里面
- CMake具有相对高级的语法,内置的函数能够处理configure,install等常见需求
- CMake可以自动检测当前的编译器,需要添加哪些flag,必须OpenMP,只需要在CMakeLists,txt中指明target_link_libraries(a.out OpenMP::OpenMP_CXX)即可
3.CMake的常见命令行调用
生成构建文件Makefile
表示输出makefile文件的目录
表示试用clang++作为编译器
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_STANDARD=17
构建可执行文件,编译
cd build;make
或者make -C build,优点是:跨平台
或者cmake --build build
4.CMakeLists.txt常见语法
add_executable(输出的可执行文件 输入的多个源文件)
add_executable(a.out main.cpp hello.cpp)
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) # 为 myexec 链接刚刚制作的库 libtest.a
5.为什么需要库library?
有时候我们会有多个可执行文件,他们之间用到的某些功能是相同的,我们想把这些共用的功能做成一个库,方便大家一起共享.
- 静态库和动态库都是多个.o文件的打包
- 库中的函数可以被可执行文件调用,也可以被其他库文件调用。
- 库文件又分为静态库文件和动态库文件。
- 其中静态库相当于直接把代码插入到生成的可执行文件中,会导致体积变大,但是只需要一个文件即可运行.
- 而动态库则只在生成的可执行文件中生成“插桩”函数,当可执行文件被加载时会读取指定目录中的.dll文件,加载到内存中空闲的位置,并且替换相应的“ 插桩”指向的地址为加载后的地址,这个过程称为重定向。
这样以后函数被调用就会跳转到动态加载的地址去。

- eg:以course/01/05/hello.cpp为例:objdump -D ./build/a.out |less
插装函数puts如下,可跳转到libc.so库中,plt就是插装函数
Windows:
- 可执行文件同目录,其次是环境变量%PATH%
Linux:
- ELF格式可执行文件的RPATH,其次是/usr/lib等
6.CMake 中的静态库与动态库
(1)add_library
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
- 动态库有很多坑,特别是 Windows 环境下,初学者自己创建库时,建议使用静态库。
- 创建库以后,要在某个可执行文件中使用该库,只需要:
target_link_libraries(myexec PUBLIC test) # 为 myexec 链接刚刚制作的库 libtest.a
其中 PUBLIC 的含义稍后会说明(CMake 中有很多这样的大写修饰符)
- eg:course/01/06
动态库必须在可执行文件的同目录或者系统目录下,可执行程序才能运行;


静态库则无所谓,静态库是将实现hello()直接放在a.out中,它不需要动态查找,但是可执行文件会变大
objdump -D ./build/a.out |less

(2)动态库的符号隐藏方式
什么是符号隐藏?
- 在同一个文件中,如果有一些函数我们并不想要让外部访问,我们通常会添加 static 修饰符,把它设置为内部链接属性。
- 但是通常库不太可能是单文件组成,这些文件中有些是做接口给外部使用,有些则单纯的只是库的内部实现。对于外部使用者来说,内部实现的这些符号没有实际的作用,理论上我们完全可以像对待文件内部符号一样把它们统统隐藏掉。
符号隐藏的作用
安全,去掉不必要的符号,可以增加逆向破解的难度。
压缩空间,符号实际上是放在 dll 中的,去掉这些符号可以缩减 dll 的大小
性能,符号隐藏掉意味着它不会参与到动态链接过程,编译器可以有更大的优化空间,可能会产生更好的性能。
做法:
set_target_properties(mylib
PROPERTIES CXX_VISIBILITY_PRESET hidden
)
or
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
然后将公开的接口设置__attribute__ ((visibility (“default”)))即可
- mylib.h
#pragma once
void __attribute__((visibility("default"))) say_hello();
通常会定义宏来协助处理这一部分内容
- 下面是来自GCC WIKI的一个模板
// Generic helper definitions for shared library support
#if defined _WIN32 || defined __CYGWIN__
#define FOX_HELPER_DLL_IMPORT __declspec(dllimport)
#define FOX_HELPER_DLL_EXPORT __declspec(dllexport)
#define FOX_HELPER_DLL_LOCAL
#else
#if __GNUC__ >= 4
#define FOX_HELPER_DLL_IMPORT __attribute__ ((visibility ("default")))
#define FOX_HELPER_DLL_EXPORT __attribute__ ((visibility ("default")))
#define FOX_HELPER_DLL_LOCAL __attribute__ ((visibility ("hidden")))
#else
#define FOX_HELPER_DLL_IMPORT
#define FOX_HELPER_DLL_EXPORT
#define FOX_HELPER_DLL_LOCAL
#endif
#endif
// Now we use the generic helper definitions above to define FOX_API and FOX_LOCAL.
// FOX_API is used for the public API symbols. It either DLL imports or DLL exports (or does nothing for static build)
// FOX_LOCAL is used for non-api symbols.
#ifdef FOX_DLL // defined if FOX is compiled as a DLL
#ifdef FOX_DLL_EXPORTS // defined if we are building the FOX DLL (instead of using it)
#define FOX_API FOX_HELPER_DLL_EXPORT
#else
#define FOX_API FOX_HELPER_DLL_IMPORT
#endif // FOX_DLL_EXPORTS
#define FOX_LOCAL FOX_HELPER_DLL_LOCAL
#else // FOX_DLL is not defined: this means FOX is a static lib.
#define FX_API
#define FOX_LOCAL
#endif // FOX_DLL
使用方法:
- 在编译动态库的时候,设置FOX_DLL和FOX_DLL_EXPORTS这两个宏
class FOX_API Fox {};
(3)position-independent code (PIC) 编译动态库 .so
position-independent code (PIC):
- 用于生成位置无关代码。位置无关代码,可以理解为代码无绝对跳转,跳转都为相对跳转。生成动态库时,需要加上-fPIC选项。
有-fPIC选项好处:
1)添加 -fPIC 选项实现真正意义上的多个进程共享 .so 库。多个进程引用同一个 -fPIC 动态库时,可以共用内存。这一个库在不同进程中的虚拟地址不同,操作系统会把它们映射到同一块物理内存上。
2)不添加 -fPIC 选项,加载 .so 库时,需要对代码段引用的数据对象重定位,重定位会修改代码段的内容,造成每个使用这个 .so 文件代码段的进程在内核里都会生成这个 .so 文件代码段的 copy,每个 copy 都不一样,取决于这个 .so 文件代码段和数据段内存映射的位置。不添加 -fPIC 选项,消耗内存,编译的.so文件的优点是加载速度快。
用法:
add_library(mylib SHARED ##.cc)
set_target_properties(mylib PROPERTIES POSITION_INDEPENDENT_CODE ON)
其他:若编译成静态库,也不会报错:
add_library(mylib STATIC ##.cc)
set_target_properties(mylib PROPERTIES POSITION_INDEPENDENT_CODE ON)
(4)生成动态库的 SO-NAME
- ref:cmake:生成动态库的 SO-NAM
静态库没有SO-NAME
Linux共享库的文件命名规则必须如下:
libname.so.x.y.z
最前面使用前缀lib,中间为库的名字,后缀为.so,后面跟着 3 个数字组成的版本号。"x"表示**主版本号,"y"表示次版本号,"z"表示发布版本号。各版本号含义如下:
主版本号表示库的重大升级,不同主版本号的库之间是不兼容的;
次版本号表示库的增量升级,即增加一些新的接口符号,且保持原有符号不变;
发布版本号表示库的一些错误修正、性能的改进等,并不增加任何新的接口,也不对接口进行更改。相同主、次版本号,不同发布版本号的库之间完全兼容,依赖于某个发布版号的程序可以在任何一个其他发布版本号中正常运行,而无需做任何修改。
现在的 Linux 中也存在不少不遵守上述规定的"顽固份子",比如最基本的 C 语言库——Glibc。
SO-NAME作用:
- 这个“SO-NAME“即共享库的文件名去掉次版本号和发布版本号,保留主版本号的名称。比如 libfoo.so.2.6.1 的 SO-NAME 为 libfoo.so.2
- 建立以 SO-NAME 为名字的软链接的目的是使得所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的 SO-NAME,而不是使用详细的版本号。
用法:
- 这里主要使用的是 set_target_properties()命令及其 PROPERTIES 中的 VERSION 和 SOVERSION 参数。其中 VERSION指定库的完整版本号,SOVERSION 指定库的 SONAME。
project(test)
cmake_minimum_required(VERSION 3.16)
add_library(mylib SHARED ${CMAKE_CURRENT_SOURCE_DIR}/lib/mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(xx
PROPERTIES CXX_VISIBILITY_PRESET hidden
)
set_target_properties(mylib PROPERTIES POSITION_INDEPENDENT_CODE ON)
set_target_properties(mylib PROPERTIES VERSION 1.0.0 SOVERSION 1)
测试:
wangji@script-wang:~/code/test/my_course/course/11/03_library/04$ ll build/
total 96
drwxr-xr-x 3 wangji wangji 4096 Feb 13 10:11 ./
drwxr-xr-x 5 wangji wangji 4096 Feb 13 10:10 ../
-rw-r--r-- 1 wangji wangji 14172 Feb 13 10:11 CMakeCache.txt
drwxr-xr-x 8 wangji wangji 4096 Feb 13 10:11 CMakeFiles/
-rw-r--r-- 1 wangji wangji 9698 Feb 13 10:11 Makefile
-rw-r--r-- 1 wangji wangji 2740 Feb 13 10:11 cmake_install.cmake
lrwxrwxrwx 1 wangji wangji 13 Feb 13 10:11 libmylib.so -> libmylib.so.1*
lrwxrwxrwx 1 wangji wangji 17 Feb 13 10:11 libmylib.so.1 -> libmylib.so.1.0.0*
wangji@script-wang:~/code/test/my_course/course/11/03_library/04$ ldd build/main
linux-vdso.so.1 (0x00007ffc7ffb5000)
libmylib.so.1 => /home/wangji/code/test/my_course/course/11/03_library/04/build/libmylib.so.1 (0x00007f8aeca92000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8aec888000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8aeca9e000)
(5)include_directories,link_directories,link_libraries和target_link_libraries解析
INCLUDE_DIRECTORIES(添加头文件目录)
它相当于g++选项中的-I参数的作用,也相当于环境变量中增加路径到CPLUS_INCLUDE_PATH变量的作用
比如:include_directories(“/opt/MATLAB/R2012a/extern/include”)
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:$MATLAB/extern/include
LINK_DIRECTORIES(添加需要链接的库文件目录)
link_directories(directory1 directory2 …)
它相当于g++命令的-L选项的作用,也相当于环境变量中增加LD_LIBRARY_PATH的路径的作用。
LINK_DIRECTORIES(“/opt/MATLAB/R2012a/bin/glnxa64”)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/MATLAB/R2012a/bin/glnxa64
LINK_LIBRARIES(添加需要链接的库文件路径,注意这里是库的全路径)
LINK_LIBRARIES(“/opt/MATLAB/R2012a/bin/glnxa64/libmx.so”)
TARGET_LINK_LIBRARIES (设置要链接的库文件的名称)
TARGET_LINK_LIBRARIES(myProject hello),连接libhello.so库
(6)–no-copy-dt-needed-entries问题
DSO missing from command line问题:
- 前置条件
libA.so在编译过程中显式的链接了libB.so
可执行文件中使用了libB.so的函数
binuntils版本 ≥ 2.22
- eg:仅供参考,下面的代码写法不符合C++的写法规范
libB.so
#include <stdio.h>
int funB1(){
printf("in funB1");
return 0;
}
int funB2(){
printf("in funB2");
return 0;
}
编译libB.so:
- 该库中只有一个函数funA1,该函数在内部调用了libB中的funB1函数。且该函数会被可执行文件调用。
$ gcc libB.cpp -fPIC -shared -o libB.so
libA.so的源码:
#include <stdio.h>
int funB1();
int funA1(){
printf("in funA1 \n");
funB1();
return 0;
}
编译libA.so:
$ gcc libA.cpp -fPIC -shared -o libA.so -Wl,-rpath=./ -L./ -lB
main.cpp
int funA1();
int funB2();
int main(){
funA1();
funB2();
return 0;
}
编译main.cpp
gcc main.cpp -L./ -lA
/usr/bin/ld: /tmp/ccDQXTKy.o: undefined reference to symbol '_Z5funB2v'
.//libB.so: error adding symbols: DSO missing from command line
collect2: error: ld returned 1 exit status
$ ldd libA.so
linux-vdso.so.1 => (0x00007ffd09def000)
libB.so => ./libB.so (0x00007fc513d7d000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc5139b3000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc514181000)
这里已经显示链接了libB.so,但是编译器要我们显式的链接libB.so?
这是binutils在2.22版本以后,默认把–no-copy-dt-needed-entries这个选项打开了。当打开了这个选项的时候,编译器在链接的时候是不会递归的去获取依赖动态库的依赖项的,于是就会出现上述的问题。
$ gcc main.cpp -L./ -Wl,--copy-dt-needed-entries -lA
Cmake用法:
cmake使用方法“set(CMAKE_EXE_LINKER_FLAGS “-Wl,–copy-dt-needed-entries”)
7.为什么 C++ 需要声明
在多文件编译章中,说到了需要在 main.cpp 声明 hello() 才能引用。为什么?
- 因为需要知道函数的参数和返回值类型:这样才能支持重载,隐式类型转换等特性。
例如 show(3),如果声明了 void show(float x),那么编译器知道把 3 转换成 3.0f 才能调用。 - 让编译器知道 hello 这个名字是一个函数,不是一个变量或者类的名字:这样当我写下 hello() 的时候,他知道我是想调用 hello 这个函数,而不是创建一个叫 hello 的类的对象。
其实,C++ 是一种强烈依赖上下文信息的编程语言,
- 举个例子:
vector < MyClass > a; // 声明一个由 MyClass 组成的数组
如果编译器不知道 vector 是个模板类,那他完全可以把 vector 看做一个变量名,把 < 解释为小于号,从而理解成判断‘vector’这个变量的值是否小于‘MyClass’这个变量的值。
正因如此,我们常常可以在 C++ 代码中看见这样的写法:typename decay::type
因为 T 是不确定的,导致编译器无法确定 decay 的 type 是一个类型,还是一个值。
因此用 typename 修饰来让编译器确信这是一个类型名…… - eg:course/01/06

8.为什么需要头文件?
问题:如果能只写一遍,然后自动插入到需要使用hello()的地方
- 问题如下所示:

头文件 - 批量插入几行代码的硬核方式
- 把 hello() 的声明放到单独一个文件 hello.h 里,然后在需要用到 hello() 这个声明的地方,打上一个记号,#include “hello.h”。
- 这个编译前替换的步骤逐渐变成编译器的了一部分,称为预处理阶段,#define 定义的宏也是这个阶段处理的。
- 此外,在实现的文件 hello.cpp 中导入声明的文件 hello.h 是个好习惯,可以保证当 hello.cpp 被修改时,比如改成 hello(int),编译器能够发现 hello.h 声明的 hello() 和定义的 hello(int) 不一样,避免“沉默的错误”。(对支持重载的 C++ 不奏效)
- 示例图如下:
- eg:course/01/07

递归地使用头文件
- 在 C++ 中常常用到很多的类,和函数一样,类的声明也会被放到头文件中。
- 有时候我们的函数声明需要使用到某些类,就需要用到声明了该类的头文件,像这样递归地 #include 即可:
- course/01/08

- 解决如果多个头文件都引用了 MyClass.h,那么 MyClass 会被重复定义两遍的方法:
在头文件前面加上一行:#pragma once,这样当预处理器第二次读到同一个文件时,就会自动跳过 - eg:course/01/09

9.CMake 中的子模块
复杂的工程中,我们需要划分子模块,通常一个库一个目录,比如:
- 这里我们把 hellolib 库的东西移到 hellolib 文件夹下了,里面的 CMakeLists.txt 定义了 hellolib 的生成规则。
- 要在根目录使用他,可以用 CMake 的 add_subdirectory 添加子目录,子目录也包含一个 CMakeLists.txt,其中定义的库在 add_subdirectory 之后就可以在外面使用。
- 子目录的 CMakeLists.txt 里路径名(比如 hello.cpp)都是相对路径
- eg:course/01/10


(1)子模块的头文件如何处理
因为 hello.h 被移到了 hellolib 子文件夹里,因此 main.cpp 里也要改成
- eg:course/01/10

- 如果要避免修改代码,我们可以通过 target_include_directories 指定a.out 的头文件搜索目录:(其中第一个 hellolib 是库名,第二个是目录)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC hellolib)
target_include_directories(a.out PUBLIC hellolib)
- 这样甚至可以用 <hello.h> 来引用这个头文件了,因为通过 target_include_directories 指定的路径会被视为与系统路径等价:

(2)总结
- eg:course/01/10/main.cpp
#include <cstdio>
//方式1
#include "hellolib/hello.h"
//方式2
//#include "hello.h" or #include <hello.h>
//CMake中增加# target_include_directories(a.out PUBLIC hellolib)
int main() {
hello();
return 0;
}
- 如果另一个 b.out 也需要用 hellolib 这个库,难道也得再指定一遍搜索路径吗?
不需要,其实我们只需要定义 hellolib 的头文件搜索路径,引用他的可执行文件 CMake 会自动添加这个路径: - eg:course/01/11
my_course/course/01/11/hellolib/CMakeLists.txt
add_library(hellolib STATIC hello.cpp)
target_include_directories(hellolib PUBLIC .)
my_course/course/01/11/CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_subdirectory(hellolib)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC hellolib)
- 此外,如果不希望让引用 hellolib 的可执行文件自动添加这个路径,把 PUBLIC 改成 PRIVATE 即可。
这就是他们的用途:决定一个属性要不要在被 link 的时候传播(传染病,去传染别人)。
11.目标的一些其他选项
除了头文件搜索目录以外,还有这些选项,PUBLIC 和 PRIVATE 对他们同理:
target_include_directories(myapp PUBLIC /usr/include/eigen3) # 添加头文件搜索目录
target_link_libraries(myapp PUBLIC hellolib) # 添加要链接的库
target_add_definitions(myapp PUBLIC MY_MACRO=1) # 添加一个宏定义,等价于#define 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) #给所有的目标添加编译器命令行选项
12.第三方库 - 作为纯头文件引入
有时候我们不满足于 C++ 标准库的功能,难免会用到一些第三方库。
- 最友好的一类库莫过于纯头文件库了,这里是一些好用的 header-only 库:
nothings/stb - 大名鼎鼎的 stb_image 系列,涵盖图像,声音,字体等,只需单头文件!
Neargye/magic_enum - 枚举类型的反射,如枚举转字符串等(实现方式很巧妙)
g-truc/glm - 模仿 GLSL 语法的数学矢量/矩阵库(附带一些常用函数,随机数生成等)
Tencent/rapidjson - 单纯的 JSON 库,甚至没依赖 STL(可定制性高,工程美学经典)
ericniebler/range-v3 - C++20 ranges 库就是受到他启发(完全是头文件组成)
fmtlib/fmt - 格式化库,提供 std::format 的替代品(需要 -DFMT_HEADER_ONLY)
gabime/spdlog - 能适配控制台,安卓等多后端的日志库(和 fmt 冲突!)
- 用法:只需要把他们的 include 目录或头文件下载下来,然后 include_directories(spdlog/include) 即可。
- 缺点:函数直接实现在头文件里,没有提前编译,从而需要重复编译同样内容,编译时间长。
- eg:course/01/12

13.第三方库 - 作为子模块引入
第二友好的方式则是作为 CMake 子模块引入,也就是通过 add_subdirectory。
- 方法就是把那个项目(以fmt为例)的源码放到你工程的根目录:
- 这些库能够很好地支持作为子模块引入:
fmtlib/fmt - 格式化库,提供 std::format 的替代品
gabime/spdlog - 能适配控制台,安卓等多后端的日志库
ericniebler/range-v3 - C++20 ranges 库就是受到他启发
g-truc/glm - 模仿 GLSL 语法的数学矢量/矩阵库
abseil/abseil-cpp - 旨在补充标准库没有的常用功能
bombela/backward-cpp - 实现了 C++ 的堆栈回溯便于调试
google/googletest - 谷歌单元测试框架
google/benchmark - 谷歌性能评估框架
glfw/glfw - OpenGL 窗口和上下文管理
libigl/libigl - 各种图形学算法大合集
- eg:course/01/13

fmt - 使用这个神奇的格式化库


14.CMake - 引用系统中预安装的第三方库
可以通过 find_package 命令寻找系统中的包/库:
find_package(fmt REQUIRED)
target_link_libraries(myexec PUBLIC fmt::fmt)
- 为什么是 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 列表
fmt::fmt
spdlog::spdlog
range-v3::range-v3
TBB::tbb
OpenVDB::openvdb
Boost::iostreams
Eigen3::Eigen
OpenMP::OpenMP_CXX
- 不同的包之间常常有着依赖关系,而包管理器的作者为 find_package 编写的脚本(例如/usr/lib/cmake/TBB/TBBConfig.cmake)能够自动查找所有依赖,并利用刚刚提到的 PUBLIC PRIVATE 正确处理依赖项,比如如果你引用了 OpenVDB::openvdb 那么 TBB::tbb 也会被自动引用。
- 其他包的引用格式和文档参考
15.安装第三方库 - 包管理器
(1) arch的pacman
Linux 可以用系统自带的包管理器(如 apt)安装 C++ 包。
pacman -S fmt
(2)ubuntu apt-get
(3)vcpkg
- Windows 则没有自带的包管理器。因此可以用跨平台的 vcpkg:https://github.com/microsoft/vcpkg
- 使用方法:下载 vcpkg 的源码,放到你的项目根目录,像这样:

-
- eg:course/01/14,对应的gittee链接
-
- 用 git clone --depth=1 的好处是限制 clone 的深度,不会下载 Git 协作的历史记录,这样可以大大加快克隆的速度 depth用于指定克隆深度,为1即表示只克隆最近一次commit
- ./vcpkg install fmt:x64-linux,可以使用./vcpkg install fmt:linux提前看看要下载什么系统的。要不然会全下载下来
set -e
git clone https://github.com/microsoft/vcpkg.git --depth=1
rm -rf build
cmake -B build -DCMAKE_TOOLCHAIN_FILE="$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake"
cmake --build build --target a.out
cd vcpkg
sh bootstrap-vcpkg.sh
./vcpkg install fmt:x64-linux
cd ..
build/a.out
(4)CMake 添加第三方库依赖方式git submodule、 find_library、FetchContent、CPM等
- ref:C++工程:总结 CMake 添加第三方库依赖方式git submodule、 find_library、FetchContent、CPM等,CMake学习笔记05–ExternalProject_Add模块,CMake项目中集成gtest使用ExternalProject_Add
以jsoncpp 为例,介绍几种引入第三方库的方式
(1)方法1:代码依赖
这种方式是把第三方库的完整代码直接添加到我们的项目中,当做项目代码的一部分进行编译,这种方式会把第三方代码和我们的代码混在一起,并不推荐使用。首先我们需要到 jsoncpp 下载需要的头文件和实现代码,放到项目当中。
工程文件目录
├── CMakeLists.txt
├── jsoncpp
│ ├── include
│ │ └── json
│ │ ├── autolink.h
│ │ ├── config.h
│ │ ├── features.h
│ │ ├── forwards.h
│ │ ├── json.h
│ │ ├── reader.h
│ │ ├── value.h
│ │ └── writer.h
│ ├── json_batchallocator.h
│ ├── json_internalarray.inl
│ ├── json_internalmap.inl
│ ├── json_reader.cpp
│ ├── json_value.cpp
│ ├── json_valueiterator.inl
│ └── json_writer.cpp
└── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(includes_full_code)
set(CMAKE_CXX_STANDARD 14)
# 包含头文件
include_directories(./jsoncpp/include)
set(jsoncpp jsoncpp/json_reader.cpp jsoncpp/json_writer.cpp jsoncpp/json_value.cpp)
# 添加可执行代码
add_executable(includes_full_code main.cpp ${jsoncpp})
main.cpp
#include <iostream>
#include "json/json.h"
int main() {
Json::Value json;
json["name"] = "Wiki";
json["age"] = 18;
std::cout << json.toStyledString() << std::endl;
return 0;
}
(2)方法2:优化代码依赖
- jsoncpp文件夹多了一个 CMakeLists.txt 文件
- subdirectory_example
├── CMakeLists.txt
├── jsoncpp
│ ├── CMakeLists.txt
│ ├── include
│ │ └── json
│ │ ├── autolink.h
│ │ ├── config.h
│ │ ├── features.h
│ │ ├── forwards.h
│ │ ├── json.h
│ │ ├── reader.h
│ │ ├── value.h
│ │ └── writer.h
│ ├── json_batchallocator.h
│ ├── json_internalarray.inl
│ ├── json_internalmap.inl
│ ├── json_reader.cpp
│ ├── json_value.cpp
│ ├── json_valueiterator.inl
│ └── json_writer.cpp
└── main.cpp
jsoncpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(jsoncpp)
add_library(${PROJECT_NAME} json_reader.cpp json_value.cpp json_writer.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/include)
CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(multi_cmakelists)
# 添加子工程
add_subdirectory(jsoncpp)
add_executable(${PROJECT_NAME} main.cpp)
# 链接子工程
target_link_libraries(${PROJECT_NAME} jsoncpp)
(3)find_library:编译库方式引入
- 最佳方式:这里只需要导入jsoncpp的头文件和.a文件。用户可以编写脚本下载jsoncpp的头文件和库文件,然后使用include_directories、link_directories或者target_include_directories,target_link_directories()引入即可。
- demo
├── CMakeLists.txt
├── jsoncpp
│ ├── include
│ │ └── json
│ │ ├── autolink.h
│ │ ├── config.h
│ │ ├── features.h
│ │ ├── forwards.h
│ │ ├── json.h
│ │ ├── reader.h
│ │ ├── value.h
│ │ └── writer.h
│ └── libjsoncpp.a
└── main.cpp
cmake_minimum_required(VERSION 3.17)
project(find_library_example)
include_directories(jsoncpp/include)
add_executable(${PROJECT_NAME} main.cpp)
find_library(jsoncpp_lib NAMES jsoncpp PATHS ./jsoncpp)
target_link_libraries(${PROJECT_NAME} ${jsoncpp_lib})
(4)FetchContent
- 不建议使用:建议通过压缩包的方式引入,因为直接引入git仓库可能会很慢。
- 相关的还有:ExternalProject_Add:eg:使用CMake项目中集成gtest,太麻烦了,建议直接写脚本处理,不要使用CMake处理
- demo
工程文件目录
├── CMakeLists.txt
└── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(fetch_content_example)
include(FetchContent)
#FetchContent_Declare(jsoncpp
# GIT_REPOSITORY https://github.com/open-source-parsers/jsoncpp.git
# GIT_TAG 1.9.4)
# 建议使用压缩包的方式依赖,下载速度更快
FetchContent_Declare(jsoncpp
URL https://github.com/open-source-parsers/jsoncpp/archive/1.9.4.tar.gz)
FetchContent_MakeAvailable(jsoncpp)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} jsoncpp_lib)
(5) CPM
- 不建议使用:
- demo
工程文件目录
├── CMakeLists.txt
├── cmake
│ ├── CPM.cmake
│ ├── get_cpm.cmake
│ └── testing.cmake
└── main.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(cpm_example)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/)
include(CPM)
#CPMAddPackage(
# GIT_REPOSITORY https://github.com/open-source-parsers/jsoncpp.git
# GIT_TAG 1.9.4)
# 建议使用压缩包的方式依赖,下载速度更快
CPMAddPackage(
NAME jsoncpp
URL https://github.com/open-source-parsers/jsoncpp/archive/1.9.4.tar.gz)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} jsoncpp_lib)
(6)find_package
- 自行写一个脚本,将其安装到某个目录下
- demo
# 拉取代码
git clone https://github.com/open-source-parsers/jsoncpp
cd jsoncpp
mkdir -p build/debug
cd build/debug
# 生成Makefile
cmake -DCMAKE_BUILD_TYPE=release -DBUILD_STATIC_LIBS=OFF -DBUILD_SHARED_LIBS=ON -DARCHIVE_INSTALL_DIR=. -DCMAKE_INSTALL_INCLUDEDIR=include -G "Unix Makefiles" ../..
# 安装
make && make install
CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(find_package_example)
find_package(jsoncpp REQUIRED)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} jsoncpp_lib)
(7)git submodule
-
ref:git submodules