华南虎视觉组主页华南虎视觉组主页
首页
项目
通用
教程
Gitea
首页
项目
通用
教程
Gitea
  • 现代 CMake 教程

    • 【01】安装与基本介绍
    • 【02】CMake 的文件分布
    • 【03】变量的设置与引用
    • 【04】运算符与条件、循环语句
    • 【05】目标构建
      • 1. 前期准备
        • 1.1 编译的 4 个步骤
        • 1.2 回顾 CMake 的地位
        • 1.3 动态库 / 静态库
      • 2. 目标构建
        • 2.1 回顾:可执行文件构建
        • 2.2 普通库目标构建
        • 2.3 接口库目标构建
        • 2.4 导入目标构建
      • 3. 补充内容
        • 3.1 其余目标库
        • 3.2 设置目标属性
        • 3.3 非必要不使用全局包含、链接
        • 3.4 访问库的方式
    • 【06】变量参与 C++ 程序的编译
    • 【07】宏与函数
    • 【08】find_package 详解
    • 【09】生成器表达式
    • 【10】项目的导出与安装
    • 【11】项目实战
    • 【12】CTest
    • 【13】CPack

【05】目标构建

1. 前期准备

在开始之前,我们先了解一些基本的知识并回顾【01】安装与基本介绍的内容作为前期准备

1.1 编译的 4 个步骤

编译型语言编译一般都有 4 个步骤,分别是:,这里的编译仅指将源代码翻译成汇编代码。而我们平时称的编译则指将源代码翻译成可执行文件的整个过程。Ubuntu 下常见的编译工具是 Unix Makefiles,我们在终端输入

make

的时候,用到的就是 Unix Makefiles。而所有的编译工具均会根据指定的或者默认的环境变量,例如在 Makefile 文件中,可能可以找到类似

CC = gcc
CXX = g++

的内容,从而选择指定的编译器。编译器则根据编译工具所制定的规则对一系列源码进行解析、编译、链接,例如可以在 Makefile 文件中指定

CFLAGS = -g -O2

并使用类似

$(CC) $(CFLAGS)

的语句完成对编译器相关编译选项的设置,最终在编译时生成对应的符合要求的目标(可执行文件、动态库、静态库)

1.2 回顾 CMake 的地位

cmake-layer

CMake 作为一个跨平台的编译工具,能够使用通用的语法(组态档)生成编译工具(包括Unix Makefiles、Ninja、Visual Studio……)能够解析的文件(建构档),从而完成对项目的构建。

1.3 动态库 / 静态库

1.3.1 是什么

在 Windows 下,经常会看到扩展名为.dll的文件,称为动态链接库Dynamic-Link Library。同样,在Ubuntu下/usr/local/lib目录下也能看到一系列名为*.so的文件,称为共享对象Shared Object,这一类都属于动态库。

另外,在 Windows 下也有部分扩展名为*.lib的文件,Ubuntu 下也许能在/usr/local/lib目录下看到一些*.a文件,这些都属于静态库。

提示

Windows 下的*.lib文件有两种,一种是静态库,一种是DLL导入库,后者居多,不过在 CMake 开发上简单当做静态库也是可以的。

1.3.2 区别

  1. 静态库 这种库在编译的时候会直接整合到目标程序中,所以利用静态库编译成的文件会比较大,这类函数库最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。

  2. 动态库 与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个符号而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去解析这个符号并完成动态库地址的重定位,也就是说可执行文件无法单独运行。这样从产品功能升级的角度来说是方便的,只要替换对应动态库即可完成升级,不必重新编译整个可执行文件。

2. 目标构建

2.1 回顾:可执行文件构建

现给定一个文件架构:

.
├── CMakeLists.txt (待创建)
└── main.cpp

这是最简单的一类情况,即仅有一个 *.cpp 构成可执行程序的源文件。对于这种架构的构建方式在【01】安装与基本介绍中已经做过了演示。对于这种通常只需要短短几行 CMake 语句即可完成,首先在当前目录直接创建 CMakeLists.txt,输入:

cmake_minimum_required(VERSION 3.16) # 指定 CMake 最小版本号,小于此版本的无法通过 CMake
project(MyProject)                   # 定义项目名
add_executable(my_main main.cpp)     # 使用 main.cpp 生成可执行文件 my_main

