CMake 保姆级教程(上)

目录
CMake 保姆级教程(上)
CMake 保姆级教程(下)

1. CMake概述

CMake 是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有Makefile(通过 make 命令进行项目的构建),大多是IDE软件都集成了make,比如:VS 的 nmake、linux 下的 GNU make、Qt 的 qmake等,如果自己动手写 makefile,会发现,makefile 通常依赖于当前的编译平台,而且编写 makefile 的工作量比较大,解决依赖关系时也容易出错。

而 CMake 恰好能解决上述问题, 其允许开发者指定整个工程的编译流程,在根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需make编译即可,所以可以把CMake看成一款自动生成 Makefile的工具,其编译流程如下图:
在这里插入图片描述

  • 蓝色虚线表示使用makefile构建项目的过程
  • 红色实线表示使用cmake构建项目的过程
  • 介绍完CMake的作用之后,再来总结一下它的优点:

跨平台

  • 能够管理大型项目
  • 简化编译构建过程和编译过程
  • 可扩展:可以为 cmake 编写特定功能的模块,扩充 cmake 功能

2. CMake的使用

CMake支持大写、小写、混合大小写的命令。如果在编写CMakeLists.txt文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。

2.1 注释

2.1.1 注释行#

CMake 使用 # 进行行注释,可以放在任何位置。

# 这是一个 CMakeLists.txt 文件
cmake_minimum_required(VERSION 3.0.0)

2.1.2 注释块#[[ ]]

CMake 使用 #[[ ]] 形式进行块注释。

#[[ 这是一个 CMakeLists.txt 文件。
这是一个 CMakeLists.txt 文件
这是一个 CMakeLists.txt 文件]]
cmake_minimum_required(VERSION 3.0.0)

2.2 只有源文件

2.2.1 共处一室

准备工作,为了方便测试,在我本地电脑准备了这么几个测试文件

  • add.c
#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
    return a+b;
}
  • sub.c
#include <stdio.h>
#include "head.h"

// 你好
int subtract(int a, int b)
{
    return a-b;
}
  • mult.c
#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
    return a*b;
}
  • div.c
#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
    return (double)a/b;
}
  • head.h
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif
  • main.c
#include <stdio.h>
#include "head.h"

int main()
{
    int a = 20;
    int b = 12;
    printf("a = %d, b = %d\n", a, b);
    printf("a + b = %d\n", add(a, b));
    printf("a - b = %d\n", subtract(a, b));
    printf("a * b = %d\n", multiply(a, b));
    printf("a / b = %f\n", divide(a, b));
    return 0;
}

上述文件的目录结构如下:

$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c

3.添加 CMakeLists.txt 文件

在上述源文件所在目录下添加一个新文件 CMakeLists.txt,文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)

接下来依次介绍一下在 CMakeLists.txt 文件中添加的三个命令:

  • cmake_minimum_required:指定使用的 cmake 的最低版本

    • 可选,非必须,如果不加可能会有警告
  • project:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。

# PROJECT 指令的语法是:
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
       [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
       [DESCRIPTION <project-description-string>]
       [HOMEPAGE_URL <url-string>]
       [LANGUAGES <language-name>...])
add_executable:定义工程会生成一个可执行程序
add_executable(可执行程序名 源文件名称)
  • 这里的可执行程序名和project中的项目名没有任何关系

  • 源文件名可以是一个也可以是多个,如有多个可用空格或;间隔

# 样式1
add_executable(app add.c div.c main.c mult.c sub.c)
# 样式2
add_executable(app add.c;div.c;main.c;mult.c;sub.c)

4.执行CMake 命令

万事俱备只欠东风,将 CMakeLists.txt 文件编辑好之后,就可以执行 cmake命令了。

# cmake 命令原型
$ cmake CMakeLists.txt文件所在路径
$ tree
.
├── add.c
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c

0 directories, 7 files
robin@OS:~/Linux/3Day/calc$ cmake .

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

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

$ tree -L 1
.
├── add.c
├── CMakeCache.txt         # new add file
├── CMakeFiles             # new add dir
├── cmake_install.cmake    # new add file
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── Makefile               # new add file
├── mult.c
└── sub.c

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

$ 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

# 查看可执行程序是否已经生成
$ tree -L 1
.
├── add.c
├── app					# 生成的可执行程序
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── Makefile
├── mult.c
└── sub.c

最终可执行程序app就被编译出来了(这个名字是在CMakeLists.txt中指定的)。

2.2.2 VIP 包房

