和Ai一起学习CMake(一)

和Ai一起学习CMake

现在人工智能爆火,ChatGPT、new bing等层出不穷。我们借助Ai来学习一下CMake。下面是我与Ai的问答,这个学习主要是通过Ai来学习,但是防止Ai乱说话,我会结合自身的知识和实际操作给出相应的补充。

我的环境如下:

  • 操作系统:Windows11
  • CMake版本:3.25
  • 默认生成器:Visual Studio 17 2022
  • 默认编译器:MSVC
  • 终端:Powershell
  • 编辑器:nvim、notepad--、vscode

如果你的环境和我的不一样,那么出现的问题可能就会不一样,要学会自己修改。关于编辑器,前期建议不要使用vscode,因为vscode会有提示,前期应该是打基础的时候,自己手动打命令这样才能熟练,当你开始觉得已经熟悉了CMake的命令语法,就可以使用vscode,这样可以提高效率,每个人的掌握的速度不一样,因人而异。

CMake的介绍

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。CMake能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。CMake不直接编译软件,而是结合原生构建系统来构建软件。

CMake的产生可以追溯到20世纪90年代,当时开发人员使用的构建工具很有限,需要为不同的平台编写不同的Makefile,这极大地增加了软件开发的难度和复杂性。为了解决这个问题,Bill Hoffman于2000年创建了CMake项目,并开源发布,以便于更多的人使用和贡献。

CMake使用平台无关的配置文件来控制软件编译过程,并生成可在您选择的编译器环境中使用项目文件,比如可以生成vs项目文件或者makefile。

下载与安装CMake

下载直接到CMake官网下载即可,就不过多细说。当下载完CMake之后会得到一个压缩包,我们安装CMake其实就是去解压这个压缩包。解压之后可以得到下面的内容:

其中D:\mySoft\cmake是我的cmake的安装根目录,bin目录中就是包含了CMake的cmake.exe等可执行文件,然后也包含了带图形界面的cmake工具。doc目录就是存放文档的,man就是存放手册的。然后还有一个share文件夹,里面也存放了一些东西。我们无需关心除了bin以外的文件夹,这些文件夹就相当于对CMake的一些相关的概念和使用手册一样。我们主要关心的是bin目录。点开bin目录可以看到下面的内容:

其中,cmake.exe就是我们经常会使用到的cmake命令,它是重头戏。然后还有一个cmake-gui.exe,这个就是图形化的cmake,它的特点就是直观和易用,但是并不是我们学习CMake的主要部分。然后还有一个ctest.exe这个是用来测试的,然后还有另外的两个可执行程序。我们只使用CMake的命令行部分,也只学习构建编译的部分,其他的相关内容可以去CMake的官网学习。

配置CMake环境

前面知识介绍了CMake的一些组成。因为CMake相当于是Windows的第三方软件,Windows操作系统并不认识CMake,这就意味着,我们不可以全局地访问CMake,也就不能直接使用CMake,所以我们需要把CMake配置到环境变量中。

我以我当前CMake的安装路径为例。我把D:\mySoft\cmake\bin配置到系统环境变量path中。具体操作如下:

win键->输入“环境”->打开->环境变量->系统变量->path->新建->粘贴(D:\mySoft\cmake\bin)->一路点击确定关掉所有窗口。

这个过程很类似其他软件的环境变量的配置,例如Java的JAVA_HOMECLASSPATH的配置,还有像MinGW的配置,还有MySQL的配置。练多了就会了,然后有时候可能会出现冲突的,则需要点击上面最后一张图中的上移下移操作即可。

配置好了之后在终端输入cmake --version查看一下cmake的版本号,如果出现下面的输出则说明CMake的配置已经完成了,如果不是,则是没有配置好,请重新检查一下步骤。

PS D:\> cmake --version
cmake version 3.25.0-rc1

CMake suite maintained and supported by Kitware (kitware.com/cmake).

因为版本不同而输出的结果不一样那也无伤大雅,只要不少说找不到命令就行。

我与Ai的问答

在学习CMake之前,我们先制定一下学习计划,以便我们是有目标地学习,而不是盲目的学习。毕竟有目标的学习我们会知道要学什么,我们也会学比较实用的,而不是学了不用。

学习计划

结合最新的人工智能来一起学习是最快的,效率翻倍。但是需要有经验的人来引导Ai,要不然会被Ai带入死胡同。所以由我来带领大家一起来和Ai学习CMake,我的作用就是引导和纠正。我这里使用的Ai是new bing.

我:

我需要的是学习CMake的学习计划,请你帮我制定一个CMake的学习计划。

new bing:

好的,以下是一个简单的CMake学习计划:

  1. 学习CMake的基本语法和命令,包括CMakeLists.txt文件的编写方法。
  2. 学习如何使用CMake编译一个简单的程序,例如Hello World。
  3. 学习如何使用CMake编译一个包含多个源文件的程序。
  4. 学习如何使用CMake编译一个包含头文件和库文件的程序。
  5. 学习如何使用CMake编译一个跨平台的程序。
  6. 学习如何使用CMake安装一个项目。
  7. 学习如何使用CMake编译一个包含测试的程序。

你可以参考以下链接来学习CMake:

希望这些信息能帮到你!

源: 与必应的对话, 2023/4/30
(1) cmake学习总结(一) - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/150869814.
(2) CMake简明教程(中文版) - 知乎 - 知乎专栏. https://zhuanlan.zhihu.com/p/492932151.
(3) cmake使用详细教程(日常使用这一篇就足够了) - CSDN博客. https://blog.csdn.net/iuu77/article/details/129229361.

补充:上面是new bing给出的回答。它告诉了我们CMake的学习计划,我们等下将结合它的学习计划对点学习CMake。并且new bing给出了学习CMake的参考链接,这些链接都是前人写好了的,可以拿来参考。最后一块是new bing的搜索链接。

学习计划的第一点:CMake的基本语法和命令

先来了解一下CMake的一些基本的语法和命令。CMake的语法规则是从上往下执行的。特别是后面学习到的命令是一定要有顺序要求的,例如CMake命令中带target命令要放到带add命令的后面,这应该很明显。因为只有先生成了target才能对target做其他操作,即先增加目标,然后才对目标做其他操作。如果想在cmake构建项目的时候打印输出一些信息就可以使用message命令(后面会提到用法)。set命令一般只有两个参数,其含义就是把后面的值赋值给前面的值。前面的值一般都是CMake内置的变量,或者是我们自己自定义的变量,所以set命令更像是我们学习编程语言中的赋值操作一样,只是这个set是CMake的语法而已。

接下来有请new bing.

我:

好,那么开始学习CMake。先从你给的计划中的第一个开始吧。CMake的基本语法和命令有哪些,写CMake时有什么要求,文件应该怎么样命名,CMake的源文件和C++的源文件的位置关系是什么,请你使用精炼的语言回答。

new bing:

CMake的基本语法和命令十分简单,具有以下要点:

  1. CMake由指令(command)和注释组成,空白、换行、tab仅作为语素分隔符。
  2. 注释以井号——“#”开头,至行尾结束。
  3. 指令后加小括号,用于传入参数,指令按照从上至下顺序进行解析。
  4. CMakeLists.txt文件是CMake的构建脚本,用于描述如何构建一个项目。
  5. CMakeLists.txt文件应该放在项目的根目录下。
  6. CMakeLists.txt文件中应该包含project指令,用于定义项目名称和版本号。
  7. CMakeLists.txt文件中应该包含add_executable或add_library指令,用于定义可执行文件或库文件。
  8. CMakeLists.txt文件中应该包含target_link_libraries指令,用于链接库文件。

希望这些信息能帮到你!

补充:

  • CMake中没有多行注释,但是可以使用多个单行注释来实现多行注释的效果,CMake的注释是以#号开头。
  • CMake要求工程主目录和所有存放源代码子目录下都要编写 CMakeLists.txt 文件,源文件必须命名为CMakeLists.txt
  • target_link_libraries命令应该放在add_executable之后,用于指明连接进来的库。官方推荐使用这个接口,而不推荐使用link_libraries,因为link_libraries需要放到add_executable之前。

学习计划二:CMake的简单模板

上一小节只是了解了CMake的一些基本的命令和基本的语法规则,那么从这一小节开始,我们就开始使用CMake去构建我们的C++项目,那么我们先来一个最简单的CMake的模板。就是基本写CMake都要有的命令,就类似学C语言是的main函数一样。

我:

请你给出一个简单的CMakeLists.txt的模板。

new bing:

下面是一个简单的CMakeLists.txt模板,你可以根据自己的需要进行修改:

# 设置CMake的最小版本
cmake_minimum_required(VERSION 3.0)
# 设置项目的名称,这里的名称是 my_project
project(my_project)
# 设置C++的标准版本为C++11
set(CMAKE_CXX_STANDARD 11)
# 生成可执行文件my_project.exe 其中main.cpp是依赖的C++源文件
add_executable(my_project main.cpp)	

希望这个回答能够帮到你!

我们利用这个模板来简单创建一个项目。

在文件加demo下有一个简单的main.cpp文件,内容如下:

#include <iostream>
using namespace std;
int main() {
        cout << "Hello, CMake" << endl;
        return 0;
}

创建一个CMakeLists.txt文件,内容如下:

# 设置CMake的最小版本
cmake_minimum_required(VERSION 3.10)
# 设置项目的名称,这里的名称是 my_project
project(demo)
# 设置C++的标准版本为C++11
set(CMAKE_CXX_STANDARD 11)
# 生成可执行文件my_project.exe 其中main.cpp是依赖的C++源文件
add_executable(demo main.cpp)

查看当前项目目录:

PS D:\myCoding\Terminal\C++\demo> ls

    Directory: D:\myCoding\Terminal\C++\demo

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---    2023/4/30 星期日    15:30            313 CMakeLists.txt
-a---    2023/4/30 星期日    15:29            106 main.cpp

我这里是Windows平台下的PowerShell

构建项目

然后现在我们去构建项目。问一下new bing.

我:

我现在在demo文件夹下已经创建并编辑好了main.cppCMakeLists.txt,请问我应该怎么去构建这个项目。

new bing:

你可以使用以下命令来构建你的项目:

mkdir build
cd build
cmake ..
make

其中,mkdir build用于创建一个名为build的文件夹,cd build用于进入这个文件夹,cmake ..用于在这个文件夹下生成Makefile,make用于编译并生成可执行文件。

希望这个回答能够帮到你!

源: 与必应的对话, 2023/4/30
(1) CMake. https://cmake.org/.
(2) Running CMake | CMake. https://cmake.org/runningcmake/.
(3) CMake Tutorial | CMake. https://cmake.org/cmake-tutorial/.

那么我们现在就使用new bing提示我们的答案去构建项目。

当我已经创建好一个build文件夹之后,并进入了文件夹build,然后执行cmake ..,终端输出的结果如下:

PS D:\myCoding\Terminal\C++\demo\build> cmake ..
-- Building for: Visual Studio 17 2022
-- Selecting Windows SDK version 10.0.22000.0 to target Windows 10.0.22621.
-- The C compiler identification is MSVC 19.35.32217.1
-- The CXX compiler identification is MSVC 19.35.32217.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.35.32215/bin/Hostx64/x64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.35.32215/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: D:/myCoding/Terminal/C++/demo/build

从结果中可以看到,默认使用的生成器是Visual Studio 17 2022。因为我这里是Windows平台。但是new bing给我的cmake命令确实基于Mingw的,自然不可以使用make命令(包括mingw32-make也不行)。那我就继续问new bing。

我:

如果我采用的生成器是Windows平台下的Visual Studio 17 2022,请问我应该怎么通过cmake来构建并运行C++项目,我在PowerShell终端,我不使用IDE。

new bing:

你好,你可以在PowerShell终端中使用cmake构建C++项目。你需要先安装cmake和Visual Studio,然后在PowerShell中运行以下命令:

mkdir build
cd build
cmake ..
cmake --build .

这将创建一个名为“build”的文件夹,然后将您的源代码放入其中。接下来,您需要在PowerShell中导航到该文件夹并运行上述命令。这将生成您的可执行文件。

补充:这里的cmake --build .命令是cmake提供的通用的命令,不管使用的是什么生成器都是可以的,而如果使用make命令那必须采用的生成器是GCCmake才行,如是在Windows平台下则使用对应的mingw32-make命令,但是这样做的话就不够通用,因为如果使用不同的生成器来构建项目就要单独去记住对应的生成的命令,太麻烦了,所以使用cmake提供的通用的命令即可。

我:

可不可以解释一下cmake --build .的意思。

new bing:

cmake --build .是CMake的一个命令,用于构建项目。它会在当前目录下查找CMakeLists.txt文件,然后根据其中的指令生成构建系统。这个命令会自动调用构建系统的生成器,如make或ninja等,来编译和链接项目。如果你想指定编译器、编译选项、链接选项等,可以在cmake --build .后面加上相应的参数。例如,如果你想使用Release模式编译项目,可以使用cmake --build . --config Release命令。

那我们就采用cmake --build .命令去生成项目的可执行文件。

PS D:\myCoding\Terminal\C++\demo\build> cmake --build .
MSBuild version 17.5.1+f6fdcf537 for .NET Framework

  Checking Build System
  Building Custom Rule D:/myCoding/Terminal/C++/demo/CMakeLists.txt
  main.cpp
  demo.vcxproj -> D:\myCoding\Terminal\C++\demo\build\Debug\demo.exe
  Building Custom Rule D:/myCoding/Terminal/C++/demo/CMakeLists.txt

那么从终端的打印信息可以看到,确实生成了对应的demo.exe文件。我们进入对应的目录去运行它。

PS D:\myCoding\Terminal\C++\demo\build> cd D:\myCoding\Terminal\C++\demo\build\Debug
PS D:\myCoding\Terminal\C++\demo\build\Debug> .\demo.exe
Hello, CMake

结果成功输出了Hello, CMake.

切换生成器

我们上面构建项目的时候采用的是Windows平台下默认的Visual Studio,那可不可以切换成Mingw的生成器呢。

我:

我当前的操作系统是Windows,它默认使用的生成器是Visual Studio,如果我想切换到Mingw,那么我的cmake命令应该如何写?

new bing:

如果你想在Windows上使用Mingw作为编译器,可以在运行cmake命令时指定生成器为MinGW Makefiles。例如,你可以使用以下命令来生成Makefile并编译项目:

cmake -G "MinGW Makefiles" -DCMAKE_C_COMPILER=gcc.exe -DCMAKE_CXX_COMPILER=g++.exe ..
cmake --build .

其中,-DCMAKE_C_COMPILER=gcc.exe-DCMAKE_CXX_COMPILER=g++.exe分别指定了C和C++编译器为gcc和g++。如果你的Mingw安装在非默认路径下,还需要指定Mingw的安装路径。

其中,如果你不知道-G后面的字符串应该写什么的话,你可以直接输入cmake -G回车,这样就可以看到你的操作系统支持的生成器,并且知道它们的名字和使用说明。

例如我的操作系统支持的生成器如下所示:

PS D:\> cmake -G
CMake Error: No generator specified for -G

Generators
* Visual Studio 17 2022        = Generates Visual Studio 2022 project files.
                                 Use -A option to specify architecture.
  Visual Studio 16 2019        = Generates Visual Studio 2019 project files.
                                 Use -A option to specify architecture.
  Visual Studio 15 2017 [arch] = Generates Visual Studio 2017 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 14 2015 [arch] = Generates Visual Studio 2015 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 12 2013 [arch] = Generates Visual Studio 2013 project files.
                                 Optional [arch] can be "Win64" or "ARM".
  Visual Studio 11 2012 [arch] = Deprecated.  Generates Visual Studio 2012
                                 project files.  Optional [arch] can be
                                 "Win64" or "ARM".
  Visual Studio 9 2008 [arch]  = Generates Visual Studio 2008 project files.
                                 Optional [arch] can be "Win64" or "IA64".
  Borland Makefiles            = Generates Borland makefiles.
  NMake Makefiles              = Generates NMake makefiles.
  NMake Makefiles JOM          = Generates JOM makefiles.
  MSYS Makefiles               = Generates MSYS makefiles.
  MinGW Makefiles              = Generates a make file for use with
                                 mingw32-make.
  Green Hills MULTI            = Generates Green Hills MULTI files
                                 (experimental, work-in-progress).
  Unix Makefiles               = Generates standard UNIX makefiles.
  Ninja                        = Generates build.ninja files.
  Ninja Multi-Config           = Generates build-<Config>.ninja files.
  Watcom WMake                 = Generates Watcom WMake makefiles.
  CodeBlocks - MinGW Makefiles = Generates CodeBlocks project files.
  CodeBlocks - NMake Makefiles = Generates CodeBlocks project files.
  CodeBlocks - NMake Makefiles JOM
                               = Generates CodeBlocks project files.
  CodeBlocks - Ninja           = Generates CodeBlocks project files.
  CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
  CodeLite - MinGW Makefiles   = Generates CodeLite project files.
  CodeLite - NMake Makefiles   = Generates CodeLite project files.
  CodeLite - Ninja             = Generates CodeLite project files.
  CodeLite - Unix Makefiles    = Generates CodeLite project files.
  Eclipse CDT4 - NMake Makefiles
                               = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - MinGW Makefiles
                               = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
  Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
  Kate - MinGW Makefiles       = Generates Kate project files.
  Kate - NMake Makefiles       = Generates Kate project files.
  Kate - Ninja                 = Generates Kate project files.
  Kate - Unix Makefiles        = Generates Kate project files.
  Sublime Text 2 - MinGW Makefiles
                               = Generates Sublime Text 2 project files.
  Sublime Text 2 - NMake Makefiles
                               = Generates Sublime Text 2 project files.
  Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
  Sublime Text 2 - Unix Makefiles
                               = Generates Sublime Text 2 project files.

其中生成器前面带有*号的表示默认的生成器,例如我这里默认的生成器是Visual Studio 17 2022.

那么我的电脑上是装有MinGW的,那么我切换到MinGW来试试看。我们把demo文件夹复制一份,然后清空build目录下的所有内容。

