C/C++跨平台构建工具CMake-----灵活添加库并实现开发和生产环境的分离

1.概述

平时我们开发C/C++程序时都是在类似Window 的Visual Studio这类的IDE上,这些IDE集成了各种好用的工具,并提供了强大的代码调试和搜索等功能,能够很高效的帮助我们实现我们想要的功能。既然这样,那为啥我们还需要需要学习CMake的构建工具呢?我学习的主要原因是:我是一个Android开发者,作为Android开发者,熟悉jni的开发是重中之重,界面开发的时代已经过去,现在Android在VR领域和音视频邻域的地位逐渐的上升,而这些领域无不需要jni的加持,而现在的Andoid jni都是使用Cmake辅助构建的,所以学习Cmkae构建工具很有必要。

在本文中,我们将会学习如何使用Cmake工具创建一个项目,然后介绍如何在项目中添加和使用库以及如何将我们的库设置成可以动态切换的。这里可动态切换的意思是,当我们在开发环境时使用开发环境的库,在生产环境时使用生产环境的库。

注意:本文内容是Cmake官方练习的第二个练习,读者也可以直接去官方下载练习自己动手体验

2.创建项目

当我们在集成开发环境(IDE)中创建项目特别简单,只需要点击file->new project就能创建一个项目,IDE会帮我们把基本的结构准备好。但是我们今天直接自己手动创建。如下图所示: 在上面的截图中,我们创建了一个根目录AddLibraryDemo表示整个项目的主目录,在这个目录下,我们创建了build目录,用于存放编译后的产物,创建MathFunctions表示子模块目录,因为我们的项目可能会由很多的模块组成。创建CMakeLists.txt作为整个项目的构建脚本。而tutorial.cxx是本文中的演示程序,TutorialConfig.h.in是为了配置动态编译生成头文件的配置文件。

接下来我们看下在子模块MathFunctions中的目录结构及配置: 在MathFunctions的子模块中,目录结构特别简单,就是一个构建脚本CMakeLists.txt和需要演示的C++文件。接下来我们就介绍下如何使用CMake配置并运行我们创建的AddLibraryDemo项目。

3 配置运行项目

首先,我们先使用C++编写一个简单的功能,数学中的开平方根,为了演示,在本项目中会使用两种方式开平方根,一种是使用系统标准库中的函数std::sqrt(x),另外一种是使用自己实现的开平方根的方式。这个例子来源于官网的练习题。

3.1 编写开平方根示例代码

接下来我们看下实现开平方根的例子。在我们创建的根目录下的tutorial.cxx中编写开平方根的代码,代码如下所示:

cpp 复制代码
#include <cmath>
#include <iostream>
#include <string>
#include "TutorialConfig.h"

int main(int argc, char* argv[])
{
  if (argc < 2) {
    // report version
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR
     << "."<< Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

  // convert input to double
  const double inputValue = std::stod(argv[1]);
  const double outputValue = std::sqrt(inputValue);
  std::cout << "The square root of " << inputValue << " is " << 
  outputValue << std::endl;
  return 0;
}

3.2 编写CMake构建脚本

在我们的根目录下的CMakeLists.txt中编写构建命令,如下所示:

javascript 复制代码
cmake_minimum_required(VERSION 3.10)
# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)
# add the executable
add_executable(Tutorial tutorial.cxx)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

进入我们之前创建的build目录下,输入如下命令编译项目:

bash 复制代码
cmake ..
cmake --build .

编译结果:

然后输入命令:Tutorial.exe 100 运行结果如下:

在这个开平方根的例子中,我们使用了系统标准库中的std::sqrt(x),我们首先从控制台获取用户输入的值并做合法性验证,最后输出开平方根后的值。 #include "TutorialConfig.h" 这个头文件是编译动态生成的,我们在CMakeLists,txt 中会配置,也就是结合TutorialConfig.h.in文件生成的,让我们在项目中可以使用Tutorial_VERSION_MAJOR这个我们在CMakeLists.txt中配置的值

4.使用子模块实现求平方根的功能

上面的内容演示了如何实现求平方的功能,如我们所见,源代码都放在了根目录下,项目很小的时候这样做也没啥问题,但是当项目变得很大的时候,大量的源代码会让维护变得困难并且CMake构建脚本会变得很复杂,为了解决这个问题,我们可以使用子模块的方法,就是把整个项目的功能拆分成一个个的子模块,在根目录下只负责整合编译构建这些子模块,每个子模块拥有自己的编译脚本,负责本模块的编译工作。这样的话,整个项目的可维护性就变好了,接下来我们看下如何在项目中使用CMake工具添加子模块,这里需要用到一个新的CMake命令:add_subdirectory(子模块目录)

4.1 在子模块中实现两种求平方根的方法

在我们新建的子模块MathFunctions中,我们实现两种求平方根的方法,一种是使用标准库中的std::sqrt(x)函数,另外一种是自己编写算法实现,如下所示: (1) 使用标准库中的sqrt函数

MathFunctions.h

cpp 复制代码
#pragma once
namespace mathfunctions {
double sqrt(double x);
}

MathFunctions.cpp

cpp 复制代码
#include "MathFunctions.h"
#include <cmath>
namespace mathfunctions {
double sqrt(double x)
{
  return std::sqrt(x);
}
}

(2)使用自己编写的函数mysqrt

mysqrt.h

cpp 复制代码
#pragma once
namespace mathfunctions {
namespace detail {
double mysqrt(double x);
}
}

mysqrt.cpp

cpp 复制代码
#include "mysqrt.h"
#include <iostream>
namespace mathfunctions {
namespace detail {
double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  double result = x;

  // do ten iterations
  for (int i = 0; i < 10; ++i) {
    if (result <= 0) {
      result = 0.1;
    }
    double delta = x - (result * result);
    result = result + 0.5 * delta / result;
    std::cout << "Computing sqrt of " << x << " to be " << result << 
    std::endl;
  }
  return result;
}
}
}

