CMake保姆级编程

文档来源:爱编程的大丙

视频链接

CMake保姆级变成(上)

CMake保姆级变成(下)

ubuntu下查看cmake版本:

1
2
# 终端输入
cmake --version

一、基础

1.源文件与CmakeLists.txt均在一个目录下

文件下的目录结构:

1
2
3
4
5
6
$ tree
.
├── head.h
├── a.c
├── b.c
└── main.c

所有源码全部放在一个文件夹下,在该目录下编写CmakeLists.txt:

1
2
3
cmake_minimum_required(VERSION 3.16)
project(CALC) # 工程名字可以与可执行程序名不一样
add_executable(app a.c b.c main.c)
  • cmake_minimum_required:指定使用的 cmake 的最低版本

  • project:定义工程名称

  • add_executable:定义工程会生成一个可执行程序

1
add_executable(可执行程序名 源文件名称)

CMakeLists.txt同级目录下终端打开输入

1
cmake .

当执行cmake命令之后,CMakeLists.txt 中的命令就会被执行,所以一定要注意给 cmake 命令指定路径的时候一定不能出错

执行命令之后,看一下源文件所在目录中是否多了一些文件:

1
2
3
4
5
6
7
8
9
10
11
12
$ tree -L 1
.
├── add.c
├── CMakeCache.txt # new add file
├── CMakeFiles # new add dir
├── cmake_install.cmake # new add file
├── CMakeLists.txt
├── head.h
├── a.c
├── b.c
├── main.c
└── Makefile # new add file

注意: 可以通过创建一个build目录,在build目录与CmakeLists.txt文件同级,在build目录下终端输入cmake ..,则cmake产生的中间文件均会放在build目录下

我们可以看到在对应的目录下生成了一个 makefile 文件,此时再执行 make 命令,就可以对项目进行构建得到所需的可执行程序了

1
make 
1
2
3
4
5
6
7
8
9
$ make
Scanning dependencies of target app
[ 16%] Building C object CMakeFiles/app.dir/add.c.o
[ 33%] Building C object CMakeFiles/app.dir/div.c.o
[ 50%] Building C object CMakeFiles/app.dir/main.c.o
[ 66%] Building C object CMakeFiles/app.dir/mult.c.o
[ 83%] Building C object CMakeFiles/app.dir/sub.c.o
[100%] Linking C executable app
[100%] Built target app

2.定义变量set

假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在 cmake 里定义变量需要使用 set

1
2
3
4
5
# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
  • VAR:变量名
  • VALUE:变量值
1
2
3
# 如下实例
set(SRC_LIST ac B.c main.c)
add_executable(app ${SRC_LIST})
(1)使用c++标准

在编写 C++ 程序的时候,可能会用到 C++11、C++14、C++17、C++20 等新特性,那么就需要在编译的时候在编译命令中制定出要使用哪个标准,终端命令行直接输入:

1
$ g++ *.cpp -std=c++11 -o app

上面的例子中通过参数 -std=c++11 指定出要使用c++11标准编译程序,C++ 标准对应有一宏叫做 DCMAKE_CXX_STANDARD。在 CMake 中想要指定 C++ 标准有两种方式:

  • CMakeLists.txt 中通过set命令指定
1
2
3
4
5
6
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)
  • 在执行 cmake 命令的时候指定出这个宏的值
1
2
3
4
5
6
#增加-std=c++11
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
#增加-std=c++14
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=14
#增加-std=c++17
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=17

示例

1
2
# c99 标准  CMakeLists.txt 在上一级目录下
cmake .. -DCMAKE_C_STANDARD=99
(2)指定输出的路径

CMake 中指定可执行程序输出的路径,也对应一个宏,叫做 EXECUTABLE_OUTPUT_PATH,它的值还是通过 set 命令进行设置:

1
2
3
set(HOME /home/zxz/Proj/CLionProj/Linux_C/Point/bin)
# set(HOME ../bin)
set(EXECUTABLE_OUTPUT_PATH ${HOME})
  • 第一行:定义一个变量用于存储一个绝对路径(也可以创建相对路径,bin,build目录与CMakeLists.txt同级,在build目录下执行cmake ..
  • 第二行:将拼接好的路径值设置给 EXECUTABLE_OUTPUT_PATH 宏
    • 如果这个路径中的子目录不存在,会自动生成,无需自己手动创建

由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的./对应的就是makefile文件所在的那个目录。

3.搜索文件

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦也不现实。所以,在CMake中为我们提供了搜索文件的命令,可以使用aux_source_directory命令或者 file 命令。

(1)aux_source_directory
1
aux_source_directory(< dir > < variable >)
  • dir:要搜索的目录

  • variable:将从 dir 目录下搜索到的源文件列表存储到该变量中

实例项目:

项目目录:

1
2
3
4
5
6
7
8
9
10
11
12
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/CMake_Demo/De2$ tree
.
├── bin
│   └── De2
├── build
├── CMakeLists.txt
├── include
│   └── head.h
└── src
├── a.cpp
├── b.cpp
└── main.cpp

head.h

1
2
3
4
5
6
7
8
9
#ifndef DE1_HEAD_H
#define DE1_HEAD_H

// 加法
int add(int a, int b);
// 减法
int sub(int a, int b);

#endif //DE1_HEAD_H

a.cpp

1
2
3
4
5
6
#include "head.h"

int add(int a, int b)
{
return a+b;
}

b.cpp

1
2
3
4
5
6
#include "head.h"

int sub(int a, int b)
{
return a-b;
}

main.cpp

1
2
3
4
5
6
7
8
9
#include <iostream>
#include "head.h"

int main() {
int a = 20,b=15;
std::cout << "the sum = " << add(a,b) << std::endl;
std::cout << "the sub = " << sub(a,b) << std::endl;
return 0;
}

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cmake_minimum_required(VERSION 3.16)

PROJECT(De2)

set(CMAKE_CXX_STANDARD 14)

# 定义可执行文件的输出路径
set(HOME ${PROJECT_SOURCE_DIR}/bin)

# cmake 是输出一般信息
message("PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")

# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)

# 指定可执行文件的输出路径
set(EXECUTABLE_OUTPUT_PATH ${HOME})

add_executable(De2 ${SRC_LIST})

注意:

  • ${PROJECT_SOURCE_DIR}是指最近包含PROJECT()语句的CMakeLists.txt所在的目录,如上的CMakeLists.txt中不包含了PROJECT(De2)语句,而CMakeLists.txt所在的目录是~/Proj/CLionProj/CMake_Demo/De2
  • ${CMAKE_CURRENT_SOURCE_DIR}这是cmake当前正在处理的源目录的完整路径,在该程序中,在build目录下终端输入cmake ..处理的是build的上级目录下的CMakeLists.txt文件,则此处的${CMAKE_CURRENT_SOURCE_DIR}也是和而CMakeLists.txt所在的目录一致

在build目录下终端输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cmake ..
PROJECT_SOURCE_DIR = /home/zxz/Proj/CLionProj/CMake_Demo/De2
CMAKE_CURRENT_SOURCE_DIR = /home/zxz/Proj/CLionProj/CMake_Demo/De2
-- Configuring done
-- Generating done
-- Build files have been written to: /home/zxz/Proj/CLionProj/CMake_Demo/De2/build


$ make
[ 25%] Building CXX object CMakeFiles/De2.dir/src/a.cpp.o
[ 50%] Building CXX object CMakeFiles/De2.dir/src/b.cpp.o
[ 75%] Building CXX object CMakeFiles/De2.dir/src/main.cpp.o
[100%] Linking CXX executable ../bin/De2
[100%] Built target De2

bin目录下可以找到可执行文件De2

(2)file
1
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
  • GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中
  • GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中

搜索当前目录的 src 目录下所有的源文件,并存储到变量中

1
2
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
  • CMAKE_CURRENT_SOURCE_DIR 宏表示当前访问的 CMakeLists.txt 文件所在的路径

  • 关于要搜索的文件路径和类型可加双引号,也可不加:

1
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")

4.包含头文件

在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是 include_directories:

1
include_directories(headpath)

实例项目

项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/CMake_Demo/De2$ tree
.
├── bin
│   └── De2
├── build
├── CMakeLists.txt
├── include
│   └── head.h
└── src
├── a.cpp
├── b.cpp
└── main.cpp

需要在CMakeLists.txt中添加语句,指定头文件路径

1
2
# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)
  • ${PROJECT_SOURCE_DIR}是指最近包含PROJECT()语句的CMakeLists.txt所在的目录

5.制作动态库或静态库

有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,下面来讲解在cmake中生成这两类库文件的方法

(1)制作静态库

cmake中,如果要制作静态库,需要使用的命令如下,关键字STATIC

1
add_library(库名称 STATIC 源文件1 [源文件2] ...) 

在Linux中,静态库名字分为三部分:lib+库名字+.a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充

实例项目:将src中的源文件编译成静态库

项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/CMake_Demo/De4$ tree
.
├── bin
├── build
├── CMakeLists.txt
├── include
│   └── head.h
├── main.cpp
└── src
├── a.cpp
└── b.cpp

4 directories, 5 files

CMakeLists.txt

1
2
3
4
5
6
7
8
# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件,并且存储值SRC_LIST中(数组)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 将SRC_LIST中的源文件制作为 静态库
add_library(calc STATIC ${SRC_LIST})

这样最终就会生成对应的静态库文件libcalc.a

(2)制作动态库

cmake中,如果要制作动态库,需要使用的命令如下,关键字SHARED

1
add_library(库名称 SHARED 源文件1 [源文件2] ...) 

在Linux中,动态库名字分为三部分:lib+库名字+.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充

CMakeLists.txt

1
2
3
4
5
6
7
8
# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件,并且存储值SRC_LIST中(数组)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 生成动态库
add_library(calc SHARED ${SRC_LIST})
(3)指定输出路径
方式1-适用动态库

对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录:

1
2
3
4
5
6
7
8
9
10
11
# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件,并且存储值SRC_LIST中(数组)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 设置动态库生成路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

# 生成动态库
add_library(calc SHARED ${SRC_LIST})

通过set命令给EXECUTABLE_OUTPUT_PATH宏设置了一个路径,这个路径就是可执行文件生成的路径

方式2-动静态库均适用(一般用这个)

由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用EXECUTABLE_OUTPUT_PATH宏了,而应该使用LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件都适用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件,并且存储值SRC_LIST中(数组)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 设置动态(静态)库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

# 生成动态库
add_library(calc SHARED ${SRC_LIST})

# 生成静态库
add_library(calc STATIC ${SRC_LIST})

6.包含库文件

在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令

(1)链接静态库

项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/CMake_Demo/De4$ tree
.
├── bin
├── build
├── CMakeLists.txt
├── include
│   └── head.h
├── main.cpp
└── src
├── a.cpp
└── b.cpp

4 directories, 5 files

src目录下的a.cppb.cpp编译成静态库文件

链接静态库命令如下:

1
link_libraries(<static lib> [<static lib>...])
  • 参数1:指定出要链接的静态库的名字
    • 可以是全名 libxxx.a
    • 也可以是掐头(lib)去尾(.a)之后的名字 xxx
  • 参数2-N:要链接的其它静态库的名字

或者

1
2
# 这种方式需要 放到 add_executable(De3 main.cpp) 之后 
target_link_libraries(可执行文件名 静态库名)
  • 参数2:指定出要链接的静态库的名字

    • 可以是全名 libxxx.a

    • 也可以是掐头(lib)去尾(.a)之后的名字 xxx

如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:

1
link_directories(<lib path>)

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cmake_minimum_required(VERSION 3.16)

project(De3)

set(CMAKE_CXX_STANDARD 14)

# 定义可执行文件的输出路径
set(HOME ${PROJECT_SOURCE_DIR}/bin)

# cmake 是输出一般信息
message("PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")

# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件,并且存储值SRC_LIST中(数组)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 将SRC_LIST中的源文件制作为 静态库
add_library(do STATIC ${SRC_LIST})
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 为最后生成的可执行文件链接静态库
link_libraries(do)

# 指定可执行文件的输出路径
set(EXECUTABLE_OUTPUT_PATH ${HOME})

# 生成可执行文件
add_executable(De3 main.cpp)

# 链接库
#target_link_libraries(De3 do)

build目录下打开终端,输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cmake ..
PROJECT_SOURCE_DIR = /home/zxz/Proj/CLionProj/CMake_Demo/De4
CMAKE_CURRENT_SOURCE_DIR = /home/zxz/Proj/CLionProj/CMake_Demo/De4
-- Configuring done
-- Generating done
-- Build files have been written to: /home/zxz/Proj/CLionProj/CMake_Demo/De4/build


$ make
Scanning dependencies of target do
[ 20%] Building CXX object CMakeFiles/do.dir/src/a.cpp.o
[ 40%] Building CXX object CMakeFiles/do.dir/src/b.cpp.o
[ 60%] Linking CXX static library ../lib/libdo.a
[ 60%] Built target do
Scanning dependencies of target De3
[ 80%] Building CXX object CMakeFiles/De3.dir/main.cpp.o
[100%] Linking CXX executable ../bin/De3
[100%] Built target De3

此时的项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ tree
.
├── bin
│   └── De3
├── build
│  └── .....(此处省略)
├── CMakeLists.txt
├── include
│   └── head.h
├── lib
│   └── libdo.a
├── main.cpp
└── src
├── a.cpp
└── b.cpp
(2)链接动态库

在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的

Linux静态库与动态库

CMake中链接动态库

1
2
3
4
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • target:指定要加载动态库的文件的名字

    • 该文件可能是一个源文件
    • 该文件可能是一个动态库文件
    • 该文件可能是一个可执行文件
  • PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为 PUBLIC

    • 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可

    • 动态库的链接具有传递性,如果动态库 A 链接了动态库 B、C,动态库 D 链接了动态库 A,此时动态库 D 相当于也链接了动态库 B、C,并可以使用动态库 B、C 中定义的方法

    1
    2
    target_link_libraries(A B C)
    target_link_libraries(D A)
    • PUBLIC:在 public 后面的库会被 Link 到前面的target 中,并且里面的符号也会被导出,提供给第三方使用

    • PRIVATE:在 private 后面的库仅被 link 到前面的 target 中,并且终结掉,第三方不能感知你调了啥库

    • INTERFACE:在 interface 后面引入的库不会被链接到前面的 target 中,只会导出符号

区别

动态库的链接和静态库是完全不同的:

  • 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了
  • 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存
  • 静态库的扩展名一般为.a.lib;动态库的扩展名一般为.so.dll
  • 动态库(共享库)在编译时不会放到连接的目标程序中,即可执行文件无法单独运行。(缺少动态库生成的可执行文件无法运行)

因此,在cmake中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
cmake_minimum_required(VERSION 3.16)

project(De5)

set(CMAKE_CXX_STANDARD 14)

# 定义可执行文件的输出路径
set(HOME ${PROJECT_SOURCE_DIR}/bin)


# cmake 是输出一般信息
message("PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}")

# 包含头文件
include_directories(${PROJECT_SOURCE_DIR}/include)

# 搜索 src 目录下的源文件,并且存储值SRC_LIST中(数组)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 将SRC_LIST中的源文件制作为 动态库 关键词 SHARED
add_library(do SHARED ${SRC_LIST})
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)