其中my_main是目标名,在 CMake 中是具有TARGET属性的变量,即目标,同时也是可执行文件的文件名。CMake 提供了一系列与目标链接有关的操作方式。其中如果my_main目标依赖了其他的目标,例如main.cpp文件中使用到了来自其他*.cpp文件定义的函数,我们还需要在CMakeLists.txt中输入

target_link_libraries(
  my_main
  PRIVATE xxxx
)

这里的xxxx就表示包含了这些函数定义的目标。这里表示需要将my_main这个目标链接至xxxx上。

此外,文档中给出了这样一句话:

New in version 3.13: The <target> doesn't have to be defined in the same directory as the target_link_libraries call.

翻译过来就是

3.13 版本的新内容:<target>不需要在与调用target_link_libraries命令相同的目录中被定义。

这句话很关键,表示在较新的 CMake 版本中,允许在整个项目中跨CMakeLists.txt文件进行链接操作,例如,即使上文的my_main在父目录的作用域,xxxx在子目录的作用域,my_main所在的CMakeLists.txt也能访问到xxxx这一目标变量。这一点有别于普通的变量,可以当做是具有目标属性的变量的专属功能。

另外代码中还出现了PRIVATE的标志,这个属于目标的传播方式,在后文会进行介绍。

2.2 普通库目标构建

在文件结构复杂时,或者需要将功能模块隔离、抽象出来时,经常需要为某个模块构建一个库,最终将这些库链接至可执行文件的目标上。

语法是:add_library(目标名 [SHARED | [STATIC]] 源文件)

还是刚才的文件结构,先创建新的文件夹及文件:

.
├── CMakeLists.txt
├── main.cpp
└── MyLib1
    ├── CMakeLists.txt (待创建)
    ├── include
    │   └── MyLib1.h
    └── src
        ├── a.cpp
        ├── b.cpp
        └── c.cpp

首先项目根目录的CMakeLists.txt需要添加

add_subdirectory(MyLib1)

来让该模块添加至该项目。此外,每个库需要有一个单独的CMakeLists.txt进行管理,因此需要在MyLib1文件夹内创建该文件,内容如下:

# 搜索 src 文件夹下的所有源文件,并添加至局部变量:my_lib_dir
# 题外话:aux 表示 auxiliary 即辅助的意思,表示这个功能是个辅助功能
aux_source_directory(src my_lib_dir)
# SHARED 表示该目标为动态库,STATIC 则表示静态库
add_library(MyLib1 SHARED ${my_lib_dir})
# 以上内容也可以写成下一行,但这么长的名字,谁会这么做呢?
# add_library(MyLib1 SHARED src/a.cpp src/b.cpp src/c.cpp)

注意,此处生成的动态库 libMyLib1.so 无法做到内存上的复用,在多个进程使用到该动态库时,仍然会开辟一块内存存储该动态库内容。为了真正实现内存上的复用,需要使用地址无关代码机制(position-independent code):

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")

话说回来,本库内的三个源文件都会包括 include 头文件夹中的内容,因此在编译时需要给目标添加头文件的搜索路径:

target_include_directories(
  MyLib1
  PUBLIC include # 表示当前 CMakeLists.txt 所在目录下的 include 文件夹
)

上文的 PUBLIC 属性,有时候还会见到 INTERFACE、PRIVATE 属性,一共 3 个属性。下面对这三个属性做个介绍:

  • PUBLIC:在绑定当前目标时给指定的内容设置公有属性,其他目标在链接当前目标时,能访问这些指定的内容
  • PRIVATE:在绑定当前目标时给指定的内容设置私有属性,其他目标在链接当前目标时,无法访问这些内容
  • INTERFACE:在绑定当前目标时给指定的内容设置接口属性,通常在接口库中使用。其他目标在链接当前目标时,只允许访问其声明(接口)

最后,如果此目标有依赖

  • 本项目中其他目标
  • 项目以外第三方目标(例如 OpenCV)

的内容,那必须要添加以下内容:

target_link_libraries(
  MyLib1
  PUBLIC xxxx
)