通过上面的例子可以看出,如果在CMakeLists.txt文件所在目录执行了cmake命令之后就会生成一些目录和文件(包括 makefile 文件),如果再基于makefile文件执行make命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样会导致整个项目目录看起来很混乱,不太容易管理和维护,此时我们就可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比如将这个目录命名为build:

$ mkdir build
$ cd build
$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/Linux/build

现在cmake命令是在build目录中执行的,但是CMakeLists.txt文件是build目录的上一级目录中,所以cmake 命令后指定的路径为..,即当前目录的上一级目录。

当命令执行完毕之后,在build目录中会生成一个makefile文件

$ tree build -L 1
build
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
└── Makefile

1 directory, 3 files

这样就可以在build目录中执行make命令编译项目,生成的相关文件自然也就被存储到build目录中了。这样通过cmakemake生成的所有文件就全部和项目源文件隔离开了,各回各家,各找各妈。

2.2 私人订制

2.2.1 定义变量: SET

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

# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
  • VAR:变量名
  • VALUE:变量值
# 方式1: 各个源文件之间使用空格间隔
# set(SRC_LIST add.c  div.c   main.c  mult.c  sub.c)

# 方式2: 各个源文件之间使用分号 ; 间隔
set(SRC_LIST add.c;div.c;main.c;mult.c;sub.c)
add_executable(app  ${SRC_LIST})

2.2.2 指定使用的C++标准: DCMAKE_CXX_STANDARD

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

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

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

1.在 CMakeLists.txt 中通过 set 命令指定

#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)

2.在执行 cmake 命令的时候指定出这个宏的值

#增加-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

在上面例子中 CMake 后的路径需要根据实际情况酌情修改。

2.2.3 指定输出的路径: EXECUTABLE_OUTPUT_PATH

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

set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
  • 第一行:定义一个变量用于存储一个绝对路径
  • 第二行:将拼接好的路径值设置给EXECUTABLE_OUTPUT_PATH宏
    • 如果这个路径中的子目录不存在,会自动生成,无需自己手动创建

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

2.3 搜索文件

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

2.3.1 aux_source_directory语法

在 CMake 中使用aux_source_directory 命令可以查找某个路径下的所有源文件,命令格式为:

aux_source_directory(< dir > < variable >)
  • dir:要搜索的目录
  • variable:将从dir目录下搜索到的源文件列表存储到该变量中
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 搜索 src 目录下的源文件
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(app  ${SRC_LIST})

2.3.2 file语法

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以,在CMake中为我们提供了搜索文件的命令,他就是file(当然,除了搜索以外通过 file 还可以做其他事情)

file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
  • GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
  • GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
    搜索当前目录的src目录下所有的源文件,并存储到变量中
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 文件所在的路径。

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

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

2.3.3 find_package 语法

find_package 寻找系统中安装的第三方库并链接他们。

find_package 语法
常用参数列表一览:

find_package(<PackageName> [version] [EXACT] [QUIET] [CONFIG] [MODULE]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
)

https://cmake.org/cmake/help/latest/command/find_package.html
find_package 命令用法举例

  • find_package(OpenCV)

    • 查找名为 OpenCV 的包,找不到不报错,事后可以通过 ${OpenCV_FOUND}查询是否找到。
  • find_package(OpenCV QUIET)

    • 查找名为 OpenCV 的包,找不到不报错,也不打印任何信息。
  • find_package(OpenCV REQUIRED) # 最常见用法

    • 查找名为 OpenCV 的包,找不到就报错(并终止 cmake 进程,不再继续往下执行)。
  • find_package(OpenCV REQUIRED COMPONENTS core videoio)

    • 查找名为 OpenCV 的包,找不到就报错,且必须具有 OpenCV::coreOpenCV::videoio这两个组件,如果没有这两个组件也会报错。
  • find_package(OpenCV REQUIRED OPTIONAL_COMPONENTS core videoio)
    查找名为 OpenCV 的包,找不到就报错,可具有 OpenCV::core OpenCV::videoio这两个组件,没有这两组件不会报错,通过 ${OpenCV_core_FOUND}查询是否找到 core 组件。

find_package 说是找“包(package)”,到底是在找什么?

寻找包配置文件
find_package(OpenCV) 实际上是在找一个名为 OpenCVConfig.cmake的文件。

注:出于历史兼容性考虑,除了 OpenCVConfig.cmake 以外 OpenCV-config.cmake 这个文件名也会被 CMake 识别到。

同理,find_package(Qt5)则是会去找名为 Qt5Config.cmake的文件。

这些形如 包名 + Config.cmake 的文件,我称之为包配置文件。