将上面的源代码放到子模块Mathfunctions下,如下所示: 说明:c++的文件后缀.cxx 和.cpp都是表示C++文件。

4.2 构建Mathfunctions子模块

在MathFunctions的CMakeLIsts.txt中编写构建命令,将MathFunctions子模块构建成一个库,供其他模块使用。只需要使用add_library()命令就可以将当前目录下的C++文件构建成一个库,如下所示:

javascript 复制代码
add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)

上面的代码中将MathFunctions.cxx mysqrt.cxx两个C++文件的源代码编译构建成一个名为MathFunctions的静态库,如下图所示: 注意:使用静态库时,我们可以不用写名字前面的lib以及后缀.a, 例如本例中的libMathFunctions.a我们使用的时候直接用MathFunctions就可以引用到这个库了

后面可以通过这个名字引用这个库,进而可以使用这个库中的功能。

4.3 在根目录引用子模块的功能

当我们把子模块编译构建好了后,就会生成一个名为MathFunctions的库,接下来我们就在根目录下引用这个库实现求平方根的功能。

4.3.1 编写构建脚本

首先是在构建脚本CMakeLists.txt中引入这个库,如下所示:

javascript 复制代码
cmake_minimum_required(VERSION 3.10)
# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

# Use add_subdirectory() to add MathFunctions to this project
add_subdirectory(MathFunctions)
# add the executable
add_executable(Tutorial tutorial.cxx)
# Use target_link_libraries to link the library to our executable
target_link_libraries(Tutorial PUBLIC MathFunctions)
# Add MathFunctions to Tutorial's target_include_directories()
# Hint: ${PROJECT_SOURCE_DIR} is a path to the project source.
 AKA This folder!
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           "${PROJECT_SOURCE_DIR}/MathFunctions"
                           )

如上面的代码所示,我们使用add_subdirectory(MathFunctions)将子模块MathFunctions添加到我们的根目录下,在其他模块中想要引用MathFunctions的功能也是这么做,接下来编译整个项目的时候,会连子模块MathFuncitons一起编译,这时会生成静态库libMathFunctions.a文件,我们使用target_link_libraries(Tutorial PUBLIC MathFunctions) 命令来链接我们的静态库,然后再将子模块中的路径配置到根目录的CMakeLists.txt中,方便找到对应的文件,使用的命令是:

javascript 复制代码
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           "${PROJECT_SOURCE_DIR}/MathFunctions"
                           )

4.3.2 编写C++代码使用MathFunctions库中提供的功能

在根目录的tutorial.cxx文件中修改求平方根的代码,使用MathFunctions库中提供的功能。如下所示:

cpp 复制代码
#include <cmath>
#include <iostream>
#include <string>

#include "TutorialConfig.h"
#include "MathFunctions.h"

