为什么要使用CMake呢?随着工程变得庞大,构建它会有很多文件的参与,手写 Makefile 等构建文件变得愈发困难且容易出错,使用 CMake 可以简化这个过程;也许,有一个智能的IDE能够帮助你构建文件,但当你把源代码发给别人时,可能对方并没有相应的工具,只能面对源文件措手无策,或者重新编写构建文件,而如果有 CMake,则能由一个文件根据不同的平台生成相应的建构档,事情就会变得轻松而愉快。

参考链接:

基本的CMake项目

先从最简单的单文件做起,假设现在目录下有一个main.cpp文件:

  1. #include <iostream>
  2. int main() {
  3. std::cout << "Hello!";
  4. }

我们可以在和源代码相同的目录下新建一个CMakeLists.txt,然后写入这样的内容:

  1. cmake_minimum_required(VERSION 3.13)
  2. project(cmake_test)
  3. set(CMAKE_CXX_STANDARD 14)
  4. add_executable(cmake_testapp main.cpp)

这个的例子来自 CLion 的教程。下面是每一行的解释:

命令 解释
cmake_minimum_required(VERSION 3.13) 指定CMake的最低版本要求,如果不加会有警告
project(cmake_test) project命令定义项目的名称
set(CMAKE_CXX_STANDARD 14) set命令用来给变量赋值,这里CMAKE_CXX_STANDARD变量用来指定所需最低的C版本,这里要求C版本最低为C++14
add_executable(cmake_testapp main.cpp) add_executable会添加一个生成可执行文件的“目标(target)”,目标的名字为cmake_testapp,其将从main.cpp被构建(build)

请读者留意“目标(target)”的概念,这在现代 CMake 中很是重要。

保存CMakeLists.txt文件,然后在命令行中输入cmake .,便会生成相应的构建文件。

不过通常我们不会在源代码的文件夹里直接构建,因为构建过程会生成很多文件,和源代码会在一起,显得比较杂乱。通常会新建一个目录,可以在当前目录,也可以在任何喜欢的地方,只要在这个目录下将CMakeLists.txt的位置告诉 CMake 就可以,然后在这个文件夹里面执行构建的操作,称为“Out of source building”。

比如我们在当前目录下新建一个 build 目录,然后在其中进行构建操作。这样,因为 CMakeLists.txt 的位置在上一级目录,可以用..表示。

Unix 下会默认生成 Makefile,生成后执行 make 命令,就会根据 Makefile 中的内容进行一系列操作。

  1. mkdir build && cd build
  2. cmake ..
  3. make

Windows 下默认生成 Visual Studio 的项目文件 *.vcxproj

也可以使用-G命令指定生成的构建文件。如果之前生成的是其他构建文件,需要先删除构建目录下的 CMakeCache.txt

  1. cmake -G "MinGW Makefiles" ..

一个项目中可以有多个目标,我们可以指定多个生成可执行文件的目标。

比如我们有 test1.cpptest2.cpp 两个cpp,它们分别会生成不同的程序。可以新建两个名为 test1test2 的目标(名字随意),修改后的CMakeLists.txt如下所示:

  1. cmake_minimum_required(VERSION 3.13)
  2. project(cmake_test)
  3. set(CMAKE_CXX_STANDARD 14)
  4. add_executable(test1 test1.cpp)
  5. add_executable(test2 test2.cpp)

修改过 CMakeLists.txt 之后,需要重新执行 cmake 操作,重新生成 CMake 缓存以及构建脚本。如果使用的是IDE,那么可能会自动进行这个过程,或者提示用户进行这个操作。

注:使用make时,可以用make <target>指定要生成的目标,比如make test1。如不指定,则默认全部执行。如果使用了 IDE,可能会有下拉框以供选择。

多文件(一)

首先新建一个目录,这个目录就是我们的项目目录,比如“cmake_test”,我们的操作都在这个目录下执行,而 CMakeLists.txt 一般放在项目根目录下。

  1. cmake_test
  2. | CMakeLists.txt
  3. | main.cpp
  4. | solution.cpp
  5. | solution.h
  6. |
  7. \---build

现在,我们的可执行文件的目标需要多个源代码文件生成,比如下面这样,其中 solution.cppsolution.hmain.cpp 都在同一级目录(项目根目录)下:

  1. // solution.h
  2. #ifndef SOLUTION
  3. #define SOLUTION
  4. class Solution {
  5. public:
  6. static int add(int a, int b);
  7. static int subtract(int a, int b);
  8. static int multiply(int a, int b);
  9. };
  10. #endif
  1. // solution.cpp
  2. #include "solution.h"
  3. int Solution::add(int a, int b) {
  4. return a + b;
  5. }
  6. int Solution::subtract(int a, int b) {
  7. return a - b;
  8. }
  9. int Solution::multiply(int a, int b) {
  10. return a * b;
  11. }
  1. // main.cpp
  2. #include <iostream>
  3. #include "solution.h"
  4. int main() {
  5. std::cout << Solution::add(3, 5) << ' ';
  6. std::cout << Solution::multiply(3, 3);
  7. }