# 指定可执行文件的输出路径
set(EXECUTABLE_OUTPUT_PATH ${HOME})

# 生成可执行文件
add_executable(De5 main.cpp)

# 链接动态库
target_link_libraries(De5 do)

target_link_libraries(De5 do) 中:

  • app: 对应的是最终生成的可执行程序的名字
  • do:这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为 libdo.so,在指定的时候一般会掐头(lib)去尾(.so)。

build目录下终端输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cmake ..
PROJECT_SOURCE_DIR = /home/zxz/Proj/CLionProj/CMake_Demo/De5
CMAKE_CURRENT_SOURCE_DIR = /home/zxz/Proj/CLionProj/CMake_Demo/De5
-- Configuring done
-- Generating done
-- Build files have been written to: /home/zxz/Proj/CLionProj/CMake_Demo/De5/build

$ make
Scanning dependencies of target do
[ 20%] Building CXX object CMakeFiles/do.dir/src/a.cpp.o
[ 40%] Building CXX object CMakeFiles/do.dir/src/b.cpp.o
[ 60%] Linking CXX shared library ../lib/libdo.so
[ 60%] Built target do
Scanning dependencies of target De5
[ 80%] Building CXX object CMakeFiles/De5.dir/main.cpp.o
[100%] Linking CXX executable ../bin/De5
[100%] Built target De5