int main(int argc, char* argv[])
{
  if (argc < 2) {
    // report version
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
              << Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

  // convert input to double
  const double inputValue = std::stod(argv[1]);
  const double outputValue = mathfunctions::sqrt(inputValue);
  std::cout << "The square root of " << inputValue << " is " 
  << outputValue
            << std::endl;
  return 0;
}

上面的代码中我们使用了#include "MathFunctions.h" 引入了在子模块中定义的头文件,然后使用const double outputValue = mathfunctions::sqrt(inputValue); 求取平方根的值

4.4 运行

在本节的开头提到使用两种方法求平方根,一种是标准库中的,一种是我们自己实现的,如果我们要在这两种方法中切换,有两种办法,一种是修改调用方的代码,即修改Turtorial.cxx中的代码,这种做法通常不可取,因为如果我们提供功能需要调用方去做很多修改的话,还不如不提供。所以这种方法不可取,另外一种方法是在库里面修改,给调用方提供一个接口。 在本项目中,我们需要修改的地方就是子模块的MathFunctions.cxx文件:

cpp 复制代码
#include "MathFunctions.h"
#include <cmath>
#include "mysqrt.h"

namespace mathfunctions {
double sqrt(double x)
{
  // return detail::mysqrt(x); // 使用自定义的函数
  return std::sqrt(x); // 使用标准库中的函数
}
}

我们可以修改上面的代码,构建项目。运行结果如下: (1)使用标准库中的函数: (2)使用自定义的函数

5 添加可调试的功能

在上面的内容中介绍了添加子模块的库并调用库中的功能,本节就是本文的主要内容,实现开发环境和生产环境的分离。如此一来,开发者就可以自己选择是使用系统提供的求平方根函数还是使用自己实现的函数。这种方式特别适合用于调试程序。实现这一个功能可以使用CMake提供的option()命令,使用这个命令,开发者可以定义一个变量去表示开发或者是生产环境,或者是使用某个库的标志,设置了后我们就可以通过改变我们定义的变量的值去动态切换我们的函数实现了。

5.1 编写子模块的CMakeLists.txt

要实现可动态切换我们的函数实现的功能,需要我们在构建脚本中编写对应的代码,步骤如下: 首先,使用option()命令定义一个变量USE_MYMATH,并设置其值为ON,表示使用我们自定义的函数,为OFF则使用系统提供的函数

cpp 复制代码
// MathFunctions/CMakeLists.txt
option(USE_MYMATH "use my math implementation" ON)

然后我们使用if()语句来判断USE_MYMATH 的值,并且使用target_compile_definitions()命令来开启或者关闭我们的源代码编译选项。如下所示:

cpp 复制代码
// MathFunctions/CMakeLists.txt
if(USE_MYMATH)
    target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

这时候的MathFunctions/CMakeLists.txt就如下所示:

cpp 复制代码
// MathFunctions/CMakeLists.txt
option(USE_MYMATH "use my math implementation" ON)
add_library(MathFunctions MathFunctions.cxx mysqrt.cxx)

if(USE_MYMATH)
    target_compile_definitions(
    MathFunctions 
    PRIVATE 
    "USE_MYMATH"
    )
    
endif()

上面的代码中我们会发现如果USE_MYMATH的值变成了OFF,即不需要使用我们自定义的函数时,本不应该再让mysqrt.cxx文件参与编译,因为我们根本没使用它的功能,这里可以将我们自己实现的功能单独编译成一个静态库,并且在if()语句块里面链接这个库,代码如下所示:

cpp 复制代码
// MathFunctions/CMakeLists.txt
option(USE_MYMATH "use my math implementation" ON)
add_library(MathFunctions MathFunctions.cxx)

add_library(SqrtLibrary STATIC mysqrt.cxx)

if(USE_MYMATH)
    target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
    target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

至此,我们的CMake构建脚本就编写好了

5.2 在C++代码中使用USE_MYMATH变量

要想实现动态切换我们的两种开平方根功能的实现,仅仅编写构建脚本还不够,构建脚本只能告诉编译器该构建哪些文件,具体需要实现的功能还是得修改具体的实现代码,在C++代码中,我们可以直接使用我们定义的USE_MYMATH变量来觉得使用的实现函数。如下所示:

cpp 复制代码
#include "MathFunctions.h"
#include <cmath>
#ifdef USE_MYMATH
 #include "mysqrt.h"
#endif

namespace mathfunctions {
double sqrt(double x)
{
  #ifdef USE_MYMATH
    return detail::mysqrt(x); // 使用自定义的函数
  #else
    return std::sqrt(x); // 使用标准库中的函数
  #endif
}
}

如上面的代码所示,如果我们定义了USE_MYMATH这个变量,那么就使用自定义的求平方根函数,否则使用标准库的求平方根函数

5.3 运行代码

当代码都编写完成后,我们可以进入build目录,然后使用命令:

cpp 复制代码
cmake ..

编译项目,编译完成后使用:

cpp 复制代码
cmake --build .

构建项目,构建玩成后使用下面命令运行项目:

cpp 复制代码
Tutorial.exe 100

这时,因为默认的实现时使用我们自定义的求平方根方法,假如我们想切换求平方根的方法为系统提供的,则运行下面的命令:

cpp 复制代码
cmake .. -DUSE_MYMATH=OFF

然后再运行:

cpp 复制代码
cmake --build .

构建项目,接着运行:

cpp 复制代码
Tutorial.exe 100

查看运行结果,所有的运行结果如下所示: 至此,本文结束。

相关推荐
怀澈12213 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
chnming198737 分钟前
STL关联式容器之set
开发语言·c++
威桑1 小时前
MinGW 与 MSVC 的区别与联系及相关特性分析
c++·mingw·msvc
熬夜学编程的小王1 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins1 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法
Mr.131 小时前
什么是 C++ 中的初始化列表?它的作用是什么?初始化列表和在构造函数体内赋值有什么区别?
开发语言·c++
阿史大杯茶1 小时前
AtCoder Beginner Contest 381(ABCDEF 题)视频讲解
数据结构·c++·算法
C++忠实粉丝1 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
我们的五年2 小时前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
程序猿阿伟2 小时前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链