Qt5Config.cmake 是在安装 Qt5 时,随 libQt5Core.so 等实际的库文件,一起装到系统中去的。

以 Arch Linux 系统为例:

  • 包配置文件位于 /usr/lib/cmake/Qt5/Qt5Config.cmake。
  • 实际的动态库文件位于 /usr/lib/libQt5Core.so。

以 Ubuntu 22.04 系统为例:

  • 包配置文件位于/usr/lib/x86_64-linux-gnu/cmake/Qt5/Qt5Config.cmake
  • 实际的动态库文件位于 /usr/lib/x86_64-linux-gnu/libQt5Core.so

包配置文件包含什么?

因此 find_package 不是直接去找具体的动态库文件和头文件(例如 libQt5Core.so)。而是去找包配置文件(例如Qt5Config.cmake),这个配置文件里包含了包的具体信息,包括动态库文件的位置,头文件的目录,链接时需要开启的编译选项等等。

而且某些库都具有多个子动态库,例如 Qt 就有 libQt5Core.so、libQt5Widgets.so、libQt5Network.so。因此 CMake 要求所有第三方库作者统一包装成一个 Qt5Config.cmake 文件包含所有相关信息(类似于 nodejs 的 package.json),比你单独的一个个去找动态库文件要灵活的多。

在XXXConfig.cmake文件中,库的路径都是相对路径,比如说…/…/libXXX.so,而不是绝对路径/usr/lib/libXXX.so…
这样做可以让CMake更容易找到对应的动态库,因为不一定每个人的系统库都是安装在/usr/lib/…下!比如Arch Linux和Ubuntu的库安装路径就不一样!因此相对路径会更加方便。

包配置文件怎么来的?

  • 包配置文件由第三方库的作者(Qt的开发团队)提供,在这个库安装时(Qt的安装程序或apt install等)会自动放到 /usr/lib/cmake/XXX/XXXConfig.cmake这个路径(其中XXX是包名),供 CMake 用户找到并了解该包的具体信息。

  • /usr/lib/cmake 这个位置是 CMake 和第三方库作者约定俗成的,由第三方库的安装程序负责把包配置文件放到这里。如果第三方库的作者比较懒,没提供 CMake 支持(由安装程序提供XXXConfig.cmake),那么得用另外的一套方法(FindXXX.cmake),稍后细谈。
    Windows 系统下的搜索路径

  • <prefix>/

  • <prefix>/cmake/

  • <prefix>/<name>*/

  • <prefix>/<name>*/cmake/

  • <prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/

  • <prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/

  • <prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/cmake/
    其中:

  • <prefix> 是变量 ${CMAKE_PREFIX_PATH},Windows 平台默认为 C:/Program Files

  • <name> 是你在 find_package(<name> REQUIRED) 命令中指定的包名。

  • <arch> 是系统的架构名。
    Unix 类系统下的搜索路径

<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/
<prefix>/(lib/<arch>|lib*|share)/<name>*/
<prefix>/(lib/<arch>|lib*|share)/<name>*/cmake/
<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/cmake/

其中:

  • <prefix> 变量 ${CMAKE_PREFIX_PATH},Unix 平台默认为 /usr。
  • <name> 是你在 find_package(<name> REQUIRED) 命令中指定的包名。
  • <arch> 是系统的架构,例如x86_64-linux-gnu i386-linux-gnu
    (用于伺候 Ubuntu 喜欢把库文件套娃在 /usr/lib/x86_64-linux-gnu 目录下)

举例说明 find_package 搜索路径

例如你是 64 位的 Linux 系统,·find_package(Qt5 REQUIRED) ·会依次搜索:

/usr/lib/cmake/Qt5/Qt5Config.cmake
/usr/lib/x86_64-linux-gnu/cmake/Qt5/Qt5Config.cmake
/usr/share/cmake/Qt5/Qt5Config.cmake
/usr/lib/Qt5/Qt5Config.cmake
/usr/lib/x86_64-linux-gnu/Qt5/Qt5Config.cmake
/usr/share/Qt5/Qt5Config.cmake
/usr/Qt5/lib/cmake/Qt5/Qt5Config.cmake
/usr/Qt5/lib/x86_64-linux-gnu/cmake/Qt5/Qt5Config.cmake
/usr/Qt5/share/cmake/Qt5/Qt5Config.cmake
/usr/Qt5/lib/Qt5/Qt5Config.cmake
/usr/Qt5/lib/x86_64-linux-gnu/Qt5/Qt5Config.cmake
/usr/Qt5/share/Qt5/Qt5Config.cmake