此时的项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── bin
│   ├── De4
│   └── De5
├── build
│  └── .....(此处省略)
├── CMakeLists.txt
├── include
│   └── head.h
├── lib
│   └── libdo.so
├── main.cpp
└── src
├── a.cpp
└── b.cpp

(3)链接第三方库

可以同时链接 自己编写的动态库以及第三方库

1
target_link_libraries(De5 pthread)

上面的可执行文件De5链接了第三方的动态库 pthread线程库

但是当我们运行可执行文件时 ./De5,可能回报错,具体情况百度即可

有一些情况是因为我们没有将链接的动态库路径告知我们的程序,下面的语句可以在CMake中导入动态(静态)库路径

1
link_directories(path)

7.日志

CMakeLists.txt中可以添加消息(日志)输出语句,在cmake是会输出在控制台

1
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
  • (无) :重要消息
  • STATUS :非重要消息
  • WARNING:CMake 警告,会继续执行
  • AUTHOR_WARNING:CMake 警告 (dev), 会继续执行
  • SEND_ERRORCMake 错误,继续执行,但是会跳过生成的步骤
  • FATAL_ERRORCMake 错误,终止所有处理过程

CMake 的命令行工具会在stdout上显示 STATUS 消息,在 stderr 上显示其他所有消息。CMake 的 GUI 会在它的 log 区域显示所有消息。