PS D:\myCoding\Terminal\C++> cp demo/* demo2/
PS D:\myCoding\Terminal\C++> cd .\demo2\
PS D:\myCoding\Terminal\C++\demo2> rm build/*
PS D:\myCoding\Terminal\C++\demo2> ls

    Directory: D:\myCoding\Terminal\C++\demo2

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----     2023/5/1 星期一    14:15                build
-a---    2023/4/30 星期日    16:14            112 CMakeLists.txt
-a---    2023/4/30 星期日    15:29            106 main.cpp

然后依次执行下面的命令:

cmake -G "MinGW Makefiles" ..
mingw32-make
.\demo2.exe

终端打印结果如下:

PS D:\myCoding\Terminal\C++\demo2\build> cmake -G "MinGW Makefiles" ..
-- The C compiler identification is GNU 8.1.0
-- The CXX compiler identification is GNU 8.1.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: D:/mySoft/mingw64-8.1.0/bin/gcc.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: D:/mySoft/mingw64-8.1.0/bin/c++.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: D:/myCoding/Terminal/C++/demo2/build
PS D:\myCoding\Terminal\C++\demo2\build> mingw32-make
[ 50%] Building CXX object CMakeFiles/demo2.dir/main.cpp.obj
[100%] Linking CXX executable demo2.exe
[100%] Built target demo2
PS D:\myCoding\Terminal\C++\demo2\build> .\demo2.exe
Hello, CMake

如果版本太高的话可能回导致版本不兼容的问题,所以尽量不要使用最新的Mingw的版本。我这里使用的版本是8.1.0.

从终端的输出中可以看到,使用mingw32-make成功生成了可执行文件demo2.exe,然后去运行它成功输出结果。

学习计划三:CMake编译一个包含多个源文件的程序

前面的例子中只是一个简单的源文件,内容太过于单调,接下来开始进入多个源文件的阶段。

例如:在含有主函数的C++源文件中,写一些四则运算的函数声明,然后对应创建4个C++源文件。最后在主函数中调用这4个函数。

  • main.cpp
#include <iostream>
using namespace std;

double add(double a, double b);
double sub(double a, double b);
double mult(double a, double b);
double div(double a, double b);

int main() {
        double result;
        double a = 3;
        double b = 8;
        result = add(a, b);
        cout << "add: " << result << endl;
        result = sub(a, b);
        cout << "sub: " << result << endl;
        result = mult(a, b);
        cout << "mult: " << result << endl;
        result = div(a, b);
        cout << "div: " << result << endl;
        return 0;
}
  • add.cpp
double add(double a, double b) {
        return a + b;
}
  • sub.cpp
double sub(double a, double b) {
        return a - b;
}
  • mult.cpp
double mult(double a, double b) {
        return a * b;
}
  • div.cpp
double div(double a, double b) {
        if (b < 1e-8) {
                std::cout << "除数不能为零" << std::endl;
                return -1;
        }
        return a / b;
}

问一下newbing,我们的CMakeLists.txt应该怎么写。

我:

如果我现在需要使用CMAKE编译构建多个C++源文件,那么我的CMakeLists.txt应该怎么写呢。

new bing:

如果你需要使用CMake编译构建多个C++源文件,你可以在CMakeLists.txt文件中添加以下命令:

add_executable(my_program main.cpp file1.cpp file2.cpp)

这将会编译并链接main.cpp、file1.cpp和file2.cpp文件,生成可执行文件my_program。你可以根据需要添加更多的源文件。如果你的源文件分布在不同的目录中,你可以使用aux_source_directory命令来添加它们。希望这能帮到你。如果你还有其他问题,请随时问我。

那我们就通过newbing的提示去写一下CMakeLists.txt.具体内容如下:

cmake_minimum_required(VERSION 3.20)
project(demo3)
set(CMAKE_CXX_STANDARD 11)
add_executable(${PROJECT_NAME} main.cpp add.cpp sub.cpp mult.cpp div.cpp)

其中${PROJECT_NAME}表示获取project()中的项目名称,即我们生成的可执行文件是项目名.exe

然后我们去构建项目。

cd build
cmake ..
cmake --build .

如果生成的过程中有报错,请自行检查语法写对了没有。我这里是写对了,然后成功生成了可执行文件。demo3.exe.

然后去运行它。

PS D:\myCoding\Terminal\C++\demo3\build\Debug> .\demo3.exe
add: 11
sub: -5
mult: 24
div: 0.375

我这里使用的MSVC编译器,即采用默认的Visual Studio生成器,因为懒得多打一个-G.

可见运行出的结果是没有问题的。

但是这个CMakeLists.txt的写法并不好,因为当有多个源文件的时候也这样写,首先看起来就特别的难看,然后维护起来也比较麻烦,当然IDE会自动帮我们这样添加,但是我还是觉得这样的CMakeLists.txt还是不够好。

那么其实刚刚new bing也给出了一个好一点的提示,就是告诉我们可以使用aux_source_directory命令来添加源文件。

但通过我和newbing的对话中,newbing告诉我aux_source_directory是一个旧命令了。

我:

那么当我的项目的C++源文件多了之后,依次手动罗列太麻烦了,有没有好一点的CMakeLists.txt的写法。

new bing:

是的,你可以使用以下命令来自动添加所有源文件和头文件:

file(GLOB SOURCES "*.cpp")
file(GLOB HEADERS "*.h")
add_executable(MyApp ${SOURCES} ${HEADERS})

这将会自动添加所有的.cpp和.h文件。你可以根据需要修改MyApp为你的项目名称。希望这能帮到你。如果你还有其他问题,请随时问我。

我:

这个写法我倒是第一次见,那么请问这样的写法和使用aux_source_directory有什么区别吗,哪个更有优势一点。

new bing:

aux_source_directory是一个旧的命令,它已经被弃用了。使用file(GLOB SOURCES "*.cpp")file(GLOB HEADERS "*.h")命令可以自动添加所有源文件和头文件。这种方法更加简单,而且不需要手动罗列每个文件。希望这能回答你的问题。如果你还有其他问题,请随时问我。

那我们学新不学旧,就使用新的命令来更新我们的CMakeLists.txt.更新如下:

cmake_minimum_required(VERSION 3.20)
project(demo3)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
add_executable(${PROJECT_NAME} ${SOURCES})

因为我们还没有使用头文件,所以先这样写。

然后采用同样的方式先把build目录下的文件都删除,然后重新构建项目。命令都是重复的就不再赘述了。

重新构建项目并编译运行之后,结果是一样的。那么以后就推荐使用这个写法了,就不再使用旧版本的写法了。新的写法语义看起来也比较明确。

虽然file命令看起来非常容易理解,但我们还是问一下newbing它的意思吧。

我:

请你详细告诉我file命令的具体语法及其含义。

new bing:

在CMake中,它通常用于查找源文件和头文件。以下是一些常见的用法:

  • file(GLOB SOURCES "*.cpp"): 查找所有的.cpp文件并将它们存储在SOURCES变量中。
  • file(GLOB_RECURSE SOURCES "*.cpp"): 查找所有的.cpp文件并将它们存储在SOURCES变量中,包括子目录。
  • file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}): 复制源目录中的data目录到二进制目录中。

希望这能回答你的问题。如果你还有其他问题,请随时问我。

OK,那么到了这一步,你已经学会使用CMake编译多个源文件的项目了。但这还只是冰山一角。

学习计划四:CMake编译一个包含头文件和库文件的程序

引入头文件

前面的例子中只有多个源文件,并没有引入头文件的。那么接下来我们就开始引入头文件。然后CMakeLists.txt对应写法其实在上一小节new bing已经告诉我们了。

那就快速写一下:

  • main.cpp
#include "mymath.h"
#include <iostream>
using namespace std;

int main() {
        double result;
        double a = 3;
        double b = 8;
        result = add(a, b);
        cout << "add: " << result << endl;
        result = sub(a, b);
        cout << "sub: " << result << endl;
        result = mult(a, b);
        cout << "mult: " << result << endl;
        result = div(a, b);
        cout << "div: " << result << endl;
        return 0;
}
  • mymath.h
#ifndef _MYMATH_H_
#define _MYMATH_H_
double add(double a, double b);
double sub(double a, double b);
double mult(double a, double b);
double div(double a, double b);
#endif
  • add.cpp
#include "mymath.h"
double add(double a, double b) {
        return a + b;
}
  • sub.cpp
#include "mymath.h"
double sub(double a, double b) {
        return a - b;
}
  • mult.cpp
#include "mymath.h"
double mult(double a, double b) {
        return a * b;
}
  • div.cpp
#include "mymath.h"
#include <iostream>
double div(double a, double b) {
        if (b < 1e-8) {
                std::cout << "除数不能为零" << std::endl;
                return -1;
        }
        return a / b;
}
  • CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(demo4)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADER "*.h")
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADER})

涉及到新的内容说明一下:

  • CMake在获取变量的值是时候采用${变量名}的方式获取。

  • PROJECT_NAME:项目的名称,就是project(demo4)中的demo4.

  • SOURCES:我们自定义的变量名,它表示的意思是我们当前CMakeLists.txt所在目录下的所有.cpp文件。

  • HEADER:我们自定义的变量名,它表示的意思是我们当前CMakeLists.txt所在目录下的所有.h文件。

然后构建项目并编译运行可执行文件。

 cmake ..
 cmake --build .
 cd .\Debug\
 .\demo4.exe

输出结果如下:

add: 11
sub: -5
mult: 24
div: 0.375

结果和前面的是一样的。只不过我们封装了一个头文件。

如果我们想把我们写好的这四个四则运算的函数封装成一个静态库或者动态库,然后发送给别人使用,应该怎么做呢?在CMake中编译成静态库和动态库是比较简单的,但是一开始学习的时候也会遇到一些奇奇怪怪的的bug,当你理解了之后,那么你的技术水平又更上一层楼。那我们接着往下走。

CMake生成和使用静态库

关于库的重要性应该是在学习C++阶段的时候就应该知道了,如果对库的概念没有认识,那么可以看一下new bing的回答。

我:

什么是静态库

new bing:

静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中的这种库。静态库是一组目标文件的集合,可以简单看成是很多目标文件经过压缩打包后形成的一个文件。静态库对函数库的链接是放在编译时期完成的,程序在运行时与函数库再无瓜葛,移植方便。

我:

如果我想把我写好的一些函数封装成一个静态库,我的CMakeLists.txt应该怎么写?

new bing:

你可以在CMakeLists.txt中使用add_library命令来创建静态库。下面是一个示例:

add_library(my_lib STATIC
    src/my_lib.cpp
    include/my_lib.h
)

其中,my_lib是你的静态库的名称,src/my_lib.cpp是你的源文件,include/my_lib.h是你的头文件。这个命令将创建一个名为libmy_lib.a的静态库文件。

我:

我的平台是Windows平台,那么生成的静态库是以.a为后缀的,我可以正常使用吗?

new bing:

在Windows平台上,静态库的文件名通常以.lib为后缀。如果你的静态库文件名以.a为后缀,那么你需要将它重命名为.lib。你可以使用Visual Studio的命令提示符来完成这个操作。下面是一个示例:

ren my_lib.a my_lib.lib

我:

如果直接重命名会怎么样,我还可以正常使用吗?

new bing:

如果你直接重命名,你可能会遇到一些问题。在Windows平台上,静态库和动态库的文件格式是不同的。如果你将一个动态库重命名为静态库,那么你可能会遇到一些链接错误。因此,我建议你使用Visual Studio的命令提示符来将.a文件转换为.lib文件。

那么我们就通过new bing的提示来写一下CMakeLists.txt,然后把我们之前写的mymath.h和它的实现*.cpp封装到静态库mymath.lib中,我这里是Windows平台,所以封装成*.lib,如果是Linux平台则直接是*.a即可。

我们在demo4的CMakeLits.txt基础上做了一点修改,其实就是多了生成静态库的部分,具体内容如下:

cmake_minimum_required(VERSION 3.20)
project(demo4)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADER "*.h")
add_library(mymath STATIC
    add.cpp
    sub.cpp
    mult.cpp
    div.cpp
    mymath.h
)
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADER})

然后再构建项目并编译。可知看到终端输出了生成静态库mymath.lib.因为我使用的是Visual Studio生成器,所以就直接生成了*.lib,不需要额外的转换。如果采用GCC生成的静态库,然后想在Windows平台使用*.a静态库才需要通过new bing的提示进行转换。

mymath.vcxproj -> D:\myCoding\Terminal\C++\demo4\build\Debug\mymath.lib

然后在demo5中去使用一下我们的静态库mymath.lib.创建完demo5文件夹后就把静态库复制过去。

PS D:\myCoding\Terminal\C++\demo4\build\Debug> cp .\mymath.lib  D:\myCoding\Terminal\C++\demo5\

然后把主函数和头文件复制到demo5中,不要把add.cpp等实现文件也复制过来!!!

PS D:\myCoding\Terminal\C++\demo4> cp main.cpp ..\demo5\
PS D:\myCoding\Terminal\C++\demo4> cp mymath.h ..\demo5\

现在demo5的项目目录如下:

PS D:\myCoding\Terminal\C++\demo5> ls

    Directory: D:\myCoding\Terminal\C++\demo5

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---     2023/5/1 星期一    15:46            382 main.cpp
-a---     2023/5/1 星期一    15:45            181 mymath.h
-a---     2023/5/1 星期一    16:23          69716 mymath.lib

然后创建build目录,并在build目录下创建lib文件夹,把我们的静态库都放到这个build/lib下面。

当前项目目录:

PS D:\myCoding\Terminal\C++\demo5> tree . /F
卷 新加卷 的文件夹 PATH 列表
卷序列号为 DAC0-B3A4
D:\MYCODING\TERMINAL\C++\DEMO5
│  CMakeLists.txt
│  main.cpp
│  mymath.h
│
└─build
    └─lib
            mymath.lib

lib文件要这样放,因为构建项目的时候是在build目录下加载静态库这些东西,又要把静态库统一管理所以就把静态库放到build/lib下。

编写CMakeLists.txt,先问一下new bing.

我:

那我现在在另一个项目中使用mymath.lib静态库,我的CMakeLists.txt应该怎么写?

new bing:

你可以使用target_link_libraries命令来链接静态库。下面是一个示例:

add_executable(my_app main.cpp)
target_link_libraries(my_app mymath)

其中,my_app是你的可执行文件的名称,main.cpp是你的源文件,mymath是你的静态库的名称。这个命令将会链接名为libmymath.a的静态库文件。

我:

add_executabletarget_link_libraries有什么顺序要求吗?

这里new bing说错了,我在后面指正出来了,add_executabletarget_link_libraries是有顺序要求的。add_executable一定要在target_link_libraries前面。

new bing:

没有顺序要求。你可以先调用add_executable,然后再调用target_link_libraries,也可以反过来。下面是一个示例:

add_executable(my_app main.cpp)
target_link_libraries(my_app mymath)
target_link_libraries(my_app mymath)
add_executable(my_app main.cpp)

这两种写法都是可以的。

我:

那如果使用的是动态库也没有顺序要求吗?

new bing:

对于动态库,也没有顺序要求。你可以先调用add_executable,然后再调用target_link_libraries,也可以反过来。下面是一个示例:

add_executable(my_app main.cpp)
target_link_libraries(my_app mymath)
target_link_libraries(my_app mymath)
add_executable(my_app main.cpp)

这两种写法都是可以的。

根据new bing的提示,我们将CMakeLists.txt编辑为如下内容:

cmake_minimum_required(VERSION 3.20)
project(demo5)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADER "*.h")
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADER})
target_link_libraries(${PROJECT_NAME} ./lib/mymath) # 这里的 . 表示在build目录下

然后构建项目,编译运行。

cd build
cmake ..
cmake --build .
cd Debug
.\demo5.exe

因为target_link_libraries(${PROJECT_NAME} ./lib/mymath)中的.这个表示的太让人困惑了,所以我们干脆把lib文件夹放在当前项目目录下。例如:

PS D:\myCoding\Terminal\C++\demo5> tree . /F
卷 新加卷 的文件夹 PATH 列表
卷序列号为 DAC0-B3A4
D:\MYCODING\TERMINAL\C++\DEMO5
│  CMakeLists.txt
│  main.cpp
│  mymath.h
│
├─build
└─lib
        mymath.lib

然后对应的CMakeLists.txt应该这样写:

cmake_minimum_required(VERSION 3.20)
project(demo5)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADER "*.h")
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADER})
target_link_libraries(${PROJECT_NAME} ${CMAKE_SOURCE_DIR}/lib/mymath.lib)

其中${CMAKE_SOURCE_DIR}表示的就是根目录下的CMakeLists.txt所在的位置。后面的静态库的名字要写完全。

然后构建项目和编译运行都是一样的结果,就是采用第二种写法的话语义更加明确。

然后前面new bing提到add_executabletarget_link_libraries没有顺序要求,这里虽然知道new bing在胡说八道,但是我们还是来简单试试看。

其他的不用改,我们就只是把CMakeLists.txt中的add_executabletarget_link_libraries的顺序调换一下。

cmake_minimum_required(VERSION 3.20)
project(demo5)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADER "*.h")
target_link_libraries(${PROJECT_NAME} ${CMAKE_SOURCE_DIR}/lib/mymath.lib)
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADER})

然后再去构建编译运行。看看结果。为了避免冲突建议把build目录下的内容都清空。

PS D:\myCoding\Terminal\C++\demo5> rm .\build\*

在执行cmake --build .的时候就报错了:

-- Configuring incomplete, errors occurred!
See also "D:/myCoding/Terminal/C++/demo5/build/CMakeFiles/CMakeOutput.log".

所以new bing还是会乱说话的。还好有我这个有经验的人在。既然报错了就没有必要再往下测试了,就算可以通过什么方式可以挽救回来也没必要,就是简单的顺序问题,遵守就可以了。

CMake生成和使用动态库

C++中的库是分有静态库和动态库之分,那么它们又有什么区别呢?看看new bing是怎么回答的。

我:

动态库是什么?

new bing:

动态库(Dynamic Library)是一种在程序运行时才被载入的库,它的链接是在程序运行时进行的。动态库可以被多个程序共享,因此可以减少程序的内存占用。动态库的优点是可以在不重新编译程序的情况下更新库文件,而且可以减少可执行文件的大小。动态库也称为共享库(Shared Library)。

我:

静态库和动态库有什么区别?

new bing:

静态库和动态库的最大区别是,静态库链接的时候把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度和降低程序的耦合度。静态库被使用目标代码最终和可执行文件在一起(它只会有自己用到的),而动态库与它相反,它的目标代码在运行时或者加载时链接。因此,动态库可以被多个程序共享,因此可以减少程序的内存占用。

我:

CMake中怎么生成动态库。

new bing:

在 CMake 中生成动态库,可以使用 add_library 命令。使用 add_library 命令时,第二个参数指定库的类型,SHARED 表示生成动态库,STATIC 表示生成静态库。例如,下面的代码将生成名为 testDll 的动态库,采用 $ {DIR_TESTDLL_SRCS} 路径下的源码:

add_library (testDll SHARED $ {DIR_TESTDLL_SRCS})

然后我们回到demo4,生成动态库,然后发送给demo6使用。其实在这个地方,因为new bing没有其他想法,所以一直在同一个错误绕,我也是被newbing绕了好久,然后选择去看一下其他人的CMake教程,才跳出来,Windows平台下生成动态库还是有些问题需要解决。错误的那个就简单提一下,然后就写一个正确的放在下面。

被new bing带进去的错误:new bing一直在跟我强调CMakeLists.txt的写法,大体上是对的,但是使用newbing给我的提示去使用它的CMakeLists.txt内容如下:

cmake_minimum_required(VERSION 3.20)
project(demo4)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADER "*.h")
add_library(mymath SHARED
    add.cpp
    sub.cpp
    mult.cpp
    div.cpp
    mymath.h
)

然后构建编译运行,一路没有问题,但是隐含的问题就是,我们的C++源文件并没有做修改,导致了我只能生成*.dll文件,没有生成*.lib文件。在Windows平台下的动态库其实是有两种分别是:

  • .lib结尾的动态库索引(函数名等的地址)
  • .dll结尾的动态库实现(真正的函数实现)

所以通过上面的构建编译后,会发现我们发布给其他程序用不了我们的动态库,因为我们只发送了*.dll,也就是只是发生了真正函数的实现,并没有把这些函数的声明发送,所以其他应用程序是访问不到的,所以就发送链接错误的情况。只有我们把动态库的*.lib文件也生成并发布给其他应用程序,然后把动态库的*.lib(索引)编译到可执行文件中,然后在运行的时候才能动态地访问到*.dll中的内容。我这里一直在强调是动态库的*.lib,而不是静态库,这个是有区别的,单纯的静态库是把声明和实现都编译到*.lib中;而动态库的*.lib是只把声明编译到*.lib,然后实现编译到*.dll中。这个应该很容易理解。当然,只有Windows才是这样分开来做,如果是Linux就不会这样分开。在Linux中*.a就是静态库,*.so就是动态库。

我:

你了解Windows平台下的动态库吗?

new bing:

是的,我知道一些关于Windows平台下的动态库。在Windows下,动态库通常以.dll结尾。动态库一般包含引入库(.lib)文件和动态库(.dll)文件。dll包含函数实现和变量;lib包含该dll的中函数和变量的声明和位置,引入lib库后,就可以找到dll库。编译链接时只需要lib,程序运行时才动态加载dll,将该dll映射到进程的地址空间中,然后访问dll中导出的函数。发布时,需要携带lib和dll文件。编译出的可执行文件较小。

那么原理明白完之后我们就开始去生成Windows平台下的动态库。

因为刚刚提到,因为没有修改C++源文件才导致了指生成*.dll文件,那么我们去重新写C++的源文件。其实主要就在头文件的部分做修改,实现的文件是不用修改的。需要使用的关键字是__declspec。这个关键字是只有微软的MSVC才用的,其他不用,也用不了。

例子:

封装四则运算函数。

  • MyMath.h
#pragma once // MSVC特有的,当然也可以使用#ifndef那套
#ifdef MYMATH_EXPORT // 在CMake生成动态库的时候会定义这个变量,在调用库的时候就不会有
	#define MYMATHLIB_API __declspec(dllexport) // 生成库的时候用
#else
	#define MYMATHLIB_API __declspec(dllimport)	// 调用库的时候用
#endif
extern "C" MYMATHLIB_API double myAdd(double a, double b);
extern "C" MYMATHLIB_API double mySub(double a, double b);
extern "C" MYMATHLIB_API double myMult(double a, double b);
extern "C" MYMATHLIB_API double myDiv(double a, double b);

这样就定义好了头文件,其中上面的一堆宏定义就是重头戏,动态库能否生成和使用就看它们了。

  • MyMath.cpp
#include "MyMath.h"
#include <iostream>

double myAdd(double a, double b) {
    return a + b;
}
double mySub(double a, double b) {
    return a - b;
}
double myMult(double a, double b) {
    return a * b;
}
double myDiv(double a, double b) {
    if (b > 0 && b < 1e-8) {
        std::cout << "除数不能为0" << std::endl;
        return -1;
    }
    return a / b;
}

实现c++文件就正常写就行。然后是CMakeLists.txt的编写,因为是在Windows平台下的,所以在写动态链接的时候还是有不一样的地方。

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyMath)
set(CMAKE_CXX_STANDARD 11)
add_library(
	MyMath_shared SHARED
	MyMath.cpp
	MyMath.h
)
# 导出 MYMATH_EXPORT 宏,供生成动态库的头文件使用
target_compile_definitions(MyMath_shared PRIVATE MYMATH_EXPORT)

为了区别静态库(主要静态库和动态库都会生成*.lib),所以我们动态库最好命名时加上_shared,当然这不是必要的,但是静态库和动态库的名字不可以一样,否则会报错,因为在Windows下链接器在链接的时候会分不清。为了后期的维护我们还是使用良好的命名比较好。写好了之后我们就去构建编译运行。

cmake -S . -B build	# 在当前目录下找到CMakeLists.txt并把构建内容放到build目录下
cmake --build build # 构建build

以后我们在构建编译的时候都使用上面两个cmake命令,比较简单和方便。然后就生成好了*.lib*.dll文件。可以在Debug目录下看到如下内容:

PS D:\myCoding\Terminal\C++\demo6\build\Debug> ls

    Directory: D:\myCoding\Terminal\C++\demo6\build\Debug

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---     2023/5/2 星期二    21:27          58368 MyMath_shared.dll
-a---     2023/5/2 星期二    21:27            977 MyMath_shared.exp
-a---     2023/5/2 星期二    21:27           2310 MyMath_shared.lib
-a---     2023/5/2 星期二    21:27        1101824 MyMath_shared.pdb
  • MyMath_shared.dll是我们实际的动态库
  • MyMath_shared.exp是我们的导出文件,就是到出宏MYMATH_EXPORT
  • MyMath_shared.lib是我们的动态库索引
  • MyMath_shared.pdb用来调试的文件

发布给其他项目的时候需要把MyMath_shared.dllMyMath_shared.libMyMath.h一起发布。我们创建一个demo7项目(文件夹),然后把我们的动态库(包括*.dll*.lib*.h,以后提到的动态库默认就是有它们三个)放到demo7的lib文件夹中。我们当前的demo7项目目录如下:

PS D:\myCoding\Terminal\C++\demo7> tree . /F
卷 新加卷 的文件夹 PATH 列表
卷序列号为 DAC0-B3A4
D:\MYCODING\TERMINAL\C++\DEMO7
│  CMakeLists.txt
│  TestMyMath.cpp
│
└─lib
        MyMath.h
        MyMath_shared.dll
        MyMath_shared.lib

我们在TestMyMath.cpp中使用一下我们写好的动态库。

  • TestMyMath.cpp
#include "MyMath.h"
#include <iostream>
using namespace std;

int main() {
	double result;
	double a = 4;
	double b = 6;
	result = myAdd(a, b);
	cout << "add: " << result << endl;
	result = mySub(a, b);
	cout << "sub: " << result << endl;
	result = myMult(a, b);
	cout << "mult: " << result << endl;
	result = myDiv(a, b);
	cout << "div: " << result << endl;
	return 0;
}
  • CMaktLists.txt
cmake_minimum_required(VERSION 3.20)
project(TestMyMath)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
include_directories(${CMAKE_SOURCE_DIR}/lib)
link_directories(${CMAKE_SOURCE_DIR}/lib)
add_executable(${PROJECT_NAME} ${SOURCES})
target_link_libraries(${PROJECT_NAME} PRIVATE MyMath_shared)

从上面的代码中可以看到我使用了include_directories命令,而不用file。也挺有意思的,可以看一下new bing的回答。

我:

为什么是使用file(GLOB HEADERS "lib/*.h")报错了,而使用include_directories(${CMAKE_SOURCE_DIR}/lib)就正确了.

new bing:

file(GLOB HEADERS "lib/*.h")命令用于将lib目录下所有的.h文件添加到一个名为HEADERS的变量中。这个命令并不会将lib目录添加到编译器的包含路径中,所以它不能用来让编译器找到你的头文件。

如果你想要让编译器能够找到你的头文件,你应该使用include_directories函数来将包含头文件的目录添加到编译器的包含路径中。例如,你可以这样使用include_directories函数:

include_directories(${CMAKE_SOURCE_DIR}/lib)

在上面的示例中,我们使用了include_directories函数来将项目目录下的lib文件夹添加到编译器的包含路径中。这样,当你在C++代码中包含头文件时,编译器就能够在这个目录中查找头文件。

我:

include_directories不是过时了吗?CMake官方好像不再推荐使用它了

new bing:

是的,你说得对。include_directories函数已经过时了,CMake官方不再推荐使用它。现在,CMake官方推荐使用更加现代化的target_include_directories函数来管理目标的包含路径。

target_include_directories函数允许你为每个目标单独指定包含路径。这样,你就可以更好地控制每个目标的编译选项,避免不同目标之间的冲突。下面是一个简单的示例,它展示了如何使用target_include_directories函数来为一个名为MyExecutable的可执行文件指定包含路径。

cmake_minimum_required(VERSION 3.5)

# 设置项目名称
project(MyExecutable)

# 创建一个名为 MyExecutable 的可执行文件
add_executable(MyExecutable main.cpp)

# 为 MyExecutable 目标指定包含路径
target_include_directories(MyExecutable PRIVATE ${CMAKE_SOURCE_DIR}/lib)

在上面的示例中,我们使用了target_include_directories函数来为MyExecutable目标指定包含路径。这样,在编译MyExecutable目标时,编译器就会在项目目录下的lib文件夹中查找头文件。

那我们就修改一下上面的CMakeLists.txt,具体内容如下:

cmake_minimum_required(VERSION 3.20)
project(TestMyMath)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
add_executable(${PROJECT_NAME} ${SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib)
target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib/MyMath_shared.lib)

因为CMake也是从上往下读的,所以命令中前面带有target的都要放到add_executable之后,因为只有生成了目标然后才能对目标做其他操作。

注意target_link_libraries命令的当前写法是Windows的特有写法,必须完全指明,否则你就会得到这个错误:

LINK : fatal error LNK1104: 无法打开文件“MyMath_shared.lib” [D:\myCoding\Terminal\C++\demo7\build\TestMyMath.vcxproj]

并不是lib目录下同时有*.dll*.lib而导致的,你把*.dll移到别的地方在重新构建编译也是得到上面的报错。一切都准备就绪后就开始构建编译运行。如果终端打印下面的内容,所以编译成功了。

 TestMyMath.vcxproj -> D:\myCoding\Terminal\C++\demo7\build\Debug\TestMyMath.exe

然后运行看看。

PS D:\myCoding\Terminal\C++\demo7> .\build\Debug\TestMyMath.exe
PS D:\myCoding\Terminal\C++\demo7>

运行了个寂寞,啥也没有对吧。然后仔细检查一下TestMyMath.cpp文件,发现写的没有问题。这是我故意挖的坑。因为我前面说过动态库是动态加载的,我们是生成了可执行文件,但是并没有把动态库放在可执行文件的旁边,所以,可执行文件在运行的时候就找不到对应的函数实现,所以就运行了个寂寞。这个时候我们需要把我们的动态库(*.dll)放到*.exe文件的旁边,然后再运行看看。

PS D:\myCoding\Terminal\C++\demo7> .\build\Debug\TestMyMath.exe
add: 10
sub: -2
mult: 24
div: 0.666667

这样就没有问题了。上面是对函数的封装,那如果对类封装应该怎么写呢?其实都是一样的,也需要使用到__declspec关键字,只是这个关键字应该放在class类名中间。例如:

class __declspec(export) MyClass
{
public:
    MyClass();
    ~MyClass();
    void test();
};

这样的写法只适合给生成动态库的时候使用在调用库的时候没有必要用__declspec(export),如果使用了就对应也会生成可执行文件的*.lib,这个是没有必要的,也就是说__declspec(export)是生成库的时候使用,而调用库的时候应该使用__declspec(import)。所以我们之前的宏定义就是在干这件事情,之前看不懂这些宏是干嘛用的,现在应该知道了。

例如:封装一个MyClass

  • MyClass.h
#pragma once
#ifdef MYCLASS_EXPORT
	#define MYCLASSLIB_API __declspec(export)
#else
	#define MYCLASSLIB_API __declspec(import)
#endif
class MYCLASSLIB_API MyClass
{
public:
    MyClass();
    ~MyClass();
    void test();
};

然后创建MyClass.cpp给对应的函数进行对应的实现。需要注意的是,在封装动态库的时候,MyClass.cpp中要把MyClass.h涉及到的所有函数都要实现,否则在生成动态库的时候就会报错所动态库找不到对应的实现。

关于静态库和动态库的内容还是比较重要的,我花了挺多的精力和时间在这上面,请你务必体会我的良苦用心。

学习计划五:CMake编译一个跨平台的程序

上一小节中,我们是在Windows平台下完成的,如果在Linux平台就用不了,需要做点修改,才能进行跨平台开发。

C++源码级别上的修改

我们前面在封装动态库的时候,头文件做了一堆宏定义,在Windows下,MSVC认识这个东西,但是换到Linux上,又不认识这个东西了,所有就很头疼。有没有跨平台的通用的写法呢?当然有,我们问一下new bing.

我:

C++中怎么写跨平台的代码,我记得在Windows下有一个_Win32宏,用来标识是Windows平台下的,但是我记得不太清楚,你可以帮我补充一下吗?

new bing:

在C++中编写跨平台代码通常需要避免使用特定于平台的函数和库。您提到的_Win32宏是一个预处理器宏,用于在Windows平台上编译时定义。您可以使用预处理器指令来检查这个宏是否已定义,然后根据需要编写特定于平台的代码。例如:

#ifdef _WIN32
// Windows-specific code
#else
// Non-Windows code
#endif

这样,当您在Windows平台上编译代码时,只有Windows-specific code部分会被编译;而在其他平台上编译时,只有Non-Windows code部分会被编译。

OK,知道了这个之后,我们在Linux操作系统上生成一下上一小节的动态库,看一下会报什么错误。

我的环境:

  • 操作系统:WSL(Windows Subsystem for Linux)Unbuntu 20.04
  • 编译器:GCC(这里是大写,表示包含gcc、g++等)
  • 生成器:make

源文件和头文件还是上一小节的,我们原封不动的放到linux环境下编译的时候就会报下面的错误。

cukor_unix@WIN-L1ECGP4JN8T:~/myCoding/Cpp/Demo6$ cmake -S . -B build
-- The C compiler identification is GNU 11.3.0
-- The CXX compiler identification is GNU 11.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/cukor_unix/myCoding/Cpp/Demo6/build
cukor_unix@WIN-L1ECGP4JN8T:~/myCoding/Cpp/Demo6$ cmake --build build
[ 50%] Building CXX object CMakeFiles/MyMath_shared.dir/MyMath.cpp.o
In file included from /home/cukor_unix/myCoding/Cpp/Demo6/MyMath.cpp:1:
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:5:41: error: expected constructor, destructor, or type conversion before ‘(’ token
    5 |         #define MYMATHLIB_API __declspec(dllexport)
      |                                         ^
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:10:12: note: in expansion of macro ‘MYMATHLIB_API’
   10 | extern "C" MYMATHLIB_API double myAdd(double a, double b);
      |            ^~~~~~~~~~~~~
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:5:41: error: expected constructor, destructor, or type conversion before ‘(’ token
    5 |         #define MYMATHLIB_API __declspec(dllexport)
      |                                         ^
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:11:12: note: in expansion of macro ‘MYMATHLIB_API’
   11 | extern "C" MYMATHLIB_API double mySub(double a, double b);
      |            ^~~~~~~~~~~~~
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:5:41: error: expected constructor, destructor, or type conversion before ‘(’ token
    5 |         #define MYMATHLIB_API __declspec(dllexport)
      |                                         ^
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:12:12: note: in expansion of macro ‘MYMATHLIB_API’
   12 | extern "C" MYMATHLIB_API double myMult(double a, double b);
      |            ^~~~~~~~~~~~~
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:5:41: error: expected constructor, destructor, or type conversion before ‘(’ token
    5 |         #define MYMATHLIB_API __declspec(dllexport)
      |                                         ^
/home/cukor_unix/myCoding/Cpp/Demo6/MyMath.h:13:12: note: in expansion of macro ‘MYMATHLIB_API’
   13 | extern "C" MYMATHLIB_API double myDiv(double a, double b);
      |            ^~~~~~~~~~~~~
gmake[2]: *** [CMakeFiles/MyMath_shared.dir/build.make:76: CMakeFiles/MyMath_shared.dir/MyMath.cpp.o] Error 1
gmake[1]: *** [CMakeFiles/Makefile2:83: CMakeFiles/MyMath_shared.dir/all] Error 2
gmake: *** [Makefile:91: all] Error 2

因为只有Windows才需要做导出和导入的操作,在Windows平台下的动态库是分为*.lib*.dll的。而在Linux平台下不需要做导入和导出的操作,那么我们只需要把MYMATHLIB_API定义为空,然后指明不是Windows平台下即可,这样就可以解决了不同平台的问题。

MyMath.h修改如下:

#ifndef _MYMATH_H_
#define _MYMATH_H_

#ifndef _WIN32
        #define MYMATHLIB_API

#else
        #ifdef MYMATH_EXPORT
        	#define MYMATHLIB_API __declspec(dllexport)
        #else
        	#define MYMATHLIB_API __declspec(dllimport)
        #endif
#endif

extern "C" MYMATHLIB_API double myAdd(double a, double b);
extern "C" MYMATHLIB_API double mySub(double a, double b);
extern "C" MYMATHLIB_API double myMult(double a, double b);
extern "C" MYMATHLIB_API double myDiv(double a, double b);

#endif

然后再重新构建编译得到下面的结果:

cukor_unix@WIN:~/myCoding/Cpp/Demo6$ cmake --build build
[ 50%] Building CXX object CMakeFiles/MyMath_shared.dir/MyMath.cpp.o
[100%] Linking CXX shared library libMyMath_shared.so
[100%] Built target MyMath_shared

可以看到生成了libMyMath_shared.so,这个是Linux下的动态库,命名规则就是以lib+动态库名字+.so方式命名。所有在Linux环境下,我们没有必要表明_shared.然后我们另一个项目来调用它。调用的源文件还是上小节的TestMyMath.cpp,内容没有做修改,只是移植到Linux系统上而已。那么CMakeLists.txt应该这样写:

cmake_minimum_required(VERSION 3.20)
project(TestMyMath)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
link_directories(${CMAKE_SOURCE_DIR}/lib)
add_executable(${PROJECT_NAME} ${SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib)
target_link_libraries(${PROJECT_NAME} PRIVATE MyMath_shared)

可以发现多了一行link_directories(${CMAKE_SOURCE_DIR}/lib),这个是用来给找到库目录的,其实在Windows平台下的时候也应该加上它,但我们当时没有加也可以,但最好还是加上。构建编译运行得到下面的结果。

cukor_unix@WIN:~/myCoding/Cpp/Demo7$ cmake --build build
[ 50%] Building CXX object CMakeFiles/TestMyMath.dir/TestMyMath.cpp.o
[100%] Linking CXX executable TestMyMath
[100%] Built target TestMyMath
cukor_unix@WIN:~/myCoding/Cpp/Demo7/build$ ./TestMyMath
add: 10
sub: -2
mult: 24
div: 0.666667

CMake级别上的修改

那么这个是C++源码上的修改,那么CMake又为我们做了什么事情呢?问一下new bing.

我:

我现在要编译一个可以跨平台的C++程序,怎么样写CMakeLists.txt,才能更加通用一点呢?

new bing:

编写一个跨平台的CMakeLists.txt文件通常需要避免使用特定于平台的命令和变量。您可以使用CMake提供的预定义变量和命令来检测目标平台并根据需要编写特定于平台的代码。例如,您可以使用CMAKE_SYSTEM_NAME变量来检测目标操作系统:

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    # Windows-specific code
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    # Linux-specific code
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    # macOS-specific code
endif()

这样,在不同的平台上运行CMake时,只有与目标操作系统匹配的代码部分会被执行。

可以看到这里使用了if分支,这个是几乎所有的编程语言都会有的,其实CMake也可以类似一个编程语言了,只是它的任务是用来构建编译项目的。其中STREQUAL的全称是string equal,即字符串比较。

注意一点:endif后面的()是不可以漏掉的,这个是CMake的语法规则,一定要遵守。

我:

我可以看到上面是采用字符串比较的方式判断操作系统,那么字符串中的字符有大小写的要求吗?

new bing:

是的,CMAKE_SYSTEM_NAME变量的值是区分大小写的。在上面的示例中,我们使用了STREQUAL运算符来进行字符串比较,它是区分大小写的。因此,您需要确保字符串中的字符与目标操作系统的名称完全匹配,包括大小写。

如果您想忽略大小写进行比较,可以使用string(TOUPPER ...)string(TOLOWER ...)命令将字符串转换为大写或小写,然后再进行比较。例如:

string(TOUPPER "${CMAKE_SYSTEM_NAME}" SYSTEM_NAME_UPPER)

if(SYSTEM_NAME_UPPER STREQUAL "WINDOWS")
    # Windows-specific code
elseif(SYSTEM_NAME_UPPER STREQUAL "LINUX")
    # Linux-specific code
elseif(SYSTEM_NAME_UPPER STREQUAL "DARWIN")
    # macOS-specific code
endif()

我:

简要说一下string()的语法规则

new bing:

string()是CMake中的一个命令,用于对字符串进行操作。它的语法如下:

string(<SUBCOMMAND> [options] [<output variable>] <input>...)

其中,<SUBCOMMAND>是要执行的字符串操作,例如TOUPPERTOLOWERREPLACE等。[options]是可选的选项,用于控制字符串操作的行为。[<output variable>]是可选的输出变量,用于存储操作结果。<input>...是输入字符串。

例如,下面的命令将字符串"Hello World"转换为大写并存储在变量result中:

string(TOUPPER "Hello World" result)

执行完这条命令后,变量result的值将为"HELLO WORLD"

OK,知道了这些之后,我们就把我们在Windows下的CMakeLists.txt的写法和Linux下的CMakeLists.txt的写法结合,然后分别在这两个系统下构建编译,看看是否可以通用。不要问我为什么没有macOS的。修改之后的CMakeLists.txt内容如下:

cmake_minimum_required(VERSION 3.20)
project(TestMyMath)
set(CMAKE_CXX_STANDARD 11)
file(GLOB SOURCES "*.cpp")
link_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib)
add_executable(${PROJECT_NAME} ${SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib)
string(TOUPPER "${CMAKE_SYSTEM_NAME}" SYSTEM_NAME_UPPER)
if (SYSTEM_NAME_UPPER STREQUAL "WINDOWS")
	target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib/MyMath_shared.lib)
elseif (SYSTEM_NAME_UPPER STREQUAL "LINUX")
	target_link_libraries(${PROJECT_NAME} PRIVATE MyMath_shared)
endif()

Windows下的PowerShell输出:

PS D:\myCoding\Terminal\C++\demo7> cmake -S . -B build
-- Building for: Visual Studio 17 2022
-- Selecting Windows SDK version 10.0.22000.0 to target Windows 10.0.22621.
-- The C compiler identification is MSVC 19.35.32217.1
-- The CXX compiler identification is MSVC 19.35.32217.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.35.32215/bin/Hostx64/x64/cl.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.35.32215/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: D:/myCoding/Terminal/C++/demo7/build
PS D:\myCoding\Terminal\C++\demo7> cmake --build build
MSBuild version 17.5.1+f6fdcf537 for .NET Framework

  Checking Build System
  Building Custom Rule D:/myCoding/Terminal/C++/demo7/CMakeLists.txt
  TestMyMath.cpp
  TestMyMath.vcxproj -> D:\myCoding\Terminal\C++\demo7\build\Debug\TestMyMath.exe
  Building Custom Rule D:/myCoding/Terminal/C++/demo7/CMakeLists.txt

从终端输出内容可以看到,没有任何问题。Linux终端输出结果:

cukor_unix@WIN-L1ECGP4JN8T:~/myCoding/Cpp/Demo7$ cmake -S . -B build
-- The C compiler identification is GNU 11.3.0
-- The CXX compiler identification is GNU 11.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/cukor_unix/myCoding/Cpp/Demo7/build
cukor_unix@WIN-L1ECGP4JN8T:~/myCoding/Cpp/Demo7$ cmake --build build
[ 50%] Building CXX object CMakeFiles/TestMyMath.dir/TestMyMath.cpp.o
[100%] Linking CXX executable TestMyMath
[100%] Built target TestMyMath

从终端输出内容可以看到,没有任何问题。

学习计划六:CMake安装一个项目

在学习使用CMake安装一个项目之前,我们先把我们的项目做得看起来更像一个项目,我们之前的头文件、源文件和库文件等都是任意放置的,看起来不像是一个项目或者说这个项目管理得很乱,那么我们先把项目做得稍微标准一点点,当然这个标准没有特别指明的限。

我的标准化项目

那么从这开始以后,我们的项目包括下面的几个内容:

  • src:放置自己写的C++源文件和头文件的目录
  • include:放置库的索引头文件
  • lib:放置静态库的目录
  • bin:放置可执行文件和动态库的目录
  • doc:放置项目帮助文档的目录
  • CMakeLists.txt

可以在终端这样创建这个项目,任意进入一个文件夹(项目)之后,执行下面的命令以生成项目目录。

mkdir src
mkdir include
mkdir lib
mkdir bin
mkdir doc
echo "" > CMakeLists.txt
echo "" > src/CMakeLists.txt

可以把这些命令都放在一个cmdsh脚本中。就像下面这样:

  • Powershell批处理命令,cmakepro.ps1
mkdir src
mkdir include
mkdir lib
mkdir bin
mkdir doc
echo "" > doc/Readme.md
echo "" > CMakeLists.txt
echo "" > src/CMakeLists.txt

然后把它配置到环境变量中,那么Windows就可以全局访问到它了。

接下来我们在终端使用它来创建CMake项目,当然自己手动一个个命令输入也是没有问题的,我们这里采用的就是让电脑代替我们完成这些繁杂的工作。

创建demo8文件夹,在这个文件夹下运行下面的命令,当然要配置环境变量了才可以。

PS D:\myCoding\Terminal\C++\demo8> cmakepro

    Directory: D:\myCoding\Terminal\C++\demo8

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----     2023/5/3 星期三    15:11                src
d----     2023/5/3 星期三    15:11                include
d----     2023/5/3 星期三    15:11                lib
d----     2023/5/3 星期三    15:11                bin
d----     2023/5/3 星期三    15:11                doc

结果生成了一些文件夹和一些文件,这些东西正是我们想要的CMake项目。

安装install命令

我们之前在发布静态库和动态库的时候是通过手动复制粘贴完成的,那我们接下来要介绍的CMake的install命令其实也是复制粘贴,但是有了它之后可以让我们更加轻松一点,并且在发布端写的CMake命令更知道使用端需要需要什么库,也知道应该把库放到什么位置,这样可以较少一些不必要的麻烦。

我:

我想知道CMake中的install命令的相关内容。

new bing:

在CMake中,install命令用于指定安装规则。它有多种形式,可以用来安装目标文件、普通文件和目录等。例如,你可以使用install(TARGETS ...)来安装目标文件,使用install(FILES ...)来安装普通文件,使用install(DIRECTORY ...)来安装目录。

我:

那你可以简单举一个例子吗?

new bing:

当然可以。下面是一个简单的例子,它演示了如何使用install命令来安装一个可执行文件和一个库文件:

# 假设我们有一个可执行文件my_executable和一个库文件my_library
add_executable(my_executable main.cpp)
add_library(my_library my_library.cpp)

# 安装可执行文件到系统的bin目录
install(TARGETS my_executable DESTINATION bin)

# 安装库文件到系统的lib目录
install(TARGETS my_library DESTINATION lib)

在上面的例子中,我们使用了install(TARGETS ...)命令来安装目标文件。DESTINATION选项用于指定安装位置,这里我们将可执行文件安装到系统的bin目录,将库文件安装到系统的lib目录。

我:

cmake的install命令有没有一次性直接把可执行文件、静态库和动态库装的功能?

new bing:

是的,CMake的install命令可以一次性安装多个目标文件,包括可执行文件、静态库和动态库。你只需要在install命令的TARGETS选项后面列出所有要安装的目标文件即可。例如:

# 假设我们有一个可执行文件my_executable,一个静态库my_static_library和一个动态库my_shared_library
add_executable(my_executable main.cpp)
add_library(my_static_library STATIC my_static_library.cpp)
add_library(my_shared_library SHARED my_shared_library.cpp)

# 一次性安装所有目标文件
install(TARGETS my_executable my_static_library my_shared_library
        RUNTIME DESTINATION bin
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib)

在上面的例子中,我们使用了一条install命令来安装三个目标文件:一个可执行文件、一个静态库和一个动态库。我们使用了RUNTIMEARCHIVELIBRARY选项来分别指定可执行文件、静态库和动态库的安装位置。

我:

CMake设置输出路径到项目目录下的bin目录。

new bing:

你可以使用CMake变量来设置可执行文件和库文件的输出路径。例如,你可以使用CMAKE_RUNTIME_OUTPUT_DIRECTORY变量来设置可执行文件的输出路径,使用CMAKE_ARCHIVE_OUTPUT_DIRECTORYCMAKE_LIBRARY_OUTPUT_DIRECTORY变量来设置静态库和动态库的输出路径。

下面是一个简单的例子,它演示了如何将可执行文件和库文件的输出路径设置为bin目录:

# 指定CMake最低版本
cmake_minimum_required(VERSION 3.10)

# 指定项目名称和版本
project(MyProject VERSION 1.0)

# 设置可执行文件和库文件的输出路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)

# 添加一个可执行文件
add_executable(my_executable main.cpp)

# 添加一个静态库
add_library(my_static_library STATIC my_static_library.cpp)

# 添加一个动态库
add_library(my_shared_library SHARED my_shared_library.cpp)

在上面的例子中,我们使用了set命令来设置CMAKE_RUNTIME_OUTPUT_DIRECTORYCMAKE_ARCHIVE_OUTPUT_DIRECTORYCMAKE_LIBRARY_OUTPUT_DIRECTORY变量的值。这样,可执行文件、静态库和动态库都将被输出到${CMAKE_SOURCE_DIR}/bin目录下。

我:

我使用的是MSVC编译器,我在使用CMake安装的时候应该在终端输入什么命令?

new bing:

如果你使用的是MSVC编译器,那么你可以在终端中使用cmake --build . --target INSTALL命令来执行安装。这条命令会根据你在CMakeLists.txt文件中指定的安装规则来安装文件。

下面是一个简单的例子,它演示了如何使用MSVC编译器和CMake来执行安装:

# 创建一个build目录
mkdir build

# 进入build目录
cd build

# 运行CMake
cmake .. -G "Visual Studio 16 2019"

# 编译项目
cmake --build . --config Release

# 执行安装
cmake --build . --target INSTALL --config Release

在上面的例子中,我们首先创建了一个build目录,并进入该目录。然后,我们运行了cmake .. -G "Visual Studio 16 2019"命令来生成MSVC工程文件。接着,我们使用cmake --build . --config Release命令来编译项目。最后,我们使用cmake --build . --target INSTALL --config Release命令来执行安装。

好,那么知道了这些之后,我们现在项目demo8中封装一个Student类,然后打包成动态库,并安装到当前项目目录的install文件夹下,然后把install发送给其他项目就可以直接用了。

  • Student.h
#pragma once
#include <iostream>

#if WIN32
    #ifdef STUDENT_EXPORT
        #define STUDENTLIB_API __declspec(dllexport)
    #else
        #define STUDENTLIB_API __declspec(dllimport)
    #endif
#else
    #define STUDENTLIB_API

#endif

class STUDENTLIB_API Student
{
private:
    std::string name;
    int age;
    int id;
public:
    Student(std::string name, int age, int id);
    ~Student();
    void info();
    void setAge(int age);
};
  • Student.cpp
#include "Student.h"

Student::Student(std::string name, int age, int id)
{
    this->name = name;
    this->age = age;
    this->id = id;
}

Student::~Student()
{
}

void Student::info()
{
    std::cout << "----------------------" << std::endl;
    std::cout << "name: " << name << '\n';
    std::cout << "age: " << age << '\n';
    std::cout << "id: " << id << '\n';
    std::cout << "----------------------" << std::endl;
}

void Student::setAge(int age)
{
    this->age = age;
}
  • 项目最外层的CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(Student)
set(CMAKE_CXX_STANDARD 11)
# 设置安装的路径,CMAKE_INSTALL_PREFIX 安装前缀,Windows下,默认是Program file(x86)
# CMAKE_CURRENT_LIST_DIR 当前CMakeLists.txt的目录
set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_LIST_DIR}/install)
# 把src目录下的CMakeLists.txt加载过来,实现多级项目
add_subdirectory(src)
  • src目录下的CMakeLists.txt
# 设置*.exe和*.dll文件的输出路径,CMAKE_SOURCE_DIR表示项目的根目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
# 设置*.lib的输出路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
# 设置*.lib的加载路径,如果需要使用到静态库的话
set(CMAKE_LIBRARY_ARCHITECTURE ${CMAKE_SOURCE_DIR}/lib)
file(GLOB SOURCES "*.cpp")
file(GLOB HEADERS "*.h")
add_library(
    Student_shared SHARED
    ${SOURCES}
    ${HEADERS}
)
# 在Windows平台下,这个是一定要加的
target_compile_definitions(Student_shared PRIVATE STUDENT_EXPORT)

# 安装动态库
install(
    TARGETS Student_shared
    ARCHIVE DESTINATION lib # *.lib放到install/lib中,因为我们前面设置了安装路径为install
    RUNTIME DESTINATION bin	# *.exe或*.dll放到install/bin下
    LIBRARY DESTINATION lib # *.lib(动态库)放到install/lib中
)
# 安装头文件
install(
    FILES Student.h DESTINATION include
)
# 安装Readme.md
install(
    FILES ${CMAKE_SOURCE_DIR}/doc/Readme.md DESTINATION doc
)

可以看到安装动态库等是不需要特别指定寻找库的路径的,但是安装FILES则需要特别指定,因为Student.h在当前src文件夹下,不需要其他前缀,而Readme.md就需要。

如果不明白CMAKE_SOURCE_DIR表示什么的话可以通过CMake的message命令查看,例如:

message("它在这里:${CMAKE_SOURCE_DIR}") # 把它写在CMakeLists.txt中,构建时看输出信息

当我们的准备工作做完了之后就可以去安装了。具体命令如下:

cmake -B build # -S .可加可不加
cmake --build build --config Release # 编译成Release版本
cmake --build build --target INSTALL --config Release

如果出现下面的信息就说明安装成功了。

[build]   Student_shared.vcxproj -> D:\myCoding\Terminal\C++\Test\bin\Debug\Student_shared.dll
[build]   Building Custom Rule D:/myCoding/Terminal/C++/Test/CMakeLists.txt
[build]   -- Install configuration: "Debug"
[build]   -- Installing: D:/myCoding/Terminal/C++/Test/install/lib/Student_shared.lib
[build]   -- Installing: D:/myCoding/Terminal/C++/Test/install/bin/Student_shared.dll
[build]   -- Installing: D:/myCoding/Terminal/C++/Test/install/include/Student.h
[build]   -- Installing: D:/myCoding/Terminal/C++/Test/install/doc/Readme.md

然后当前项目目录下多了一个install文件夹。

PS D:\myCoding\Terminal\C++\Test> tree install /F
卷 新加卷 的文件夹 PATH 列表
卷序列号为 DAC0-B3A4
D:\MYCODING\TERMINAL\C++\TEST\INSTALL
├─bin
│      Student_shared.dll
│
├─doc
│      Readme.md
│
├─include
│      Student.h
│      
└─lib
        Student_shared.lib

这个就是安装的部分内容。当然还有源代码级别上的安装例如发布给*.cmake。但我们就先到这里了。然后还有一些命令还没有提到,比如find_package,在Qt的CMake配置就经常见到这个命令,但是我们还是放到下一次中吧,上面提到的这些CMake命令这些是比较基础和常用的。

posted @ 2023-05-04 19:27  Cukor丘克  阅读(206)  评论(0编辑  收藏  举报