例如你是 64 位的 Windows 系统,find_package(Qt5 REQUIRED) 会依次搜索:

C:/Program Files/Qt5Config.cmake
C:/Program Files/cmake/Qt5Config.cmake
C:/Program Files/Qt5/Qt5Config.cmake
C:/Program Files/Qt5/cmake/Qt5Config.cmake
C:/Program Files/Qt5/lib/cmake/Qt5/Qt5Config.cmake
C:/Program Files/Qt5/lib/x86_64-windows-gnu/cmake/Qt5/Qt5Config.cmake
C:/Program Files/Qt5/share/cmake/Qt5/Qt5Config.cmake
C:/Program Files/Qt5/lib/Qt5/Qt5Config.cmake
C:/Program Files/Qt5/lib/x86_64-windows-gnu/Qt5/Qt5Config.cmake
C:/Program Files/Qt5/share/Qt5/Qt5Config.cmake

还有一点,<name> 可以有额外后缀,且不分大小写(无论 Linux 还是 Windows),例如在Windows系统中:```

C:/Program Files/Qt5/cmake/Qt5Config.cmake
C:/Program Files/Qt5.12.1/cmake/Qt5Config.cmake
C:/Program Files/qt5dnmd/cmake/Qt5Config.cmake

都是可以被 find_package(Qt5 REQUIRED) 搜索到的。

在 Linux 系统中:```

/usr/lib/cmake/OpenCV/OpenCVConfig.cmake
/usr/lib/cmake/opencv4/OpenCVConfig.cmake

都是可以被 find_package(OpenCV REQUIRED) 搜索到的。

安装在非标准路径的库如何处理?

以 Qt5 为例,如果你安装在下列标准路径,find_package 能够自动找到。

  • Windows:C:/Program Files/Qt5.12.1/lib/cmake/Qt5/Qt5Config.cmake。
  • Linux:/usr/lib/cmake/Qt5/Qt5Config.cmake。

但是假如我的库不是装在这些标准路径,而是我自定义的路径,怎么办? 而且即使你不自定义安装路径,Windows 版的 Qt 默认安装就会安装到:

C:/Qt5.12.1/msvc2017_64/lib/cmake/Qt5/Qt5Config.cmake

何况我们同学有的还喜欢装到 D 盘去,Windows 是非标准路径的重灾区,他就没有一个统一的 /usr/lib 目录。然而你一旦把库安装到非标准路径,find_package 是找不到的。

这时你需要手动指定一个变量告诉他在哪儿,可以是普通变量 ${Qt5_DIR},也可以是环境变量 $ENV{Qt5_DIR},两个中只要设置了任何一个 find_package 都可以识别到。 变量一般通过命令行 -DQt5_DIR=”C:/Program Files/Qt5.12.1/lib/cmake/Qt5” 设置。

举例,Windows 系统,Qt5

例如我把 Qt5 安装到了 D:/Qt5.12.1。 首先找到他里面的 Qt5Config.cmake 文件所在位置(可以用文件管理器的“搜索”功能或者everything)。

假如你找到该文件的位置是 D:/Qt5.12.1/msvc2017/lib/cmake/Qt5/Qt5Config.cmake,那么请你设置变量 Qt5_DIR 为 D:/Qt5.12.1/msvc2017/lib/cmake/Qt5。有三种设置方法:

单次有效。 configure 阶段,可以从命令行设置(注意要加引号): cmake -B build -DQt5_DIR=”D:/Qt5.12.1/msvc2017/lib/cmake/Qt5”
全局启用。 右键“我的电脑”->“管理”->“高级”添加一个环境变量 Qt5_DIR 值为 D:/Qt5.12.1/msvc2017/lib/cmake/Qt5,然后重启 Visual Studio。这样以后你每次构建任何项目,find_package 都能自动找到这个路径的 Qt5 包了。
单项目有效。 直接在你自己项目的 CMakeLists.txt 最开头写一行(注意要加引号): set(Qt5_DIR ”D:/Qt5.12.1/msvc2017/lib/cmake/Qt5”) # 一定要加在最前面!

举例,Linux 系统,Qt5

例如我把 Qt5 安装到了 /opt/Qt5.12.1。 首先找到他里面的 Qt5Config.cmake 文件所在位置(可以用文件管理器的“搜索”功能)。 假如你找到该文件的位置是 /opt/Qt5.12.1/lib/cmake/Qt5/Qt5Config.cmake,那么请你设置变量 Qt5_DIR 为 /opt/Qt5.12.1/lib/cmake/Qt5。有三种设置方法:

单次有效。 在 configure 阶段,可以从命令行设置: cmake -B build -DQt5_DIR=”/opt/Qt5.12.1/lib/cmake/Qt5”
全局启用。 修改你的 ~/.bashrc 文件添加环境变量: export Qt5_DIR=”/opt/Qt5.12.1/lib/cmake/Qt5”,然后重启终端。这样以后你每次构建任何项目,find_package 都能自动找到这个路径的 Qt5 包了。
单项目有效。 直接在你自己项目的 CMakeLists.txt 最开头写一行: set(Qt5_DIR ”/opt/Qt5.12.1/lib/cmake/Qt5”) # 一定要加在最前面!

三种方案利弊分析

  • 单次有效(通过命令行)最安全,高度推荐。
  • 全局有效(添加环境变量)可能影响以后其他项目。
    比如你的 A 项目依赖 Qt5.12.1,你设置了环境变量 Qt5_DIR=/opt/Qt5.12.1,后来又搞了个 B 项目依赖 Qt5.10.3,但是你忘了你设置过全局的环境变量指向 5.12.1 了,导致版本冲突。
  • 单项目有效(写死在 CMakeLists.txt)虽然方便了你,但是你的 CMakeLists.txt 拿到别人电脑上就冲突了
    (例如你通过 GitHub 开源的),可能你 set(Qt5_DIR D:/Qt5),而人家却需要 set(Qt5_DIR E:/Qt5) 。
    所以“单次有效”虽然劳驾您的高抬贵手每次命令行打一下 -DQt5_DIR=”D:/Qt5”,但人家也打一下 -DQt5_DIR=”E:/Qt5”,就没有冲突,各美其美,美美与共,赋能多元化社会,下沉团队合作发力面。

实际上只要你不删 build,不需要每次都 -DQt5_DIR 一下,CMake 具有“记忆”功能。

  • cmake -B build -DQt5_DIR=D:/Qt5# 只需要第一次指定好
  • cmake -B build # 以后第二次运行可以省略!
  • rm -rf build# 只有清理了 build 以后,
  • cmake -B build -DQt5_DIR=D:/Qt5# 才需要重新指定。

2.4 包含头文件: include_directories

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

include_directories(headpath)

举例说明,有源文件若干,其目录结构如下:

$ tree
.
├── build
├── CMakeLists.txt
├── include
│   └── head.h
└── src
    ├── add.cpp
    ├── div.cpp
    ├── main.cpp
    ├── mult.cpp
    └── sub.cpp

3 directories, 7 files

CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(CALC)
set(CMAKE_CXX_STANDARD 11)
set(HOME /home/robin/Linux/calc)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin/)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_executable(app  ${SRC_LIST})

其中,第六行指定就是头文件的路径,PROJECT_SOURCE_DIR宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录。

2.5 制作动态库或静态库

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

2.5.1 制作静态库: add_library

在cmake中,如果要制作静态库,需要使用的命令如下:

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

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

在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。

下面有一个目录,需要将src目录中的源文件编译成静态库,然后再使用:

.
├── build
├── CMakeLists.txt
├── include           # 头文件目录
│   └── head.h
├── main.cpp          # 用于测试的源文件
└── src               # 源文件目录
    ├── add.cpp
    ├── div.cpp
    ├── mult.cpp
    └── sub.cpp

根据上面的目录结构,可以这样编写CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
add_library(calc STATIC ${SRC_LIST})

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

2.5.2 制作动态库: add_library

在cmake中,如果要制作动态库,需要使用的命令如下:

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

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

在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。

根据上面的目录结构,可以这样编写CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
add_library(calc SHARED ${SRC_LIST})

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

2.5.3 指定输出的路径

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

cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
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 - 都适用: LIBRARY_OUTPUT_PATH
由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用EXECUTABLE_OUTPUT_PATH宏了,而应该使用LIBRARY_OUTPUT_PATH这个宏对应静态库文件和动态库文件都适用

cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
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})

2.5.4 Object Libraries

在编译项目的时候,我们还可能就是将源文件编译成 .o 的中间文件来供其他项目使用, 而不是真的将我们的源文件一次性就编译成库文件或者可执行文件, 这个时候,也可以使用 add_library 来创建一个 OBJECT 项目。

OBJECT 项目的构建和使用方法如下:

add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)
add_library(archiveExtras STATIC $<TARGET_OBJECTS:archive> extras.cpp)
add_executable(test_exe $<TARGET_OBJECTS:archive> test.cpp)