同样,为指定目标链接库的语句也提供了 PUBLIC、PRIVATE、INTERFACE 三个属性,这里不再赘述。

这里涉及到的两个语句,我们做个汇总:

  • target_include_directories:为目标添加需要包含的文件(一般是头文件)的搜索路径,该路径将绑定至目标上
  • target_link_libraries:为目标添加依赖的库,该库也将绑定至目标上

2.3 接口库目标构建

在程序开发中,有时候会遇到只有头文件(*.h、*.hpp),而没有源文件(*.c、*.cpp)的情况,而我们知道,一般在使用 add_library 的时候只能为源文件(*.h以及*.hpp之类的头文件不属于源文件)生成目标库。

在这种情况下,如果我们需要对只有头文件的库生成目标,并且进行链接,我们需要创建接口库。这种目标由于没有源文件,不会实质性的参与构建(编译),但提供了与普通目标相同的操作方式,因此接口库属于伪目标。

还是刚才的文件结构,先创建只有头文件的文件夹及 CMakeLists.txt:

.
├── CMakeLists.txt
├── main.cpp
├── MyLib1
│   ├── CMakeLists.txt
│   ├── include
│   │   └── MyLib1.h
│   └── src
│       ├── a.cpp
│       ├── b.cpp
│       └── c.cpp
└── MyLib2
    ├── CMakeLists.txt (待创建)
    └── include
        └── MyLib2.hpp

首先项目根目录的 CMakeLists.txt 需要添加

add_subdirectory(MyLib2)

创建接口库的语法与普通库类似,只是少了源文件的添加的步骤:

add_library(MyLib2 INTERFACE)
target_include_directories(
  MyLib2
  INTERFACE include # 接口库的目标只能使用 INTERFACE 属性
)
target_link_libraries(
  MyLib2
  INTERFACE xxx
)

2.4 导入目标构建

我们会遇到这种情况,仅提供了若干头文件和若干库文件(例如 *.so 和 *.a)在这种情况下我们无法通过自己 add_library 从源文件创建目标,我们需要引入导入目标。

假设某家相机厂商的 SDK 提供了以下内容,假设将其放在了项目文件夹的 camera 文件夹下,请根据以下文件结构创建一个 CMake 目标

.
├── include
│   ├── CameraApi.h
│   ├── CameraDefine.h
│   └── CameraStatus.h
└── lib
    └── libMVSDK.so

在 camera 文件夹中创建 CMakeLists.txt 文件,写入

add_library(camera SHARED IMPORTED)
set_target_properties(camera PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/include"
  IMPORTED_LOCATION "${CMAKE_CURRENT_LIST_DIR}/lib/libMVSDK.so"
)

对于 INTERFACE_INCLUDE_DIRECTORIES 的属性,我们也可以用常规的写法,写成

target_include_directories(
  camera
  INTERFACE include
)

注意

  • 在设置 IMPORTED_LOCATION 属性的时候需要指定绝对路径
  • 对于 lib 目录中存在多个 *.so 等动态库的情况下,请找到真正的动态库(其他动态库一般是陪衬的)

3. 补充内容

3.1 其余目标库

根据CMake官网对目标库的划分,可简单分为两类:二进制目标、伪目标,其大致内容如下

├── 二进制目标
│   ├── 可执行文件
│   └── 二进制库
│       ├── 普通库(动态库、静态库)
│       └── 对象库
└── 伪目标
    ├── 导入目标
    ├── 别名目标
    └── 接口库

3.1.1 对象库

允许将多个源文件设置为一个单独的目标,而不生成可执行文件或普通库。对象库可以被其他目标(例如可执行文件或普通库)链接和使用。

要创建一个对象库,同样可以使用 add_library 命令,并将 OBJECT 关键字与源文件列表一起使用。下面是一个 CMakeLists.txt 示例:

# 添加对象库
add_library(
  my_object_lib
  OBJECT file1.cpp file2.cpp file3.cpp
)
# 添加可执行文件,并链接对象库
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE my_object_lib)

在上面的示例中,add_library 命令创建了一个名为 my_object_lib 的对象库,并将 file1.cpp、file2.cpp 和 file3.cpp 作为源文件。