CMake 警告和错误消息的文本显示使用的是一种简单的标记语言,文本没有缩进,段落之间以新行做为分隔符

1
2
3
4
5
6
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")

8.变量操作

有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过 file 命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用 set 命令也可以使用 list 命令

(1)使用set进行拼接

使用set对变量名进行拼接,如下语句

1
set(变量名1 ${变量名1} ${变量名2} ...)

关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖

1
2
3
4
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
set(SRC_1 ${SRC_1} ${SRC_2})
(2)使用list进行拼接

使用list对变量名进行拼接,如下语句

1
list(APPEND <list> [<element> ...])

list 命令的功能比set要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND 表示进行数据追加,后边的参数和 set 就一样了

1
2
3
4
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
list(APPEND SRC_1 ${SRC_1} ${SRC_2})

CMake中,使用 set 命令可以创建一个 listset(var a b c d e) 命令将会创建一个 list a b c d e,但是最终打印变量值的时候得到的是 abcde

1
2
set(tmp1 a b c d e)
message(${tmp1})

控制台输出

1
abcde
(3)list字符串移除

我们在通过 file 搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如

1
2
3
4
5
6
7
$ tree
.
├── a.cpp
├── b.cpp
└── main.cpp

0 directories, 3 files

在当前这么目录有五个源文件,其中 main.cpp 是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要 a.cppb.cpp 这两个源文件就可以了。此时,就需要将main.cpp搜索到的数据中剔除出去,想要实现这个功能,也可以使用 list

1
list(REMOVE_ITEM <list> <value> [<value> ...])

通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了 REMOVE_ITEM