第一句 add_library命令就是使用 OBJECT 关键字来构建名字为 archive 的 OBJECT 项目
下面的 $<TARGET_OBJECTS:archive> 生成语句就是对 archiveOBJECT项目进行使用, TARGET_OBJECTS 会罗列出所有的 archive 的文件。

我们也可以使用 target_link_libraries 来建立其他项目和 OBJECT项目的依赖关系。

使用方法如下所示:

add_library(archive OBJECT archive.cpp zip.cpp lzma.cpp)

add_library(archiveExtras STATIC extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)

add_executable(test_exe test.cpp)
target_link_libraries(test_exe archive)

这里的 Object 中间项目是不能用于 add_custom_command TARGET 定义的依赖, 但是还是可以用 $<TARGET_OBJECTS:archive> 的方式用于 add_custom_commandOUTPUT定义和 fileGENERATE 定义。

2.6 包含库文件

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

2.6.1 链接静态库: link_directories

src
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

现在我们把上面src目录中的add.cpp、div.cpp、mult.cpp、sub.cpp编译成一个静态库文件libcalc.a。通过命令制作并使用静态链接库

测试目录结构如下:

$ tree 
.
├── build
├── CMakeLists.txt
├── include
│   └── head.h
├── lib
│   └── libcalc.a     # 制作出的静态库的名字
└── src
    └── main.cpp

4 directories, 4 files

在cmake中,链接静态库的命令如下:

link_libraries(<static lib> [<static lib>...])
  • 参数1:指定出要链接的静态库的名字
    可以是全名 libxxx.a
    也可以是掐头(lib)去尾(.a)之后的名字 xxx
  • 参数2-N:要链接的其它静态库的名字
    如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:
link_directories(<lib path>)

这样,修改之后的CMakeLists.txt文件内容如下:

cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 链接静态库
link_libraries(calc)
add_executable(app ${SRC_LIST})

添加了第8行的代码,就可以根据参数指定的路径找到这个静态库了。

2.6.2 链接动态库: target_link_libraries

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

cmake中链接动态库的命令如下:

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中定义的方法。

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

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

  • INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号。
    链接系统动态库
    动态库的链接和静态库是完全不同的:

  • 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。

  • 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存

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

cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 添加并指定最终生成的可执行程序名
add_executable(app ${SRC_LIST})
# 指定可执行程序要链接的动态库名字
target_link_libraries(app pthread)

target_link_libraries(app pthread)中:

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

链接第三方动态库: link_directories
现在,自己生成了一个动态库,对应的目录结构如下:

$ tree 
.
├── build
├── CMakeLists.txt
├── include
│   └── head.h            # 动态库对应的头文件
├── lib
│   └── libcalc.so        # 自己制作的动态库文件
└── main.cpp              # 测试用的源文件

3 directories, 4 files

假设在测试文件main.cpp中既使用了自己制作的动态库libcalc.so又使用了系统提供的线程库,此时CMakeLists.txt文件可以这样写:

cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(app ${SRC_LIST})
target_link_libraries(app pthread calc)

在第六行中,pthreadcalc都是可执行程序app要链接的动态库的名字。当可执行程序app生成之后并执行该文件,会提示有如下错误信息:

$ ./app 
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

这是因为可执行程序启动之后,去加载calc这个动态库,但是不知道这个动态库被放到了什么位置解决动态库无法加载的问题,所以就加载失败了,在 CMake 中可以在生成可执行程序之前,通过命令指定出要链接的动态库的位置,指定静态库位置使用的也是这个命令:

link_directories(path)

所以修改之后的CMakeLists.txt文件应该是这样的:

cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 指定源文件或者动态库对应的头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 指定要链接的动态库的路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 添加并生成一个可执行程序
add_executable(app ${SRC_LIST})
# 指定要链接的动态库
target_link_libraries(app pthread calc)

通过link_directories指定了动态库的路径之后,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题了。

温馨提示:使用 target_link_libraries 命令就可以链接动态库,也可以链接静态库文件。

2.7 日志: message

在CMake中可以用用户显示一条消息,该命令的名字为message

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

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

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

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

2.8 变量操作

2.8.1 追加

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

使用set拼接
如果使用set进行字符串拼接,对应的命令格式如下:

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

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

cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
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} ${TEMP})
message(STATUS "message: ${SRC_1}")

使用list拼接
如果使用list进行字符串拼接,对应的命令格式如下:

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

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

cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
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} ${TEMP})
message(STATUS "message: ${SRC_1}")