通过这种方式,对象库中的源文件将会被编译为目标文件,但不会生成一个独立的可执行文件或共享库。其他目标可以链接到对象库,并在链接时使用其中的目标文件。

3.1.2 别名目标

允许为现有的目标创建一个可供引用的别名。别名目标可以用于简化构建过程、重命名目标或创建便于引用的名称。

要创建别名目标,可以使用 add_library、add_executable 或 add_custom_target 命令,并将 ALIAS 关键字与目标名称一起使用。下面是一个 CMakeLists.txt 示例:

# 添加一个库
add_library(my_library STATIC my_source.cpp)
# 创建别名目标
add_library(lib1 ALIAS my_library)
# 添加可执行文件,并链接别名目标
add_executable(my_app main.cpp)
target_link_libraries(
  my_app
  PRIVATE lib1
)

但要注意,别名目标(例如上文中的 lib1)不得作为

  • target_link_libraries
  • target_include_directories

的目标名,即不得写成

target_include_directories(
  lib1
  PUBLIC xxx
)
# 或者
target_link_libraries(
  lib1
  PRIVATE xxx
)

的形式

3.2 设置目标属性

即使用命令 set_target_properties

一般情况下,目标属性通常很少直接使用,一般是间接使用,例如

  • 在使用 target_include_directories 的时候,会为目标设置 INTERFACE_INCLUDE_DIRECTORIES 属性
  • 在使用 target_link_libraries 的时候,会为目标设置 INTERFACE_LINK_LIBRARIES 属性

实际上,对于导入目标、接口库目标,我们可以不通过以上命令来实现对应功能,譬如说,为了指定目标包含的目录,原先写为

target_include_directories(
  my_target
  PUBLIC include
)

现在可以通过使用

set_target_properties(
  my_target PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/include
)

或者

set_property(
  TARGET my_target PROPERTIES
  INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/include
)

来实现同样功能,此操作在配置导入目标的时候比较常见。此外,CMake 官网提供了非常非常多的属性,可以点击此处查阅。

注意

在设置 INTERFACE_INCLUDE_DIRECTORIES 和 INTERFACE_LINK_LIBRARIES 属性的时候需要指定绝对路径

3.3 非必要不使用全局包含、链接

非必要情况下不要使用

  • link_library
  • include_directories

语句,会影响所有子目录的构建,带来污染问题。但有些情况下这些语句可以实现其他功能。例如 target_include_directories 可以将内容绑定到目标并且在导出目标后依赖关系仍然生效,而使用 include_directories 可以避免安装后仍然存在依赖关系。具体原因和实现细节可参考【10】项目的导出与安装 #3.4。

3.4 访问库的方式

当需要访问第三方库的时候,第三方库需要提供头文件(类似于 *.h 和 *.hpp)和库文件(包括动态库 *.so 和静态库 *.a 两种形式)。

提示

Windows 下的库文件有两种形式

  • 动态库*.dll以及导入库*.lib
  • 静态库*.lib
  • 在访问本项目的库A时,如果已经使用了target_include_directories的PUBLIC属性,将头文件路径绑定在了目标A中,那么目标B在访问A的时候,只需要

    target_link_libraries(B PUBLIC A)
    

    即可访问到A所包含的头文件路径。

  • 在访问其他项目的第三方库时,用户需要单独获取对应的库文件与头文件路径进行链接,例如 OpenCV:

    add_executable(xxx main.cpp)
    
    target_include_directories(
      xxx
      PUBLIC ${OpenCV_INCLUDE_DIRS}
    )
    
    target_link_libraries(
      xxx
      PRIVATE ${OpenCV_LIBS}
    )
    

    但实际上,OpenCV_LIBS 是包含了众多导入目标的列表变量,例如 opencv_core;opencv_highgui,并且列表中的每个变量都设置了 INTERFACE_INCLUDE_DIRECTORIES 的属性。不写 target_include_directories 命令也是正确的:

    add_executable(xxx main.cpp)
    
    target_link_libraries(
      xxx
      PRIVATE ${OpenCV_LIBS}
    )
    
Prev
【04】运算符与条件、循环语句
Next
【06】变量参与 C++ 程序的编译