Offical Tutorial(未完成)
# Exercise1
# Building a Basic Project
在使用 CMake 进行项目构建的时候,CMakeLists.txt 中必须在有下面几个部分:
指定最小的 CMake 版本 -
cmake_minimum_required()
这个命令的作用是告诉 CMake,必须使用指定的版本或更高版本的 CMake 来构建项目,否则 CMake 会生成一个错误消息并停止构建过程。
例如,假设你的 CMake 脚本使用了 CMake 3.20 中引入的 target_sources 命令,这个命令在早期版本的 CMake 中并不存在。如果你的项目需要在 CMake 3.20 及以上版本中构建,那么你可以在 CMakeLists.txt 文件的顶部添加以下行:cmake_minimum_required(VERSION 3.20)1这样做会告诉 CMake,如果使用低于 3.20 版本的 CMake 构建项目,就会生成错误消息并停止构建。这样做可以确保你的项目只会在指定版本的 CMake 中构建,从而避免了由于版本不兼容导致的构建错误和问题。
使用
project()来设置项目的名字
举例:设置项目名称为 Tutorial
project(Tutorial)使用
add_executable()命令告诉 CMake 使用指定的源代码文件创建可执行文件。
举例:设置可执行目标文件为 tutorial,源文件为tutorial.cxx
add_executable(tutorial tutorial.cxx)
项目构建方法:
# 创建一个构建文件夹
mkdir Step1_build
# 使用cmake生成项目构建系统,其中Step1为工程文件所在的目录(只需要生成一次,之后修改直接调用即可)
cmake ../Step1
# 调用构建系统构建项目
cmake --build .
2
3
4
5
6
# Specifying the C++ Standard
禁止对变量使用 CMAKE_ 开头的进行命名,因为它用来指定一些 CMake 构建项目的一些特殊变量。比如:
CMAKE_CXX_STANDARD and CMAKE_CXX_STANDARD_REQUIRED 一起使用用来指定构建项目时的 C++ 标准。
比如需要使用 C++11 时,使用如下语句:
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
2
*: 这两条的语句要放在 add_executable() 的前面
# Adding a Version Number and Configured Header File
CMake 还支持在 CMakelists.txt 中定义的变量也可以在源代码中进行使用。
举例:通过在 CMakelists.txt 中设置版本变量,然后在源文件中用该变量打印版本号。虽然我们可以在源代码中独立完成这个任务,但使用 CMakeLists.txt 可以让我们维护版本号的唯一数据源。
首先,我们修改 CMakeLists.txt 文件,使用 project () 命令设置项目名称和版本号。当调用 project () 命令时,CMake 在幕后定义 Tutorial_VERSION_MAJOR 和 Tutorial_VERSION_MINOR 和 Tutorial_VERSION_PATCH 三个变量。
比如在 project 中设置了版本号为porject(Tutorial VERSION 1.2.3)
Tutorial_VERSION_MAJOR : 主版本号,在这个例子中为 1
Tutorial_VERSION_MINOR : 次版本号,在这个例子中为 2
Tutorial_VERSION_PATCH : 修订版本号,在这个例子中为 3在同一个 CMakeLists.txt 中,使用 configure_file () 将给定的输入文件复制到输出文件并替换输入文件内容中的一些变量值。对输入文件内容中被引用为 @VAR@或 ${VAR} 的变量值进行替换。每个变量引用将被替换为变量的当前值,或者如果变量未定义,则为空字符串
比如在 TutorialConfig.h.in 中定义了三个 macro:#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@ #define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@ #define Tutorial_VERSION_PATCH @Tutorial_VERSION_PATCH@1
2
3然后使用 configure_file (TutorialConfig.h.in TutorialConfig.h) 对变量替换后,输出到文件 TutorialConfig.h 中。
在这个例子中输入和输出文件都使用了相对路径,都是 configure_file 默认指定的。
- 输入文件的路径,相对路径将根据
CMAKE_CURRENT_SOURCE_DIR的值进行处理。 - 输出文件相对路径将根据
CMAKE_CURRENT_BINARY_DIR的值进行处理。在整个教程中,我们将互换地引用项目构建目录和项目二进制目录。所以项目二进制目录也就是项目被构建的目录。
- 输入文件的路径,相对路径将根据
因为 TutorialConfig.h 被放到了项目构建目录,所以还需要使用
target_include_directories指定编译目标的头文件搜索路径:target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}"1
2- 其中,
${PROJECT_BINARY_DIR}是一个 CMake 内置的变量,它表示项目的二进制目录,即编译过程中生成的可执行文件、库文件等输出文件的存放路径。通过这行指令,编译器会在编译 Tutorial 目标时自动搜索${PROJECT_BINARY_DIR}路径下的头文件。 - PUBLIC 关键字会将指定的搜索路径设置为目标的公共搜索路径。这样一来,当其他目标链接到该目标时,它们就可以自动继承该目标的公共搜索路径。如果你不使用 PUBLIC 关键字,那么指定的搜索路径只会适用于目标本身,而不会适用于链接到它的其他目标。
举例:假设你正在编译一个游戏程序,这个程序包含了多个源文件,其中一个是用来处理游戏音效的 sound.cpp,还有一个是用来处理游戏图像的 image.cpp。为了编译这个程序,你需要将这两个源文件编译成两个目标文件 sound.o 和 image.o,然后将它们链接起来形成一个可执行文件 game.exe。
现在假设你在编写 sound.cpp 的时候需要引用一个名为 utility.h 的头文件,这个头文件定义了一些通用的函数,比如字符串处理函数等。如果你只在 sound.cpp 中使用了这个头文件,而没有在 image.cpp 中使用,那么在编译 image.cpp 时就不需要搜索这个头文件,因为它不会被使用到。
但是,如果你将 sound.o 和 image.o 这两个目标文件链接起来形成 game.exe,那么 game.exe 中就需要包含 utility.h 头文件的定义才能正确运行。为了实现这个目的,你可以在 CMakeLists.txt 文件中使用
target_include_directories()函数指定 utility.h 的搜索路径,同时使用 PUBLIC 关键字表示这个搜索路径不仅适用于 sound.o,也适用于 image.o 和链接它们的 game.exe。这样一来,不管是在编译 sound.o 还是 image.o,还是在链接 game.exe 时,都能正确地搜索到 utility.h 头文件的定义,从而确保程序能够正确运行。
- 其中,
# Exercise2
本节的几个练习的目的是学会在工程中创建和使用库。
# Creating a Library
如何给源文件添加一个我们设计的库函数呢?(以为源文件 tutorial.cxx 添加库函数 MathFunctions.h 为例子,请参考着官方教程代码食用)
这个练习中使用到的文件结构如下图所示:
.
├── CMakeLists.txt # 主目录下的 CMakeLists.txt 文件,用于描述项目的构建过程
├── MathFunctions # 包含 MathFunctions 库的子目录
│ ├── CMakeLists.txt # MathFunctions 目录下的 CMakeLists.txt 文件,用于描述 MathFunctions 库的构建过程
│ ├── mysqrt.cxx # MathFunctions 库的源文件
│ ├── mysqrt.h # MathFunctions 库的头文件
│ ├── MathFunctions.cxx # 定义了`mathfunctions`命名空间中的`sqrt`函数, 其指向detail空间的`mysqrt`函数
│ └── MathFunctions.h # 创建一个新的命名空间`mathfunctions`, 并在里面声明了`sqrt`函数
└── tutorial.cxx # 包含 main 函数的源文件,用于创建可执行文件
2
3
4
5
6
7
8
9
设计步骤:
使用
add_library()在库函数所在目录的 CMakeList.txt 中指定哪些源文件组成库
(这个例子中的 CMakeList.txt 是./MathFunctions/CMakeLists.txt)add_library(MathFunctions mysqrt.cxx)add_library () 语法分析:
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])1
2
3add_library: 创建一个静态或共享库
- <name>: 库的名称。
- STATIC | SHARED | MODULE: 指定库的类型。其中,STATIC 表示静态库,SHARED 表示共享库,MODULE 表示插件库。如果没有指定类型,则默认创建静态库。
- EXCLUDE_FROM_ALL: 如果设置了该选项,则将库从默认目标中排除,只有显式地指定时才会被构建。
举例说明:
例如,假设我们有一个库 mylibrary,它在主目录的 CMakeLists.txt 文件中被添加为子目录:如果我们想要将 mylibrary 从默认目标中排除,只在显式指定时才进行构建,可以在 mylibrary 的 CMakeLists.txt 文件中使用 EXCLUDE_FROM_ALL 选项:add_subdirectory(mylibrary)1这样,当我们在主目录中调用 make 命令时,mylibrary 将不会被构建,除非我们显式地指定它作为目标:add_library(mylibrary mylibrary.cpp) set_target_properties(mylibrary PROPERTIES EXCLUDE_FROM_ALL TRUE)1
2make mylibrary1source1 [source2 ...]: 库的源文件。
当我们创建一个新的库时,需要在主目录的
CMakeLists.txt文件中添加add_subdirectory()命令,以便在构建过程中包含该库的构建。
在 MathFunctions 目录下的 CMakeLists.txt 文件中,我们使用 add_library () 命令将MathFunctions添加为一个库:
add_library(MathFunctions mysqrt.cxx)
然后,在主目录的CMakeLists.txt文件中,我们可以使用add_subdirectory()命令将MathFunctions子目录添加为子目录:
add_subdirectory(MathFunctions)
这将告诉 CMake 在构建过程中包含MathFunctions子目录,并构建MathFunctions库。
接下来,我们可以在主目录的可执行文件中使用
target_link_directories()命令告诉CMake在构建可执行文件Tutorial时链接MathFunctions库。target_link_directories(Tutorial PUBLIC MathFunctions )1
2
3语法如下:
target_link_directories(target <INTERFACE|PUBLIC|PRIVATE> directory1 [<INTERFACE|PUBLIC|PRIVATE> directory2 ...])1
2
3target 是您希望将库链接到的目标,directory1、directory2 等是要链接的库目录的路径。
INTERFACE 可见性表示链接库的路径将被传递到 target 的依赖项中,但不会传递到 target 本身。
PUBLIC 可见性表示链接库的路径将被传递到 target 和 target 的依赖项中。
PRIVATE 可见性表示链接库的路径只会被传递到 target 本身,不会传递到 target 的依赖项中。
(关于可见性的问题,我已经在 Exercise1 作出了仔细的讨论)CMake 中链接库只提供了函数的实现,但并没有提供其头文件的位置。如果您不将头文件目录添加到目标中,编译器将无法找到这些头文件,从而导致编译错误。 所以需要使用
target_include_directories命令指定的目录添加到一个或多个 CMake 目标的头文件搜索路径中。target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}" # 这是Exercise1中要求添加的 "${PROJECT_SOURCE_DIR}/MathFunctions" )1
2
3
4通过这种方法,我们就可以成功的在 tuturial.cxx 中声明并使用我们定义的库函数。
#include "MathFunctions.h"1
嗯,我按照 offical tutorial (opens new window) 上的答案像上面这样做了,但是出现了错误,tutorial 中有两个地方不正确:
- TODO 6 中要添加 mathfunctions::sqrt (inputValue) 而不是
mysqrt(inputValue)这可以从./MathFunctions/MathFunctions.h这个头文件的定义中可以看出来。(如何定义头文件看下面有解释。) - TODO 1 中要添加
add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)而不是add_library(MathFunctions MathFunctions.cxx)这是为什么呢?
因为源文件 MathFunctions.cxx 中定义了 mathfunctions 空间中的 sqrt 函数,但是该函数定义内部是通过调用 detail::mysqrt 函数实现的,这就意味着如果不讲 mysqrt.cxx 也添加到库中,那 cmake 能在库函数中找到 mathfunctions::sqrt,但是发现该函数是调用 detail::mysqrt 实现的,库函数中没有 detail::mysqrt 函数,链接器链接失败,报错。
总结:CMake 中添加一个库的步骤
- 如果需要组织项目中自己设计的库函数,可以创建一个或多个子目录,将源文件放在其中,并添加一个
CMakeLists.txt文件来管理它们使用add_library()命令指定哪些源文件组成库。 - 在顶级的
CMakeLists.txt文件中使用add_subdirectory()命令添加子目录,以便将其添加到构建中。 - 使用
target_link_libraries()命令将库连接到我们的可执行目标中。 - 使用
target_include_directories()命令将库链接到我们的可执行目标中。
如何定义命名空间?
namespace namespace_name {
// code declarations
}
2
3
以 mysqrt.h 为例
#pragma once //确保头文件只被编译一次
namespace mathfunctions { //定义命名空间
namespace detail { //定义命名空间
double mysqrt(double x);
}
}
2
3
4
5
6
7
# 为什么我们需要头文件?
- 每个 CPP 源文件都是孤独的
在编译阶段每个 CPP 文件是分开进行编译的,这就意味着可能出现下面这种情况:
A.CPP 需要调用一个定义在 B.CPP 中的函数:
// A.CPP
void doSomething()
{
doSomethingElse(); // Defined in B.CPP
}
// B.CPP
void doSomethingElse()
{
// Etc.
}
2
3
4
5
6
7
8
9
10
11
这种情况下,A.CPP 就无法进行编译,因为 A.CPP 不知道 doSomethingElse() 这个函数存在,除非有这样一个声明:
// A.CPP
void doSomethingElse() ; // From B.CPP
void doSomething()
{
doSomethingElse() ; // Defined in B.CPP
}
2
3
4
5
6
7
这时如果我们有 C.CPP 也同样的需要 B.CPP 中的函数,我们就需要 COPY/PASTE 上面这种声明。
COPY/PASTE 难以维护
COPY/PASTE 是危险的,并且难以维护。这就意味着如果用一种方法不需要 COPY/PASTE 就可以声明doSomethingElse来自于 B.CPP 就很完美。如何做到呢?使用头文件:// B.H (here, we decided to declare every symbol defined in B.CPP) void doSomethingElse() ; // A.CPP #include "B.H" void doSomething() { doSomethingElse() ; // Defined in B.CPP } // B.CPP #include "B.H" void doSomethingElse() { // Etc. } // C.CPP #include "B.H" void doSomethingAgain() { doSomethingElse() ; // Defined in B.CPP }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
26include是如何工作的?
include 的东西会在预处理阶段简单的展开为.H 文件中的内容,比如:
A.H 头文件
// A.H
void someFunction();
void someOtherFunction();
2
3
B.CPP 源文件:
// B.CPP
#include "A.HPP"
void doSomething()
{
// Etc.
}
2
3
4
5
6
7
预处理阶段得到:
// B.CPP
void dosomeFunction();
void someOtherFunction();
void doSomething()
{
// Etc.
}
2
3
4
5
6
7
8
- 为什么需要在 B.CPP 中也 include B.H?
在上面这个例子中确实是不需要在 B.CPP 中包含 B.H, 因为 B.CPP 在定义doSomething的同时也相当于是声明了这个函数。
但是在下面两种情况下 B.CPP 中 include B.H 是非常有必要的:
- 当 B.H 中声明一些没有相对应的定义结构的数据时,B.H 是非常有必要的,比如 enums, structs, etc.
- B.H 中声明了 inline code 时,在 B.CPP 中也不需要相对应的 inline 函数的定义,在编译阶段会直接把 B.H 中 inline 函数的定义直接插入 B.CPP 代码段中。
#include <iostream> using namespace std; inline int cube(int s) { return s * s * s; } int main() { cout << "The cube of 3 is: " << cube(3) << "\n"; return 0; }1
2
3
4
5
6
7
8
- 为什么我们在编译链接程序时往往只 include 了
.h文件程序就可以运行?
因为其实对应的.cpp 文件也被独立的编译了,会在链接阶段进行链接。