文章

CMake 的简单使用

这是一篇简要介绍如何使用CMake的文章

CMake 的简单使用

引言

相比于make的繁琐,cmake不仅可以跨平台编译,还可以跨平台使用同一套代码。cmake的语法也比make简单很多,下面是一些常用的cmake命令和语法。

基础框架

1
2
3
4
cmake_minimum_required(VERSION 3.9) # cmake版本要求
project(hello) # 项目名称
set(CMAKE_CXX_STANDARD 11) # C++标准要求
add_executable(hello main.cpp) # 生成可执行文件hello,源文件为main.cpp

注意,不想make,在add_executable中不需要添加.o文件与.hpp文件,cmake会自动处理。

cmake基础命令

1
2
3
4
5
6
# 生成Makefile文件
cmake -S . -B build # -S指定源文件目录,-B指定输出目录
# 执行构建
cmake --build build # --build指定输出目录
# 运行程序
./build/hello # hello为可执行文件名

ps:第一条很多情况下是手动创建build目录,之后在build目录下执行cmake ..命令,-S .一般省略。

cmake 中的库

对于一个程序main.cpp,比如他依赖另一个文件lib.cpp和lib.hpp,lib.cpp中有一个函数foo(),我们可以将lib.cpp编译成一个库,然后在main.cpp中调用foo()。这样的好处是,可以提升编译效率,如果库的代码有所改动,只需要重新编译库,而不需要重新编译main.cpp。

1
2
3
4
add_executable(main.cpp lib.cpp) # 直接编译成可执行文件

add_libiary(lib static lib.cpp) # 编译成静态库lib.a
target_link_libraries(main lib) # 链接库lib.a

注意:静态库的后缀名为.a,动态库的后缀名为.so。

目录层级

像上述那样将程序编译成库了之后,就可以将这部分程序单独放在一个文件夹中,不然所有文件都在一个层级下,代码会很乱。

比如放lib.cpp和lib.hpp放在lib文件夹下,这时在CMakeLists.txt中就需要添加lib目录了。 文件目录结构如下:

1
2
3
4
5
6
7
8
.
├── CMakeLists.txt
├── lib
│   ├── CMakeLists.txt
│   ├── include
│   │   └── lib.hpp
│   └── lib.cpp
└── main.cpp
1
add_subdirectory(lib) # 添加lib目录

编译库的命令则要放到lib目录下的CMakeLists.txt中。

1
2
3
add_libiary(lib static lib.cpp)
# 为该库添加头文件搜索路径
target_include_directories(lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) # CMAKE_CURRENT_SOURCE_DIR为当前cmakelist当前目录,pulic指的是可以被外部看到

一般库的头文件放在include目录下,且我们调用这个库的时候也只需要包含lib.hpp就可以了,因此搜索路径添加的是include目录。使用的时候#include <lib.hpp>即可。

查找库

在程序中如果使用一些系统库与第三方库,比如opencv,boost等,我们需要在CMakeLists.txt中添加查找库的命令。

1
2
3
4
find_package(OpenCV REQUIRED) # 查找opencv库,REQUIRED表示必须找到,否则报错
....
# 在最后将这个库链接进去
target_link_libraries(lib private ${OpenCV_LIBS}) # lib为库名,OpenCV_LIBS为opencv库的变量名

多层调用的情况

假设当前的目录结构为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.
├── CMakeLists.txt
├── lib
│   ├── CMakeLists.txt
│   ├── include
│   │   └── lib.hpp
│   └── lib.cpp
├── lib2
│   ├── CMakeLists.txt
│   ├── include
│   │   └── lib2.hpp
│   └── lib2.cpp
└── lib3
    ├── CMakeLists.txt
    ├── include
    │   └── lib3.hpp
    └── lib3.cpp
└── main.cpp

假设他们之间的调用关系为lib3依赖lib2,lib2依赖lib,main.cpp依赖lib3。 那么在lib3的CMakeLists.txt中需要添加lib2的目录,lib2的CMakeLists.txt中需要添加lib的目录。 而在main.cpp的CMakeLists.txt中需要添加这三个的目录的依赖,但是只需要添加lib3的链接。

1
2
3
4
5
6
# main.cpp的CMakeLists.txt
add_subdirectory(lib1) 
add_subdirectory(lib2) 
add_subdirectory(lib3)

target_link_libraries(main lib3) # 只需要链接lib3

变量

1
2
3
4
set(MY_VAR "hello") # 定义变量MY_VAR

set(MY_VAR "hello" CACHE STRING "hello") # 定义变量MY_VAR,并添加到缓存中,方便在cmake-gui中修改
target_compile_definitions(hello PRIVATE MY_VAR=${MY_VAR}) # 将变量MY_VAR添加到编译选项中,作用是在代码中可以使用这个变量

cache变量:用于持久化存储配置信息,其核心作用是 跨多次 CMake 运行保留用户或系统的配置选择。举例来说BUILD_TYPE变量用于指定是否开启debug模式,或者是否开启编译器优化等。

如何使用这个变量

1
2
3
4
5
6
7
#include <iostream>
#include <lib.hpp>
using namespace std;
int main() {
    cout << MY_VAR << endl; // 输出hello
    return 0;
}

注意:变量的作用域是当前CMakeLists.txt文件,如果想在子目录中使用,需要使用set(MY_VAR "hello" PARENT_SCOPE),或者在子目录中重新定义。

在哪定义这个变量

1
cmake -D MY_VAR="hello" .. # 在命令行中定义变量

注意:在命令行中定义的变量会覆盖CMakeLists.txt中定义的变量。也可以将cmake改为ccmake,这样会弹出一个界面,可以在里面修改变量的值。包括修改build-type等。

加载一些未安装的第三方库的方法

比如单元测试库googletest没有安装在系统中,我们可以下载并放在项目目录下。

1
2
3
4
5
6
FetchContent_Declare(
    gtest
    URL https://github.com/google/googletest/archive/refs/tags/v1.13.0.zip
)
FetchContent_MakeAvailable(googletest) # 下载并解压
target_link_libraries(main gtest gtest_main) # 链接库

一般这种第三方库具体在cmake部分会有说明,直接复制粘贴就可以了。

注意:FetchContent_Declare的URL可以是本地路径,也可以是网络路径。

macro与function

类似于函数的作用,主要是为了简化代码。二者的区别在于,macro是直接展开的,而function是调用的。也就是说marco中set的变量后面还可以使用,而function中set的变量在函数外部是不可见的。

1
2
3
4
5
6
macro(hello name) # 定义宏hello,参数为name
    message("hello ${name}") # 输出hello name
endmacro() # 结束宏定义

hello("world") # 调用宏hello,参数为world

参考链接

本文由作者按照 CC BY 4.0 进行授权