注意solution.cppmain.cpp中引用我们自定义的头文件的方式#include "solution.h":编译器读取到solution.cpp里面的"solution.h"时,会根据相对路径寻找相应的文件进行包含,solution.h等效于./solution.h,也就是和该源代码文件相同目录下的solution.h文件,因此编译器能正确的包含(include)这份头文件到源代码文件里。

然后我们的CMakeLists.txt这样写:

  1. cmake_minimum_required(VERSION 3.13)
  2. project(cmake_test)
  3. set(CMAKE_CXX_STANDARD 14)
  4. add_executable(solution_test main.cpp solution.cpp)

这样solution_test就会由solution.cppmain.cpp构建出来。

是不是很简单?CMake只需要一条语句就能完成,而如果手动使用命令行编译则需要输入四五条命令,而且会随着文件的增多变得愈发棘手。

生成库文件

参考阅读:

当一些相对固定的功能需要经常被其他程序调用,或者有些功能想发给别人但不希望给出源码,我们可以将这些功能生成函数库,这样在使用时只需和可执行文件进行链接,可以节省编译时间。

也很简单,我们使用add_library命令即可。还是使用刚刚的代码。我们在上文的CMakeLists.txt中添加这样的语句。

  1. add_library(test_library STATIC solution.cpp)

语句中,test_library是我们生成的库的名称,STATIC表明生成的为静态链接库,solution.cpp则表示使用这个文件生成函数库。

静态库在编译时链接到可执行文件里,而动态库(shared library)则在程序运行时被加载。动态库有时也被称为共享库(shared library)。

注:生成的链接库的文件名会在指定的库名称的前边加上lib,搜索时也会根据指定的名称再在前边加上lib得到的名称进行搜索。比如,上述例子生成的库文件名就为libtest_library

编辑好CMakeLists.txt后,不要忘了重新运行一次CMake。

执行生成函数库的这个目标(target)后,一个静态库就会生成在构建目录下。

关于生成的函数库如何使用,我们稍后再讲解。

多文件(二):有层级的目录

当头文件和源代码文件被放在了不同的目录、不同层级的目录时,事情会变得稍微复杂一点。

比如以我们刚刚的三个文件为例,现在把它们放在不同的目录里,层级如下图所示:

  1. cmake_test
  2. | CMakeLists.txt
  3. | main.cpp
  4. |
  5. +---include
  6. | solution.h
  7. \---src
  8. solution.cpp

我们依然可以使用相对路径来包含头文件:

main.cpp

  1. #include "include/solution.h"
  2. ...

solution.cpp

  1. #include "../include/solution.h"
  2. ...

这样,编译器在编译cpp文件时依然能正确包含到相应的头文件,但是这样的写法不仅繁琐,也会给维护带来不少麻烦:比如随着代码的增多,代码根据新的逻辑被重新划分到了新的若干目录中,那么可能所有源代码中的include命令都要重新修改,才能让它们重新找到头文件。

所以,对于我们自定义的头文件,我们可以在包含它时只写头文件的名字,然后为编译器指定相应的搜索路径,这样,编译器在遇到一个需要包含的头文件名时,就会在其知晓的搜索路径中查找这个文件,即系统头文件路径等、以及用户指定的包含路径(include path)。而当我们的文件目录层级发生变化时,只需要重新指定头文件的搜索路径,而不用去修改每一个源代码文件。

添加包含路径(include path)

在CMake中,我们可以用include_directories命令指定包含路径。

  1. include_directories(include)

这行命令表示,我们把项目根目录下的include目录添加到头文件的搜索路径中。这项设置是全局的,也就是任何文件都能访问到include目录下的文件。

现在我们的CMakeLists.txt应该长这样:

  1. cmake_minimum_required(VERSION 3.13)
  2. project(cmake_test)
  3. set(CMAKE_CXX_STANDARD 14)
  4. include_directories(include)
  5. add_library(test_library STATIC src/solution.cpp)
  6. add_executable(solution_test main.cpp src/solution.cpp)

注意现在solution.cpp的路径发生了变化,应该写作src/solution.cpp

链接函数库

参考链接:

之前我们执行了test_library目标后,生成了一个静态库文件libtest_library.a,现在来使用它。

默认该文件生成在构建目录下。我们在项目根目录下新建一个lib目录,然后把静态库文件复制进去。

我们使用CMake的find_library命令来寻找函数库文件:

  1. find_library (TEST_LIB test_library lib)

这条命令表示:在lib目录中查找名为test_library的库,并新建一个TEST_LIB变量,把结果存放其中。

之后,还要使用target_link_libraries命令把库和我们的可执行文件链接起来,这样才能正确执行它。