1
2
3
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/*.cpp)
# 移除 main.cpp
list(REMOVE_ITEM SRC_1 ${PROJECT_SOURCE_DIR}/main.cpp)

移除的文件的名字指定给list就可以了。但是一定要注意通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在 list 中每个文件对应的路径都是一个 item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否则移除操作不会成功

(4)list的其他操作

获取list列表的长度、读取列表中指定索引的的元素,可以指定多个索引、将列表中的元素用连接符(字符串)连接起来组成一个字符串等…

CMake保姆级教程

9.宏定义

在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#define NUMBER 3

int main()
{
int a = 10;
#ifdef DEBUG
printf("我是一个程序猿, 我不会爬树...\n");
#endif
for(int i=0; i<NUMBER; ++i)
{
printf("hello, GCC!!!\n");
}
return 0;
}

上述代码:

1
2
3
#ifdef DEBUG
printf("我是一个程序猿, 我不会爬树...\n");
#endif

DEBUG 宏进行了判断,如果该宏被定义了,那么就会进行日志输出,如果没有定义这个宏,该段代码就相当于被注释掉了,因此最终无法看到日志输出(上述代码中并没有定义这个宏)

输出为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在 gcc/g++ 命令中去指定,如下

1
$ gcc test.c -DDEBUG -o app

gcc/g++ 命令中通过参数 -D 指定出要定义的宏的名字,这样就相当于在代码中定义了一个宏,其名字为 DEBUG

CMakeLists.txt中可以进行类似的定义

1
add_definitions(-D宏名称)

如下:

1
2
3
4
5
cmake_minimum_required(VERSION 3.16)
project(TEST)
# 自定义 DEBUG 宏
add_definitions(-DDEBUG)
add_executable(app ./test.c)

这样定义宏之后,上面的c++代码中的注释部分会在编译的时候 在日志中输出

10.预定义宏

功能
PROJECT_SOURCE_DIR 使用 cmake 命令后紧跟的目录,一般是工程的根目录
PROJECT_BINARY_DIR 执行 cmake 命令的目录
CMAKE_CURRENT_SOURCE_DIR 当前处理的 CMakeLists.txt 所在的路径
CMAKE_CURRENT_BINARY_DIR target 编译目录
EXECUTABLE_OUTPUT_PATH 重新定义目标二进制可执行文件的存放位置
LIBRARY_OUTPUT_PATH 重新定义目标链接库文件的存放位置
PROJECT_NAME 返回通过 PROJECT 指令定义的项目名称
CMAKE_BINARY_DIR 项目实际构建路径,假设在 build 目录进行的构建,那么得到的就是这个目录的路径

11.关键词

(1)PROJECT()

可以用来指定工程的名字和支持的语言,默认支持所有语言

PROJECT (HELLO) 指定了工程的名字,并且支持所有语言—建议

PROJECT (HELLO CXX) 指定了工程的名字,并且支持语言是C++

PROJECT (HELLO C CXX) 指定了工程的名字,并且支持语言是C和C++

该指定隐式定义了两个CMAKE的变量

<projectname>_BINARY_DIR,本例中是 HELLO_BINARY_DIR

<projectname>_SOURCE_DIR,本例中是 HELLO_SOURCE_DIR

MESSAGE关键字就可以直接使用者两个变量,都指向当前的工作目录

注意:如果改了工程名,这两个变量名也会改变,但是CMAKE给出了解决方案,又定义两个预定义变量:PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,这两个变量和HELLO_BINARY_DIRHELLO_SOURCE_DIR是一致的。所以改了工程名也没有关系

(2)MESSAGE()

向终端输出用户自定义的信息

主要包含三种信息:

  • SEND_ERROR,产生错误,生成过程被跳过。
  • SATUS,输出前缀为—的信息。
  • FATAL_ERROR,立即终止所有cmake过程.
(3)add_subdirecroty指令
1
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • 这个指令用于向当前工程添加存放源文件的子目录(src),并可以指定中间二进制和目标二进制存放的位置(bin)
  • EXCLUDE_FROM_ALL函数是将写的目录从编译中排除,如程序中的example
  • ADD_SUBDIRECTORY(src bin)
    • src子目录加入工程并指定编译输出(包含编译中间结果)路径为bin 目录
    • 如果不进行 bin 目录的指定,那么编译结果(包括中间结果)都将存放在build/src 目录

向工程添加多个特定的库文件搜索路径

1
2
3
4
link_directories(dir1 dir2 ...)
如:
# 将/usr/lib/mylibfolder 和 ./lib 添加到库文件搜索路径
link_directories(/usr/lib/mylibfolder ./lib)
(5)add_compile_options

添加编译参数

1
2
3
4
add_executable(exename source1 source2 ... sourceN)
如:
# 添加编译参数 -Wall -std=c++11 -O2
add_compile_options(-Wall -std=c++11 -O2)
(6)CMAKE_C_FLAGS

gcc编译选项

(7)CMAKE_CXX_FLAGS

g++编译选项

1
2
# 在CMAKE_CXX_FLAGS编译选项后追加-std=c++11
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
(8)CMAKE_BUILD_TYPE

编译类型(Debug, Release)

1
2
3
4
5
6
7
8
# 设定编译类型为debug,调试时需要选择debug
set(CMAKE_BUILD_TYPE Debug)

# 设定编译类型为release,发布时需要选择release
set(CMAKE_BUILD_TYPE Release)

# Debug:用于在没有优化的情况下,使用带有调试符号构建库或可执行文件。
# Release:用于构建的优化的库或可执行文件,不包含调试符号。

12.更改二进制保存路径

SET 指令重新定义 EXECUTABLE_OUTPUT_PATH LIBRARY_OUTPUT_PATH 变量 来指定最终的目标二进制的位置

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

哪里要改变目标存放路径,就在哪里加入上述的定义,所以应该在src下的CMakeLists.txt下写

13.安装

  • 一种是从代码编译后直接make install安装
  • 一种是打包时的指定 目录安装
    • 简单的可以这样指定目录:make install DESTDIR=/tmp/test
    • 稍微复杂一点可以这样指定目录:./configure –prefix=/usr

二、嵌套CMAKE

如果项目很大,或者项目中有很多的源码目录,在通过 CMake 管理项目的时候如果只使用一个 CMakeLists.txt,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个 CMakeLists.txt 文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护

项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ tree
.
├── build
├── calc
│   ├── add.cpp
│   ├── CMakeLists.txt
│   └── sub.cpp
├── CMakeLists.txt # 根目录下的 CMakeLists.txt
├── include
│   ├── calc.h
│   └── sort.h
├── sort
│   ├── CMakeLists.txt
│   ├── insert.cpp
│   └── select.cpp
├── test1
│   ├── calc.cpp
│   └── CMakeLists.txt
└── test2
├── CMakeLists.txt
└── sort.cpp

1.预备

(1)节点关系

Linux 的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。因此,我们需要了解一些关于 CMakeLists.txt 文件变量作用域的一些信息:

  • 根节点CMakeLists.txt中的变量全局有效
  • 父节点CMakeLists.txt中的变量可以在子节点中使用
  • 子节点CMakeLists.txt中的变量只能在当前节点中使用
(2)添加子目录

CMake中父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令:

1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir:指定了 CMakeLists.txt源文件和代码文件的位置,其实就是指定子目录
  • binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可
  • EXCLUDE_FROM_ALL:在子路径下的目标默认不会被包含到父路径的 ALL 目标里,并且也会被排除在 IDE 工程文件之外。用户必须显式构建在子路径下的目标

2.实施

在上面的目录中我们要做如下事情:

  • 通过 test1 目录中的测试文件进行计算器相关的测试

  • 通过test2目录中的测试文件进行排序相关的测试

现在相当于是要进行模块化测试,对于 calcsort 目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。

(1)根目录

根目录(最外层的)中的 CMakeLists.txt 文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmake_minimum_required(VERSION 3.16)
project(test)

# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)

# 头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 静态库的名字
set(CALC_LIB calc)
set(SORT_LIB sort)
# 可执行程序的名字
set(APP_NAME_1 test1)
set(APP_NAME_2 test2)
# 添加子目录
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)

在根节点对应的文件中主要做了两件事情:定义全局变量添加子目录

  • 定义的全局变量主要是给子节点使用,目的是为了提高子节点中的 CMakeLists.txt 文件的可读性和可维护性,避免冗余并降低出差的概率
  • 一共添加了四个子目录,每个子目录中都有一个 CMakeLists.txt 文件,这样它们的父子关系就被确定下来了
(2)calc目录

calc 目录中的CMakeLists.txt文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cmake_minimum_required(VERSION 3.16)
project(CALCLIB)

# 将当前目录下文件全部作为变量存储之 SRC 中
file(GLOB SRC ./)

# 包含头文件
include_directories(${HEAD_PATH})

# 重新定义目标链接库文件的存放位置
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

# 生成静态的库文件
add_library(${CALC_LIB} STATIC ${SRC})
  • file(GLOB SRC ./) 可以使用aux_source_directory(./ SRC)进行代替,搜索当前目录(calc)下的所有源文件,并且存储至 SRC变量中
  • include_directories(${HEAD_PATH}),包含头文件路径,HEAD_PATH是在根节点(最外层的CMakeLists.txt)文件中定义的
  • set(LIBRARY_OUTPUT_PATH ${LIB_PATH})中重新定义生成的库文件存放在的位置LIBRARY_OUTPUT_PATH,该程序中是生成静态库,LIB_PATH是在根节点(最外层的CMakeLists.txt)文件中定义的
  • add_library(${CALC_LIB} STATIC ${SRC}):生成静态库,静态库名字CALC_LIB是在根节点文件中定义的
(3)sort目录

sort 目录中的 CMakeLists.txt 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.16)

project(SORTLIB)

# 搜索该目录下的源文件 并将其存储之 SRC变量中
aux_source_directory(./ SRC)

# 包含头文件
include_directories(${HEAD_PATH})

# # 重新定义目标链接库文件的存放位置
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

# 生成动态库
add_library(${SORT_LIB} SHARED ${SRC})
  • add_library(${SORT_LIB} SHARED ${SRC}):生成动态库,动态库名字SORT_LIB是在根节点文件中定义的

  • 注意: 在生成库文件的时候,这个库可以是静态库也可以是动态库,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库

(4)test1目录

test1 目录中的CMakeLists.txt文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.16)

project(CALCTEST)

# 搜索该目录下的源文件 并将其存储之 SRC变量中
aux_source_directory(./ SRC)

# 包含头文件
include_directories(${HEAD_PATH})

# 链接静态库
link_libraries(${CALC_LIB})

# 重新定义目标二进制可执行文件的存放位置
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})

# 生成可执行文件
add_executable(${APP_NAME_1} ${SRC})
  • link_libraries(${CALC_LIB})链接静态库,其实对于静态库的链接也可以target_link_libraries(可执行文件名 静态库名)当然需要放到add_executable(${APP_NAME_1} ${SRC})之后
  • add_executable:生成可执行程序,APP_NAME_1 变量是在根节点文件中定义的
  • set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})重新定义目标二进制可执行文件的存放位置

注意: 此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了

(5)test2目录

test2 目录中的 CMakeLists.txt 文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.16)
project(SORTTEST)

# 搜索该目录下的源文件 并将其存储之 SRC变量中
aux_source_directory(./ SRC)

# 包含头文件
include_directories(${HEAD_PATH})

# 重新定义目标二进制可执行文件的存放位置
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
# link_directories(${LIB_PATH})

# 生成可执行文件
add_executable(${APP_NAME_2} ${SRC})

# 链接动态库
target_link_libraries(${APP_NAME_2} ${SORT_LIB})
  • link_directories:指定可执行程序要链接的动态库的路径,LIB_PATH 变量是在根节点文件中定义的
  • add_executable:生成可执行程序,APP_NAME_2 变量是在根节点文件中定义的
  • target_link_libraries:指定可执行程序要链接的动态库的名字

注意: 在生成可执行程序的时候,动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库