在CMake中,使用set命令可以创建一个list。一个在list内部是一个由分号;分割的一组字符串。例如,set(var a b c d e)命令将会创建一个list:a;b;c;d;e,但是最终打印变量值的时候得到的是abcde

set(tmp1 a;b;c;d;e)
set(tmp2 a b c d e)
message(${tmp1})
message(${tmp2})

输出的结果:

abcde
abcde

2.8.2 字符串移除

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

$ tree
.
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp

0 directories, 5 files

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

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

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

cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/*.cpp)
# 移除前日志
message(STATUS "message: ${SRC_1}")
# 移除 main.cpp
list(REMOVE_ITEM SRC_1 ${PROJECT_SOURCE_DIR}/main.cpp)
# 移除后日志
message(STATUS "message: ${SRC_1}")

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

关于list命令还有其它功能,但是并不常用,在此就不一一进行举例介绍了。

获取 list 的长度。

list(LENGTH <list> <output variable>)
  • LENGTH:子命令LENGTH用于读取列表长度
  • <list>:当前操作的列表
  • <output variable>:新创建的变量,用于存储列表的长度。

读取列表中指定索引的的元素,可以指定多个索引

list(GET <list> <element index> [<element index> ...] <output variable>)

<list>:当前操作的列表
<element index>:列表元素的索引

  • 从0开始编号,索引0的元素为列表中的第一个元素;
  • 索引也可以是负数,-1表示列表的最后一个元素,-2表示列表倒数第二个元素,以此类推
    当索引(不管是正还是负)超过列表的长度,运行会报错
  • <output variable>:新创建的变量,存储指定索引元素的返回结果,也是一个列表。

3.将列表中的元素用连接符(字符串)连接起来组成一个字符串

list (JOIN <list> <glue> <output variable>)
  • <list>:当前操作的列表
  • <glue>:指定的连接符(字符串)
  • <output variable>:新创建的变量,存储返回的字符串

4.查找列表是否存在指定的元素,若果未找到,返回-1

list(FIND <list> <value> <output variable>)
  • <list>:当前操作的列表
  • <value>:需要再列表中搜索的元素
  • <output variable>:新创建的变量
    • 如果列表<list>中存在<value>,那么返回<value>在列表中的索引
    • 如果未找到则返回-1。
  1. 将元素追加到列表中
list (APPEND <list> [<element> ...])

6.在list中指定的位置插入若干元素

list(INSERT <list> <element_index> <element> [<element> ...])
  1. 将元素插入到列表的0索引位置
list (PREPEND <list> [<element> ...])
  1. 将列表中最后元素移除
list (POP_BACK <list> [<out-var>...])
  1. 将列表中第一个元素移除
list (POP_FRONT <list> [<out-var>...])
  1. 将指定的元素从列表中移除
list (REMOVE_ITEM <list> <value> [<value> ...])
  1. 将指定索引的元素从列表中移除
list (REMOVE_AT <list> <index> [<index> ...])
  1. 移除列表中的重复元素
list (REMOVE_DUPLICATES <list>)
  1. 列表翻转
list(REVERSE <list>)
  1. 列表排序
list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
  • COMPARE:指定排序方法。有如下几种值可选:
    • STRING:按照字母顺序进行排序,为默认的排序方法
    • FILE_BASENAME:如果是一系列路径名,会使用basename进行排序
    • NATURAL:使用自然数顺序排序
  • CASE:指明是否大小写敏感。有如下几种值可选:
    • SENSITIVE: 按照大小写敏感的方式进行排序,为默认值
    • INSENSITIVE:按照大小写不敏感方式进行排序
  • ORDER:指明排序的顺序。有如下几种值可选:
    • ASCENDING:按照升序排列,为默认值
    • DESCENDING:按照降序排列

2.9 宏定义: add_definitions 和 -D

在 CMake 中,-D 选项用于定义预处理器宏。当你在命令行中使用 CMake 或 cmake --build 时,可以通过 -D 来传递宏定义。这些宏定义可以在 CMakeLists.txt 文件中使用,或者通过 add_definitions() 命令添加到编译器中。

下面是如何定义和使用 -D 的简单示例:

1. 在命令行中定义

假设你有如下的 CMakeLists.txt 文件:

cmake
cmake_minimum_required(VERSION 3.10)  
project(MyProject)  
  
add_executable(my_app main.cpp)  
  
# 使用预定义的宏  
if(MY_MACRO)  
    message("MY_MACRO is defined!")  
else()  
    message("MY_MACRO is not defined!")  
endif()
你可以这样使用 -D 选项来定义 MY_MACRO:

命令行运行如下:

cmake -DMY_MACRO=ON ..

然后,CMake 会输出 “MY_MACRO is defined!”。

2. 在 CMakeLists.txt 中使用

如果你想要在 CMakeLists.txt 中根据这个宏做某些操作,在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示:

#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;
}

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

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

$ gcc test.c -DDEBUG -o app

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

CMake中我们也可以做类似的事情,对应的命令叫做add_definitions:

add_definitions(-D宏名称)

针对于上面的源文件编写一个CMakeLists.txt,内容如下:

cmake_minimum_required(VERSION 3.0)
project(TEST)
# 自定义 DEBUG 宏
add_definitions(-DDEBUG)
add_executable(app ./test.c)

通过这种方式,上述代码中的第八行日志就能够被输出出来了。

执行命令:
1.mkdir build
2.cd build
3.cmake ..
4.make
5./app
 
执行结果:
heheda@linux:~/Linux/loveDBTeacher-v6$ mkdir build
heheda@linux:~/Linux/loveDBTeacher-v6$ cd build
heheda@linux:~/Linux/loveDBTeacher-v6/build$ cmake ..
-- The C compiler identification is GNU 7.5.0
-- The CXX compiler identification is GNU 7.5.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/heheda/Linux/loveDBTeacher-v6/build
heheda@linux:~/Linux/loveDBTeacher-v6/build$ make
Scanning dependencies of target app
[ 50%] Building C object CMakeFiles/app.dir/test.c.o
[100%] Linking C executable app
[100%] Built target app
heheda@linux:~/Linux/loveDBTeacher-v6/build$ ./app
我是一个程序猿,我不会爬树...
Hello,I am Heheda
Hello,I am Heheda
Hello,I am Heheda
heheda@linux:~/Linux/loveDBTeacher-v6/build$

3. 预定义宏

下面的列表中为大家整理了一些CMake中常用的宏:

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

2.10 Debug配置文件编写

基础配置样例如下:

cmake_minimum_required(VERSION 3.10)  
project(MyProject)  
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0 -Wall")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
add_executable(app main.cpp)  

在 CMake 中,CMAKE_CXX_FLAGS_DEBUG 是一个特殊的变量,用于指定在构建 Debug 版本的 C++ 目标时应该使用的编译器标志(flags)。这些编译器标志通常用于增加调试信息、禁用优化等,以便在调试过程中更容易地跟踪和解决问题。

下面是一些可能包含在 CMAKE_CXX_FLAGS_DEBUG 中的常见编译器标志及其含义:

  • -g 或 -ggdb:生成调试信息。这允许调试器(如 GDB)在运行时访问源代码级别的信息,如变量名、函数名、行号等。
  • -O0:禁用优化。优化可能会改变代码的执行顺序或删除某些变量,这可能会使调试变得更加困难。通过禁用优化,你可以确保调试器看到的代码与源代码更加接近。
  • -Wall:启用所有警告。这可以帮助你发现潜在的问题,如未使用的变量、类型不匹配等。
  • -Wextra:启用额外的警告。这些警告可能不是所有开发者都认为是有用的,但它们可以提供额外的信息,帮助你发现可能的错误。
  • -pedantic:启用严格的 ISO C++ 兼容性检查。这可以帮助你确保代码符合 C++ 标准。
  • -D_DEBUG 或其他宏定义:定义特定的宏,以便在代码中启用或禁用某些调试相关的功能。
    你可以在 CMakeLists.txt 文件中设置 CMAKE_CXX_FLAGS_DEBUG 变量,如下所示:
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0 -Wall -Wextra -pedantic -D_DEBUG")

请注意,上面的示例中使用了 ${CMAKE_CXX_FLAGS_DEBUG} 来引用现有的 Debug 编译器标志(如果有的话),并添加了新的标志。这可以确保你的设置不会覆盖其他 CMake 脚本或用户可能已经设置的任何标志。

另外,你也可以在命令行中通过 -DCMAKE_CXX_FLAGS_DEBUG="your_flags_here" 的方式覆盖 CMakeLists.txt 文件中设置的 CMAKE_CXX_FLAGS_DEBUG 变量。这可以在执行 cmake 命令时动态地指定不同的编译器标志。

参考资料
https://subingwen.cn/cmake/CMake-primer/
https://www.bilibili.com/video/BV14s4y1g7Zj?p=1&vd_source=63c3682e66febb42e6a271165dd5a13e
https://zhuanlan.zhihu.com/p/631259689
https://blog.csdn.net/u012939880/article/details/104007959

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值