现在,我们的CMakeLists.txt应该这样写了:

  1. cmake_minimum_required(VERSION 3.13)
  2. project(cmake_test)
  3. set(CMAKE_CXX_STANDARD 14)
  4. include_directories(include)
  5. add_library(test_library STATIC src/solution.cpp)
  6. add_executable(solution_test main.cpp)
  7. find_library (TEST_LIB test_library lib)
  8. target_link_libraries(solution_test ${TEST_LIB})

注意,这里我们add_executable命令中不再使用src/solution.cpp了,而是在最后将可执行文件和由其生成的库文件链接起来。

其实,target_link_libraries命令还可以有其他的方式,比如项目中已有的“库文件目标”的名称,即使用add_library()创建的目标。这样就不需要find_library命令了:

  1. target_link_libraries(solution_test test_library)

也可以指定一个具体的路径下的库文件进行链接:

  1. target_link_libraries(solution_test ${PROJECT_SOURCE_DIR}/lib/libtest_library.a)

选择构建类型(build type)

构建类型有Debug和Release,其中Debug会包含调试信息,Release一般会进行额外的优化来提高运行效率,也会增加编译时间。

可以在运行CMake时指定参数:

  1. cmake -DCMAKE_BUILD_TYPE=Debug ..

命令行中的-D表示参数设置,将CMAKE_BUILD_TYPE的值设置为Debug

如果使用IDE,也会有相应的设置。

使用其他包(package)

编程中,我们会遇到需要使用其他包 / 函数库的时候,比如OpenCV、Boost等。

为了清晰,我们新建一个目录,名为opencv_test

下面我们调用OpenCV,写一个简单的读取并显示图像的程序:

show-img.cpp

  1. #include <iostream>
  2. #include <opencv2/opencv.hpp>
  3. int main(int argc, char* argv[])
  4. {
  5. std::string filename;
  6. if (argc == 2) { filename = argv[1]; }
  7. else {
  8. std::cout << "Input Filename: ";
  9. std::cin >> filename;
  10. }
  11. cv::Mat img = cv::imread(filename);
  12. if (!img.empty()) {
  13. std::cout << "Image loaded successfully.\n";
  14. cv::namedWindow(filename, cv::WINDOW_NORMAL);
  15. cv::imshow(filename, img);
  16. cv::waitKey(0);
  17. } else {
  18. std::cout << "Unable to open '" << filename << "'.\n";
  19. }
  20. return 0;
  21. }

参考链接:

然后,我们可以使用find_package命令:

  1. find_package(OpenCV REQUIRED)

REQUIRED顾名思义,表示这个包是必须的,没有它不行;如果没有找到,则会中断进程,抛出错误信息。

可以在后边跟一个MODULE,表示告诉find_package使用“Module”模式寻找指定的包。该模式下,CMake会寻找一个名叫Find<PackageName>.cmake的文件,在该例下就是FindOpenCV.cmake。首先,CMake会在CMAKE_MODULE_PATH寻找这个文件,默认情况下这个变量是空的;然后在安装CMake时提供的“寻找单元(Find Modules)”中寻找。

在不指定REQUIRED的情况下,CMake会先使用“Module”模式寻找,如果寻找无果,则会进入另一种“Config”模式。这种模式下,CMake则会寻找<PackageName>Config.cmake文件,在该例下也就是OpenCVConfig.cmake文件。

用户也可以将CMAKE_FIND_PACKAGE_PREFER_CONFIG的值设置成TRUE,这样find_package优先使用“Config”模式。当用户想要使用了一个常见库的自己编译的版本时,这项功能可能会比较实用。

也可以直接指定CONFIG模式,或者使用其同义词NO_MODULE,这样只会进行“Config”模式的搜索。

查找成功后,CMake会把<PackageName>_DIR 变量的值设置为其找到.cmake文件的路径,而文件一般会提供一些有用的信息,以变量的形式供用户使用。

比如OpenCV会设置OpenCV_INCLUDE_DIRSOpenCV_LIBS等变量,OpenCV_INCLUDE_DIRS是OpenCV的包含目录路径,OpenCV_LIBS则包含链接所需的OpenCV的库文件。

注:虽然成功找到 OpenCV 的安装之后,会写入OpenCV_DIRS变量,但并不需要使用include_directories(${OpenCV_INCLUDE_DIRS})命令。

所以,我们的CMakeLists.txt可以这样编写:

  1. cmake_minimum_required(VERSION 3.13)
  2. project(opencv_test)
  3. find_package(OpenCV REQUIRED)
  4. add_executable(show_img show_img.cpp)
  5. target_link_libraries(show_img ${OpenCV_LIBS})

如果正确安装了OpenCV,理论上就能找到。

也可以给<PackageName>_DIR 变量指定值,则CMake会先在指定路径下寻找.cmake文件。

install

https://blog.csdn.net/ktigerhero3/article/details/68941252/

configure_file

其他

参见 CMake in VS Code