当前位置: 首页 > news >正文

【CMake】《CMake构建实战:项目开发卷》笔记-Chapter11-实践:基于onnxruntime的手写数字识别库

第11章 实践:基于onnxruntime的手写数字识别库

读者已经跟着本书实践了很多零零散散的实例,应该能够熟练使用CMake来构建C和C++程序了吧!不过,前面的实例往往都是针对某个特定功能编写的,我们可能很难将它们综合起来实现一个完成度较高的项目。不必担心,本章就带领大家使用C++语言实现一个完整的动态库,以及调用该库的可执行文件——手写数字识别库和手写数字识别命令行工具。相信经过本章的实践,读者一定可以将前面所学的知识融会贯通,应用于中大型项目中了!

11.1 前期设计

11.1.1 模块设计

本章不仅要实现一个手写数字识别库,还会同时编写一个recognize命令行工具,用户可以在命令行中调用该工具以识别图片中的手写数字。因此,项目中需要定义两个构建目标,分别是动态库目标num_recognizer和可执行文件目标recognize。其中,可执行文件目标recognize将链接动态库目标num_recognizer

另外,我们希望构建的手写数字识别库是一个通用库,使其能够被C++语言之外的其他编程语言调用,如C语言等。这就要求手写数字识别动态库在暴露API时,必须仅暴露符合C语言应用程序二进制接口(Application Binary Interface,ABI)的应用程序编程接口(Application Programming Interface,API)。简言之,就是暴露的接口都只能是纯C函数,函数的参数、返回值等都必须是C语言中支持的数据类型。

C++编程语言中若想将一个函数定义为C语言函数,将一个结构体定义为C语言结构体,需要在函数定义和结构体定义前指定extern "C"修饰符,或将其置于extern "C"代码块中。

11.1.2 项目目录结构

设计好需要的模块后,就可以开始着手建立项目的目录结构了。本项目目录结构如下:

ch011
├── CMakeLists.txt (目录程序)
├── cli (命令行工具的源文件目录)
│   └── recognize.c (命令行工具的源文件)
├── cmake (自定义CMake模块目录)
│   └── ...
├── include (头文件目录)
│   └── num_recognizer.h (手写数字识别库的头文件)
├── models (onnx模型文件目录)
│   └── mnist.onnx (手写数字识别模型文件)
└── src (手写数字识别库的源文件目录)└── num_recognizer.cpp (手写数字识别库的源文件)

命令行工具的源文件目录名称为cli,这是命令行接口的英文commandline interface的缩写。另外,这里用到的mnist.onnx神经网络模型文件可以在GitHub的onnx/models代码仓库中下载。

11.1.3 接口设计

在实现之前,还需要对手写数字识别库具体提供什么功能作出定义,并将接口设计出来。

作为一个实践案例,该库不会涉及过于复杂的技术。尽管如此,笔者也希望这个手写数字识别库仍然是实用的:既支持用户传入二值化的图片像素数组,也支持用户传入一个PNG图片文件的路径来进行识别。可以说是麻雀虽小,五脏俱全!

初始化

使用onnxruntime库,需要先初始化一个onnx环境(Ort::Env)供onnx会话(Ort::Session)使用。因此,手写数字识别库应当首先提供一个初始化的接口,如下所示。

//! @brief 初始化手写数字识别库
//! @return void
NUM_RECOGNIZER_EXPORT void num_recognizer_init();

接口函数最前面的NUM_RECOGNIZER_EXPORT是导出宏,后面会在CMake目录程序中使用GenerateExportHeader这一CMake模块定义它们。该CMake模块的具体用法参见9.2.3小节。

创建和析构识别器

手写数字识别模型文件也许会更新迭代,因此接口应当能够灵活地根据用户指定的模型文件来创建识别器对象,同时提供用于析构识别器对象的接口,如下所示。

//! @brief 创建识别器
//! @param model_path 模型文件路径
//! @param[out] out_recognizer 接受初始化的识别器指针的指针
NUM_RECOGNIZER_EXPORT void num_recognizer_create(const char *model_path,Recognizer **out_recognizer);//! @brief 析构识别器
//! @param recognizer 识别器的指针
NUM_RECOGNIZER_EXPORT void num_recognizer_delete(Recognizer *recognizer);

注意,num_recognizer_create接口的第二个参数类型是Recognizer**,即Recognizer结构体指针的指针。调用该接口后,程序会将创建好的识别器对象的指针赋值到该参数指向的变量中。

目前Recognizer类尚未定义,可以先在头文件中写一个前向声明,如下所示。

struct Recognizer;
识别二值化图片像素数组

手写数字识别库可以接受一个代表各像素颜色的数组作为被识别的图片对象。该数组是一个按行存储的28×28的float数组,即第一维索引对应列号,第二维索引对应行号。其中,元素的值若为0,则代表白色,为1则代表黑色,因此它实际上表示了一个二值化后的图片。该接口如下所示。

//! @brief 识别图片数据中的手写数字
//! @param recognizer 识别器的指针
//! @param input_image
//! 模型接受的输入图片数据(28×28的float数值数组,0代表白色,1代表黑色)
//! @param result 接受识别结果的数值的指针
//! @return 错误值,成功返回0
NUM_RECOGNIZER_EXPORT int num_recognizer_recognize(Recognizer *recognizer,float *input_image,int *result);
识别PNG图片

当然,只提供接受数组参数的接口并不便于用户调用。这里还提供了一个可以直接识别指定路径的PNG图片中手写数字的接口,如下所示。

//! @brief 识别PNG图片中的手写数字
//! @param recognizer 识别器的指针
//! @param png_path PNG图片文件路径
//! @param result 接受识别结果的数值的指针
//! @return 错误值,成功返回0
NUM_RECOGNIZER_EXPORT int num_recognizer_recognize_png(Recognizer *recognizer,const char *png_path,int *result);

至此,手写数字识别库的全部接口声明完毕。

接口功能实现思路

接口设计好后,不妨总结一下如果要实现这些接口的功能,需要有哪些具体的行为,借助哪些工具。

  • 对二值化图片数组进行手写数字识别可以借助onnxruntime库来完成。

  • 读取PNG图片像素可以借助libpng库来完成。

  • 将PNG图片像素数据转换为28×28的二值化图片数组,即图片缩放及二值化算法。该功能由我们自行实现。

看起来我们能够站在巨人的肩膀上来完成这件事,应该能简单不少!

11.2 第三方库

正式编写程序之前,首先需要安装刚刚提到的第三方库。onnxruntime库的安装已经在9.4.9节讲过,因此本节重点关注其他第三方库的安装:libpng库及libpng依赖的zlib库。那么,首先一起来安装zlib库吧!

Linux操作系统通常预装了zlib库,读者可以先尝试跳过zlib库的安装,看能否直接成功构建并安装libpng库。

11.2.1 安装zlib库

zlib库的源程序可以从它的GitHub代码仓库中获取。将代码克隆或下载到本地后,按照以下步骤构建并安装:

> cd zlib
> mkdir build
> cd build
> cmake -DCMAKE_BUILD_TYPE=Release ..
> cmake --build . --config Release
> cmake --install . # 需要管理员权限

在执行cmake --install命令安装CMake项目时,CMake会默认将其安装到系统目录中:在Windows中,默认安装目录前缀一般是C:\Program Files (x86)\zlib;在Linux中,默认安装目录前缀一般是/usr/local。如果想使用默认的安装目录,执行该命令时需要提供管理员权限。

当然,为cmake --install命令指定--prefix <安装目录>的参数,也可以自定义安装目录。不过采用这种方式,使用find_package命令查找第三方库时,通常需要手动指定用于提示安装目录的参数或变量。

11.2.2 安装libpng库

libpng库的源程序同样可以从其GitHub代码仓库中获取(本例采用v1.6.40版本)。其构建和安装步骤与构建和安装zlib库的步骤几乎完全相同:

> cd libpng
> mkdir build
> cd build
> cmake -DCMAKE_BUILD_TYPE=Release ..
> cmake --build . --config Release
> cmake --install . # 需要管理员权限

11.2.3 libpng的查找模块

CMake预置了zlib库的查找模块,不必自行实现;onnxruntime库的查找模块在9.4.9小节中已经实现过,因此本小节也不再重复,这里仅介绍如何实现libpng的查找模块。

libpng库自带了用于配置模式下的find_package命令的配置文件,但其中缺失关于头文件目录等属性的设置,因此需要对其进行二次包装,编写一个自定义查找模块。模块程序的核心部分如下所示。

# 调用libpng库自带的配置文件来查找软件包,其自带配置文件会创建两个导入库目标:
# 1. 动态库导入目标png_shared
# 2. 静态库导入目标png_static
find_package(libpng CONFIG CONFIGS libpng16.cmake)# 若成功查找,为两个库目标补上缺失的头文件目录属性
if(libpng_FOUND)# 获取png动态库导入目标对应动态库文件的路径,首先尝试其IMPORTED_LOCATION属性get_target_property(libpng_LIBRARY png_shared IMPORTED_LOCATION)# 若未能获得动态库文件路径,再尝试其IMPORTED_LOCATION_RELEASE属性if(NOT libpng_LIBRARY)get_target_property(libpng_LIBRARY png_shared IMPORTED_LOCATION_RELEASE)endif()# 根据png动态库的路径,设置libpng的根目录set(_png_root "${libpng_LIBRARY}/../..")# 查找png.h头文件所在目录的路径find_path(libpng_INCLUDE_DIR png.hHINTS ${_png_root}PATH_SUFFIXES include)# 为png_shared和png_static导入库目标设置头文件目录属性target_include_directories(png_shared INTERFACE ${libpng_INCLUDE_DIR})target_include_directories(png_static INTERFACE ${libpng_INCLUDE_DIR})
endif()include(FindPackageHandleStandardArgs)# 检查变量是否有效以及配置文件是否成功执行
find_package_handle_standard_args(libpng REQUIRED_VARS libpng_LIBRARY libpng_INCLUDE_DIRCONFIG_MODE)# 若一切成功,设置结果变量
if(libpng_FOUND)set(libpng_INCLUDE_DIRS ${libpng_INCLUDE_DIR})set(libpng_LIBRARIES ${libpng_LIBRARY})
endif()

本书暂未涉及find_package命令配置模式的内容,因此没有对该查找模块的原理做更多解释,感兴趣的读者可以试着结合程序注释和官方文档自行理解。

11.3 CMake目录程序

终于完成了准备工作,可以开始手写数字识别库的部分了。首先编写好CMake目录程序,在项目根目录中创建CMakeLists.txt,并把按照惯例要写的代码先写上去,如下所示。

cmake_minimum_required(VERSION 3.20)
project(num_recognizer)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
set(CMAKE_CXX_STANDARD 11) # 设置C++标准为11

11.3.1 查找软件包

接着,查找即将用到的两个软件包,如下所示。

set(onnx_version 1.10.0) # 根据下载的版本进行设置,本例使用1.10.0版本
# 请下载onnxruntime库的压缩包,并解压至该目录中
if("$ENV{onnxruntime_ROOT}" STREQUAL "")if(WIN32)set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-win-x64-${onnx_version}")elseif(APPLE)set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-osx-universal2-${onnx_version}")else()set(ENV{onnxruntime_ROOT} "${CMAKE_CURRENT_LIST_DIR}/onnxruntime-linux-x64-${onnx_version}")endif()
endif()find_package(onnxruntime 1.10 REQUIRED) # 指定依赖的最小版本
find_package(libpng REQUIRED)

其中的if条件只是为了设置onnxruntime_ROOT环境变量的值为onnxruntime软件包的安装目录,用于提示查找模块查找的路径。这里无须为libpng的查找模块提示查找的路径,因为我们将libpng安装到了默认的安装路径,而且libpng的查找模块能够在默认安装路径中找到它。

11.3.2 num_recognizer动态库目标

下面创建本实例的第一个构建目标:num_recognizer动态库目标。对应的CMake目录程序片段如下所示。

add_library(num_recognizer SHARED src/num_recognizer.cpp)include(GenerateExportHeader)
generate_export_header(num_recognizer)
set_target_properties(num_recognizer PROPERTIESCXX_VISIBILITY_PRESET hiddenVISIBILITY_INLINES_HIDDEN 1
)target_include_directories(num_recognizer PUBLIC include ${CMAKE_BINARY_DIR})
target_link_libraries(num_recognizer PRIVATE onnxruntime::onnxruntime png_shared)
target_compile_definitions(num_recognizer PRIVATE ORT_NO_EXCEPTIONS num_recognizer_EXPORTS)

这里除了调用add_library命令创建动态库构建目标,还引用了GenerateExportHeader模块,并调用了它提供的generate_export_header命令来为动态库生成导出头文件。同时,设置了该动态库目标的两个属性,用于默认隐藏符号并仅导出显式指定的符号。

为了让该库能够直接引用刚刚生成的导出头文件,这里要将当前二进制目录${CMAKE_ BINARY_DIR}加入库目标的头文件搜索目录。另外,为该库定义num_recognizer_EXPORTS宏以表示当前正在构建该库而非使用该库,确保导出头文件中的宏定义正确。

这里还将include目录加入该库的头文件搜索目录,并将onnxruntime和libpng第三方库目标链接到该库目标中。由于我们封装的是符合C语言ABI的动态库,不希望程序中有异常抛出,这里还定义了ORT_NO_EXCEPTIONS宏以禁用onnxruntime库中的异常。

11.3.3 recognize可执行文件目标

配置好动态库目标后,创建recognize命令行工具的可执行文件目标。这非常简单,只需创建目标、指定源文件并链接刚刚创建好的动态库目标。CMake目录程序片段如下所示。

add_executable(recognize cli/recognize.c)
target_link_libraries(recognize PRIVATE num_recognizer)

11.4 代码实现

拖了这么久,下面就要施展真正的“魔法”了。不过这也体现了一个项目的成功,只靠代码写得漂亮还远远不够,还要依赖井井有条的项目结构和完善的基础设施。

11.4.1 全局常量和全局变量

首先,在手写数字识别库的源文件中定义一些全局的常量和变量,如下所示。

static const char *INPUT_NAMES[] = {"Input3"}; // 模型输入参数名
static const char *OUTPUT_NAMES[] = {"Plus214_Output_0"}; // 模型输出参数名
static constexpr int64_t INPUT_WIDTH = 28;  // 模型输入图片宽度
static constexpr int64_t INPUT_HEIGHT = 28; // 模型输入图片高度
static const std::array<int64_t, 4> input_shape{1, 1, INPUT_WIDTH, INPUT_HEIGHT}; // 输入数据的形状(各维度大小)
static const std::array<int64_t, 2> output_shape{1, 10}; // 输出数据的形状(各维度大小)static Ort::Env env{nullptr};                // onnxruntime环境
static Ort::MemoryInfo memory_info{nullptr}; // onnxruntime内存信息

其中的常量都是由手写数字识别库的onnx模型的神经网络结构决定的,如果要切换到具有不同神经网络结构的模型,可能需要做出相应修改。

其中env变量即onnxruntime的环境,由于它不能在静态初始化时构造,这里暂且将它定义为未初始化的状态(即使用nullptr初始化)。它会在暴露给用户的num_recognizer_init接口函数中初始化。表示onnxruntime内存信息的memory_info变量也是同理。

11.4.2 手写数字识别类

接下来编写一个手写数字识别类Recognizer,用于封装onnxruntime会话,CMake目录程序片段如下所示。

//! @brief 手写数字识别类
struct Recognizer {//! @brief onnxruntime会话Ort::Session session;
};

每一个手写数字识别类都对应一个onnxruntime会话,每一个会话都可以加载一个onnx模型。

11.4.3 初始化接口实现

初始化接口是实现的第一个接口函数,在实现之前,先来编写一个extern "C"代码块,用于将其中的函数定义为C语言函数,如下所示。

extern "C" {
void num_recognizer_init() {env = Ort::Env{static_cast<const OrtThreadingOptions *>(nullptr)};memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
}

初始化接口的实现就是初始化两个与onnxruntime相关的全局变量:env和memory_info。

11.4.4 构造识别器接口实现

构造识别器接口的实现如下所示。

void num_recognizer_create(const char *model_path,Recognizer **out_recognizer) {Ort::Session session{nullptr};
#if _WIN32// Windows中,onnxruntime的Session接受模型文件路径时需使用const// wchar_t*,即宽字符串。因此在这里做一下转换。wchar_t wpath[256];MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, model_path, -1, wpath, 256);session = Ort::Session(env, wpath, Ort::SessionOptions(nullptr));
#elsesession = Ort::Session(env, model_path, Ort::SessionOptions(nullptr));
#endif*out_recognizer = new Recognizer{std::move(session)};
}

这里的主要逻辑就是通过模型文件路径来构造onnxruntime会话,并将其赋值给在堆上创建的手写数字识别类,将这个类的指针作为结果传给用户。

这里有一处麻烦需要处理:Windows中onnxruntime会话构造时接受的模型文件路径的编码不同,需要对传入的参数进行编码转换。为了使用Windows的编码转换API,源文件最开始也做了条件编译以引用Windows.h,如下所示。

#ifdef _WIN32
// 在Windows操作系统中,我们需要使用Windows API来帮助完成const char*到const
// wchar_t*的编码转换。因此需要引用Windows.h。
#include <Windows.h>
#endif

11.4.5 析构识别器接口实现

析构很简单,直接delete即可,如下所示。

void num_recognizer_delete(Recognizer *recognizer) { delete recognizer; }

11.4.6 识别二值化图片像素数组接口实现

我们使用的神经网络模型mnist.onnx本身就是接受一个28×28的float型数组作为输入,然后分别输出结果为0到10的可能性权重,因此只需在实现识别二值化图片像素数组的接口时通过onnxruntime库运行该模型的推理过程,最后取可能性最大的数值作为预测结果。CMake目录程序片段如下所示。

int num_recognizer_recognize(Recognizer *recognizer, float *input_image,int *result) {std::array<float, 10> results{};auto input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_image, INPUT_WIDTH * INPUT_HEIGHT,input_shape.data(), input_shape.size());auto output_tensor = Ort::Value::CreateTensor<float>(memory_info, results.data(), results.size(), output_shape.data(),output_shape.size());recognizer->session.Run(Ort::RunOptions{nullptr}, INPUT_NAMES,&input_tensor, 1, OUTPUT_NAMES, &output_tensor, 1);*result = static_cast<int>(std::distance(results.begin(), std::max_element(results.begin(), results.end())));return 0;
}

11.4.7 识别PNG图片接口实现

识别PNG图片的重点,就是如何将PNG图片读取并转换为28×28的二值化的图片像素数组。首先,借助libpng第三方库来完成PNG图片的读取,如下所示。

int num_recognizer_recognize_png(Recognizer *recognizer, const char *png_path,int *result) {int ret = 0;std::array<float, INPUT_WIDTH * INPUT_HEIGHT> input_image;FILE *fp;unsigned char header[8];png_structp png_ptr;png_infop info_ptr;png_uint_32 png_width, png_height;png_byte color_type;png_bytep *png_data;// 打开PNG图片文件fp = fopen(png_path, "rb");if (!fp) {ret = -2;goto exit3;}// 读取PNG图片文件头fread(header, 1, 8, fp);// 验证文件头确实是PNG格式的文件头if (png_sig_cmp(reinterpret_cast<unsigned char *>(header), 0, 8)) {ret = -3;goto exit2;}// 创建PNG指针数据结构png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,nullptr);if (!png_ptr) {ret = -4;goto exit2;}// 创建PNG信息指针数据结构info_ptr = png_create_info_struct(png_ptr);if (!info_ptr) {ret = -5;goto exit2;}// 设置跳转以处理异常if (setjmp(png_jmpbuf(png_ptr))) {ret = -6;goto exit2;}// 初始化PNG文件png_init_io(png_ptr, fp);png_set_sig_bytes(png_ptr, 8);// 读取PNG信息png_read_info(png_ptr, info_ptr);png_width = png_get_image_width(png_ptr, info_ptr);   // PNG图片宽度png_height = png_get_image_height(png_ptr, info_ptr); // PNG图片高度color_type = png_get_color_type(png_ptr, info_ptr); // PNG图片颜色类型// 设置跳转以处理异常if (setjmp(png_jmpbuf(png_ptr))) {ret = -7;goto exit2;}// 读取PNG的数据png_data = (png_bytep *)malloc(sizeof(png_bytep) * png_height);for (unsigned int y = 0; y < png_height; ++y) {png_data[y] = (png_byte *)malloc(png_get_rowbytes(png_ptr, info_ptr));}png_read_image(png_ptr, png_data);// 未完待续

代码执行到这就已经成功将PNG图片中每个像素的颜色值读取到了png_data这个png_bytep *类型,也就是字节指针的指针类型的变量中,它用于表示一个字节类型的二维数组。

至于这些字节是如何表示图片各个像素的颜色值的,需要根据PNG图片采用的颜色类型灵活判断:若图片采用RGB颜色类型,那么文件中每三个字节表示一个颜色值,这三个字节分别对应颜色的RGB值;若图片采用RGBA颜色类型,那么它就需要四个字节表示一个颜色值。为了方便地获取PNG图片数据中指定像素的颜色值,并将其二值化,不妨在源文件中创建一些帮助函数,如下所示。

//! @brief 将byte类型的颜色值转换为模型接受的二值化后的float类型数值
//! @param b byte类型的颜色值
//! @return 模型接受的二值化后的float类型值,0代表白色,1代表黑色。
static float byte2float(png_byte b) { return b < 128 ? 1 : 0; }//! @brief 获取PNG图片指定像素的二值化后的float类型颜色值
//! @param x 像素横坐标
//! @param y 像素纵坐标
//! @param png_width 图片宽度
//! @param png_height 图片高度
//! @param color_type 图片颜色类型
//! @param png_data 图片数据
//! @return 对应像素的二值化后的float类型颜色值
static float get_float_color_in_png(unsigned int x, unsigned int y,png_uint_32 png_width,png_uint_32 png_height, png_byte color_type,png_bytepp png_data) {if (x >= png_width || x < 0)return 0;if (y >= png_height || y < 0)return 0;switch (color_type) {case PNG_COLOR_TYPE_RGB: {auto p = png_data[y] + x * 3;return byte2float((p[0] + p[1] + p[2]) / 3);} break;case PNG_COLOR_TYPE_RGBA: {auto p = png_data[y] + x * 4; } break;default: return 0; } 
}; 

get_float_color_in_png函数仅支持了较为常见的两种PNG图片颜色类型:RGB和RGBA。感兴趣的读者可以自行扩充其支持的颜色类型。有了该函数的帮助,再回到刚才num_recognizer_recognize_png函数中尚未完成的接口实现,其后续CMake目录程序片段如下所示。

    // 将PNG图片重新采样,缩放到模型接受的输入图片大小for (unsigned int y = 0; y < INPUT_HEIGHT; ++y) {for (unsigned int x = 0; x < INPUT_WIDTH; ++x) {float res = 0;int n = 0;for (unsigned int png_y = y * png_height / INPUT_HEIGHT;png_y < (y + 1) * png_height / INPUT_HEIGHT; ++png_y) {for (unsigned int png_x = x * png_width / INPUT_WIDTH;png_x < (x + 1) * png_width / INPUT_WIDTH; ++png_x) {res += get_float_color_in_png(png_x, png_y, png_width,png_height, color_type,png_data);++n;}}input_image[y * INPUT_HEIGHT + x] = res / n;}}// 识别图片数据中的手写数字ret = num_recognizer_recognize(recognizer, input_image.data(), result);

这里完成了对PNG图片的重采样,即缩放图片到28×28这个尺寸,并将最终满足输入要求的数据存入input_image数组。到此,如果未发生错误,程序将通过复用num_recognizer_recognize接口来完成最终的识别。

除了正常的代码路径,代码中还有一些异常处理的分支,用于分别跳转到不同的标签。这些标签对应不同的退出路径,它们的代码就在函数的末尾做一些资源清理的工作,如下所示。

exit1:// 释放存放PNG图片数据的内存空间for (unsigned int y = 0; y < png_height; ++y) {free(png_data[y]);}free(png_data);exit2:// 关闭文件fclose(fp);exit3:return ret;
}

不同的退出路径需要对应不同程度的清理工作。而如果程序正常退出,那么全部退出路径都会执行到,也就会对全部使用过的资源进行清理释放。

至此,全部接口实现完毕,最后不要忘记用于结束extern "C"代码块的花括号。

11.4.8 完善手写数字识别库的头文件(以同时支持C语言)

在进行接口设计时,实际上就是在编写头文件的核心部分——接口函数。不过,只是声明这些函数,并不足以构成一个完善的公开头文件。另外,手写数字库暴露的都是C语言接口,这个头文件应当能够同时被C++语言和C语言引用。下面一起来看看应当如何完善这个头文件。

首先,头文件引用卫哨必不可少,如下所示。

#ifndef NUM_RECOGNIZER_H
#define NUM_RECOGNIZER_H

其次,要引用导出头文件,这样才能使用num_recognizer_EXPORTS等宏定义,以便为动态库导出符号,如下所示。

#include "num_recognizer_export.h"

再次是声明接口相关的类和函数。由于接口函数都是C语言的函数,当采用C++编译器时需要将接口涉及的结构体和函数声明用extern "C"包括起来。这里借助__cplusplus宏来判断是否采用C++编译器,如下所示。

#ifdef __cplusplus
extern "C" {
#endif

下面开始声明涉及的类或结构体,如下所示。

#ifdef num_recognizer_EXPORTS
struct Recognizer;
#else
typedef struct _Recognizer Recognizer;
#endif

这里涉及两种情况:该头文件被实现该库的源文件(即num_recognizer.cpp)引用,以及该头文件被用户的外部程序(包括即将编写的recognize命令行工具的源文件)引用。num_recognizer_EXPORTS宏就可以用于判断当前是在构建还是使用该库。还记得吗?它是在### 11.3.2 num_recognizer动态库目标所示的目录程序中定义的。

当该宏被定义,也就意味着当前正在构建该库。此时会前向声明Recognizer结构体,以避免声明接口函数时编译器不认识这个结构体。这个结构体的具体定义会在源文件中给出。

当该宏未被定义,也就是说该库被用户使用时,这里不能仅包含一个前向声明,否则编译器会报告找不到定义的错误。我们需要将Recognizer结构体定义为一个不透明结构体(opaque structure),即没有具体定义的结构体,这种结构体仅能出现在指针类型中,正符合接口中Recognizer类的使用场景。

接下来是在头文件中声明最开始设计的接口函数,如下所示。

//! @brief 初始化手写数字识别库
//! @return void
NUM_RECOGNIZER_EXPORT void num_recognizer_init();//! @brief 创建识别器
//! @param model_path 模型文件路径
//! @param[out] out_recognizer 接受初始化的识别器指针的指针
NUM_RECOGNIZER_EXPORT void num_recognizer_create(const char *model_path,Recognizer **out_recognizer);//! @brief 析构识别器
//! @param recognizer 识别器的指针
NUM_RECOGNIZER_EXPORT void num_recognizer_delete(Recognizer *recognizer);//! @brief 识别图片数据中的手写数字
//! @param recognizer 识别器的指针
//! @param input_image
//! 模型接受的输入图片数据(28×28的float数值数组,0代表白色,1代表黑色)
//! @param result 接受识别结果的数值的指针
//! @return 错误值,成功返回0
NUM_RECOGNIZER_EXPORT int num_recognizer_recognize(Recognizer *recognizer,float *input_image,int *result);//! @brief 识别PNG图片中的手写数字
//! @param recognizer 识别器的指针
//! @param png_path PNG图片文件路径
//! @param result 接受识别结果的数值的指针
//! @return 错误值,成功返回0
NUM_RECOGNIZER_EXPORT int num_recognizer_recognize_png(Recognizer *recognizer,const char *png_path,int *result);

最后,还要记得将前面的extern "C"代码块,以及头文件引用卫哨的#if闭合!如下所示。

#ifdef __cplusplus
} // extern "C"
#endif#endif // NUM_RECOGNIZER_H

现在,这个头文件已经相当完善了。不论用户采用C语言还是C++语言,不论是用于构建手写数字识别库,还是分发给用户使用,它都能够胜任。

11.4.9 命令行工具的实现

手写数字识别库的代码实现已经完成,下面着手命令行工具的编写。在此之前,我们需要确定命令行工具的调用方式。

命令行接口的设计,也就是命令行的参数设计十分重要。友好的参数设计可以极大地方便用户。本例配套提供的recognize命令行工具的参数设计十分简单,只需依次接收两个参数:模型文件路径和PNG图片文件路径。调用示例如下:

recognize models/mnist.onnx 2.png

为了展现C语言接口作为编程界“通用语言”的魅力,该命令行工具将采用C语言而非C++语言编写,它会调用C++编写的手写数字识别库。这也能验证刚刚编写的手写数字识别库的头文件是否完善。

代码实现相当简单,完整的源文件如下所示。

#include <num_recognizer.h>
#include <stdio.h>//! @brief 主函数
//!
//!   命令行参数应有3个:
//!   1. 命令行程序本身的文件名,即recognize;
//!   2. 模型文件路径;
//!   3. 将要识别的PNG图片文件路径。
//!
//!   例如:recognize models/mnist.onnx 2.png
//!
//! @param argc 命令行参数个数
//! @param argv 命令行参数值数组
//! @return 返回码,0表示正常退出
int main(int argc, const char **argv) {// 检查命令行参数个数是否为3个if (argc != 3) {printf("Usage: recognize mnist.onnx 3.png\n");return -1; // 返回错误码-1}int ret = 0;            // 返回码int result = -1;        // 识别结果Recognizer *recognizer; // 识别器指针num_recognizer_init(); // 初始化识别器// 使用模型文件创建识别器,argv[1]即模型文件路径num_recognizer_create(argv[1], &recognizer);// 识别图片文件中的手写数字,argv[2]即图片文件路径if (ret = num_recognizer_recognize_png(recognizer, argv[2], &result)) {// 返回值非0,识别过程发生错误printf("Failed to recognize\n");goto exit_main;}printf("%d\n", result); // 输出识别结果exit_main:num_recognizer_delete(recognizer); // 析构识别器return ret;                        // 返回正常退出的返回码0
}

11.5 构建和运行

代码实现终于告一段落,是不是迫不及待地想要构建并运行它了呢?跟着下面的步骤开始构建吧!

> cd CMake-Book/src/ch011
> mkdir build
> cd build
> cmake -DCMAKE_BUILD_TYPE=Debug ..
...
> cmake --build . --config Debug
...

构建成功后,画一张手写数字的图片。然后,调用构建好的recognize命令行工具尝试识别这幅图,命令调用方式如下:

> ./build/recognize ../models/mnist.onnx ../2.png 
2

成功识别!

如果使用MSVC构建,recognize应该在Debug子目录中,即.\Debug\recognize.exe。另外,在Windows中执行recognize.exe前,还要记得复制zlib.dll、libpng16d.dll(如果采用Release构建模式,则应复制libpng16.dll)、onnxruntime.dll到recognize.exe的同一目录中。

11.6 小结

本章借助CMake组织项目结构和构建流程,引入了多个第三方库,使用C和C++语言实现了一个完整且实用的手写数字识别库项目。相信读者通过本章的实践过程,对CMake的能力有了更加深入的理解,同时也对C和C++程序从设计到实现的完整流程有了一定把握。

本书内容已近尾声,但尚未涉及的CMake相关内容其实还有很多。在项目测试、安装、打包发布等流程中,都可以有CMake发光的地方。希望本书能够作为读者学习使用CMake的一个开始,带领读者踏进C和C++主流开发实践的大门。同时也衷心祝愿读者在将来的学习和工作中,能够用好CMake这一利器,共同建设更加高效的C和C++编程社区!

相关文章:

【CMake】《CMake构建实战:项目开发卷》笔记-Chapter11-实践:基于onnxruntime的手写数字识别库

第11章 实践&#xff1a;基于onnxruntime的手写数字识别库 读者已经跟着本书实践了很多零零散散的实例&#xff0c;应该能够熟练使用CMake来构建C和C程序了吧&#xff01;不过&#xff0c;前面的实例往往都是针对某个特定功能编写的&#xff0c;我们可能很难将它们综合起来实…...

微软主要收入云计算,OFFICE,操作系统和游戏10大分类

微软2024年主要收入10大分类是哪些,再加一列赚钱比例 微软 2024 财年的财务数据可能尚未完全统计完成&#xff0c;且官方可能没有正好按 10 大分类公布主要收入情况。不过&#xff0c;依据微软过往的业务板块和常见的收入来源&#xff0c;下面是模拟的表格&#xff0c;赚钱比例…...

PDF预览-搜索并高亮文本

在PDF.js中实现搜索高亮功能可以通过自定义一些代码来实现。PDF.js 是一个通用的、基于Web的PDF阅读器&#xff0c;它允许你在网页上嵌入PDF文件&#xff0c;并提供基本的阅读功能。要实现搜索并高亮显示文本&#xff0c;你可以通过以下几个步骤来完成&#xff1a; 1. 引入PDF…...

随笔1 认识编译命令

1.认识编译命令 1.1 解释gcc编译命令: gcc test1.cpp -o test1 pkg-config --cflags --libs opencv 命令解析&#xff1a; gcc&#xff1a;GNU C/C 编译器&#xff0c;用于编译C/C代码。 test1.cpp&#xff1a;源代码文件。 -o test1&#xff1a;指定输出的可执行文件名为t…...

【谷歌设置】chrome打开页面在新tab设置(新版)

这里一定要在搜索之后点击账户&#xff0c;然后选择更过设置 选择在新窗口打开搜索结果...

9.翻页器组件设计开发与应用(Vue父子组件通信)

翻页器组件设计开发与使用 写在前面el-pagination分页器的用法用法介绍实战案例实现代码 Vue中的父子组件用法与通信何谓父子组件搭建Paginator.vue子组件组件初步搭建父组件向子组件传参通信子组件向父组件通信 最终代码Index.vuePaginator.vue 总结 欢迎加入Gerapy二次开发教…...

MyBatis-Flex关联查询

MyBatis-Flex关联查询 在 MyBatis-Flex 中&#xff0c;我们内置了 3 种方案&#xff0c;帮助用户进行关联查询&#xff0c;比如 一对多、一对一、多对一、多对多等场景&#xff0c;他们分别是&#xff1a; 方案1&#xff1a;Relations 注解方案2&#xff1a;Field Query方案3…...

Lucene.Net 分词器选择指南:盘古分词 vs 结巴分词

文章目录 前言一、核心特性对比二、典型场景推荐1. 选择盘古分词的场景2. 选择结巴分词的场景 三、关键指标实测对比1. 分词质量测试&#xff08;F1值&#xff09;2. 性能测试&#xff08;单线程&#xff09; 四、如何选择&#xff1f;决策树五、进阶优化建议1. 盘古分词优化方…...

YOLOv11实战电力设备缺陷检测

本文采用YOLOv11作为核心算法框架&#xff0c;结合PyQt5构建用户界面&#xff0c;使用Python3进行开发。YOLOv11以其高效的实时检测能力&#xff0c;在多个目标检测任务中展现出卓越性能。本研究针对电力设备缺陷数据集进行训练和优化&#xff0c;该数据集包含丰富的电力设备缺…...

LINUX 5 vim cat zip unzip

dd u撤销 ctrlr取消撤销 q!刚才的操作不做保存 刚才是编辑模式 现在是可视化模式 多行注释...

Redis的常见命令

Redis的常见命令 官方命令文档&#xff1a;https://redis.io/docs/latest/commands/ 文章目录 Redis的常见命令Redis数据结构介绍Redis通用命令1.String类型2.Hash类型3.List类型List类型的常见命令&#xff1a;利用List结构实现&#xff1a;栈、队列、阻塞队列&#xff1a; 4.…...

LeetCode第131题_分割回文串

LeetCode 第131题&#xff1a;分割回文串 题目描述 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 难度 中等 题目链接 点击在LeetCode中查看题目 示…...

网络钓鱼攻击的威胁和执法部门的作用(第一部分)

在当今的数字世界中&#xff0c;网络犯罪分子不断开发新技术来利用个人、企业和政府机构。 最普遍和最具破坏性的网络犯罪形式之一是网络钓鱼——一种社会工程手段&#xff0c;用于欺骗人们提供敏感信息&#xff0c;例如登录凭据、财务数据和个人详细信息。 随着网络钓鱼攻击…...

用Scala玩转Flink:从零构建实时处理系统

大家好&#xff01;欢迎来到 Flink 的奇妙世界&#xff01;如果你正对实时数据处理充满好奇&#xff0c;或者已经厌倦了传统批处理的漫长等待&#xff0c;那么你找对地方了。本系列文章将带你使用优雅的 Scala 语言&#xff0c;一步步掌握强大的流处理引擎——Apache Flink。 今…...

【LeetCode】算法详解#3 ---最大子数组和

1.题目介绍 给定一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组是数组中的一个连续部分。 1 < nums.length < 105-104 < nums[i] < 104 2.解决思路 要求出…...

基于Python的心衰疾病数据可视化分析系统

【Python】基于Python的心衰疾病数据可视化分析系统 &#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 本项目基于Python开发&#xff0c;重点针对5000条心衰疾病患者的数据进行可视化分析&#…...

oracle批量删除分区

为了清理数据&#xff0c;往往需要删除一些分区 简单查看当前分区 附件 --创建测试表 -- drop table test_part purge;CREATE TABLE test_part (sales_id NUMBER,sale_date DATE,amount NUMBER ) PARTITION BY RANGE (sale_date) INTERVAL (INTERVAL 1 MONTH) -- 每个月创建…...

Android Compose入门和基本使用

文章目录 一、Jetpack Compose 介绍Jetpack Compose是什么Composable 函数命令式和声明式UI组合和继承 二、状态管理什么是状态Stateremember状态提升 三、自定义布局Layout ModifierLayout Composable固有特性测量使用内置组件固有特性测量自定义固有特性测量 四、项目中使用J…...

xLua的Lua调用C#的2,3,4

使用Lua在Unity中创建游戏对象&#xff0c;组件&#xff1a; 相关代码如下&#xff1a; Lua --Lua实例化类 --C# Npc objnew Npc() --通过调用构造函数创建对象 local objCS.Npc() obj.HP100 print(obj.HP) local obj1CS.Npc("admin") print(obj1.Name)--表方法希…...

使用 Python 连接 PostgreSQL 数据库,从 `mimic - III` 数据库中筛选数据并导出特定的数据图表

要使用 Python 连接 PostgreSQL 数据库&#xff0c;从 mimic - III 数据库中筛选数据并导出特定的数据图表&#xff0c;你可以按照以下步骤操作&#xff1a; 安装所需的库&#xff1a;psycopg2 用于连接 PostgreSQL 数据库&#xff0c;pandas 用于数据处理&#xff0c;matplot…...

算法刷题记录——LeetCode篇(2.6) [第151~160题](持续更新)

更新时间&#xff1a;2025-04-06 算法题解目录汇总&#xff1a;算法刷题记录——题解目录汇总技术博客总目录&#xff1a;计算机技术系列博客——目录页 优先整理热门100及面试150&#xff0c;不定期持续更新&#xff0c;欢迎关注&#xff01; 152. 乘积最大子数组 给你一个…...

Dijkstra求最短路径问题(优先队列优化模板java)

首先 1. 主类定义与全局变量 public class Main {static int N 100010; // 最大节点数static int INF Integer.MAX_VALUE; // 无穷大static ArrayList<Pair>[] G new ArrayList[N]; // 邻接表存储图static int[] dis new int[N]; // 存储每个节点的最短…...

【软件测试】性能测试 —— 基础概念篇

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 本期内容主要介绍性能测试相关知识&#xff0c;首先我们需要了解性能测试是什么&#xff0c;本期内容主要介绍性能测试…...

Jmeter脚本使用要点记录

一&#xff0c;使用Bean shell获取请求响应的数据 byte[] result prev.getResponseData(); String str new String(result); System.out.println(str);其中&#xff0c;prev是jmeter的内置变量&#xff0c;直接使用即可。 二&#xff0c;不同的流程中传参数 vars.put(&quo…...

HTML5

HTML5是对HTML标准的第5次修订 HTML是超文本标记语言的简称&#xff0c;是为【网页创建和其它可在网页浏览器中所看到信息】而设计的一种标记性语言。 H5优点&#xff1a;跨平台使用将互联网语义化&#xff0c;更好地被人类与机器所理解降低了对浏览器的依赖&#xff0c;更好地…...

算法—博弈问题

1.博弈问题 1.前提:每一步都是最优解的情况下&#xff0c;先手的那个人已经确定了胜负 用dp数组记录每一步操作后的结果&#xff0c;如果下一步会出现必输结果&#xff0c;那么说明执行这步操作的人必胜&#xff0c;因为必输结果的下一步操作后都是必胜的结果&#xff0c;所以在…...

vector模拟实现(2)

1.构造函数 2.拷贝构造 我们利用push_back和reserve来实现拷贝构造。 3.迭代器的实现 由于底层是一段连续的空间&#xff0c;所以我们选择用指针来实现迭代器。 4.swap 这里的swap函数是有两种方法&#xff0c;一种是开辟一段新的空间&#xff0c;然后memcpy来把原来的数据拷…...

【嵌入式系统设计师】知识点:第3章 嵌入式硬件设计

提示:“软考通关秘籍” 专栏围绕软考展开,全面涵盖了如嵌入式系统设计师、数据库系统工程师、信息系统管理工程师等多个软考方向的知识点。从计算机体系结构、存储系统等基础知识,到程序语言概述、算法、数据库技术(包括关系数据库、非关系型数据库、SQL 语言、数据仓库等)…...

输入框输入数字且保持精度

在项目中如果涉及到金额等需要数字输入且保持精度的情况下&#xff0c;由于输入框是可以随意输入文本的&#xff0c;所以一般情况下可能需要监听输入框的change事件&#xff0c;然后通过正则表达式去替换掉不匹配的文本部分。 由于每次文本改变都会被监听&#xff0c;包括替换…...

Vue3中的Inject用法全解析

大家好呀&#xff5e;今天给大家带来一个超级实用的Vue3技巧&#xff1a;如何使用inject进行组件间的通信&#xff01;如果你对组件间的数据传递、事件触发感兴趣&#xff0c;那一定不要错过这篇文章哦&#xff01;话不多说&#xff0c;直接开整&#xff5e; &#x1f31f; 什么…...

FPGA同步复位、异步复位、异步复位同步释放仿真

FPGA同步复位、异步复位、异步复位同步释放仿真 xilinx VIVADO仿真 行为仿真 综合后功能仿真&#xff0c;综合后时序仿真 实现后功能仿真&#xff0c;实现后时序仿真 目录 前言 一、同步复位 二、异步复位 三、异步复位同步释放 总结 前言 本文将详细介绍FPGA同步复位、异…...

深度解析需求分析:理论、流程与实践

深度解析需求分析&#xff1a;理论、流程与实践 一、需求分析的目标&#xff08;一&#xff09;准确捕捉用户诉求&#xff08;二&#xff09;为开发提供清晰指引 二、需求分析流程&#xff08;一&#xff09;需求获取&#xff08;二&#xff09;需求整理&#xff08;三&#xf…...

QT学习笔记4--事件

1. 鼠标事件 1.1 鼠标按下 QObject中的mousePressEvent()方法 在子类中重写该方法&#xff0c;就可以处理鼠标按下 void myLabel::mousePressEvent(QMouseEvent *ev) {if (ev->button() Qt::LeftButton) {QString str QString("mouse press x %1, y %2").…...

AnimateCC基础教学:json数据结构的测试

一.核心代码: const user1String {"name": "张三", "age": 30, "gender": "男"}; const user1Obj JSON.parse(user1String); console.log("测试1:", user1Obj.name, user1Obj.age, user1Obj.gender);/*const u…...

针对Qwen-Agent框架的源码阅读与解析:FnCallAgent与ReActChat篇

在《针对Qwen-Agent框架的Function Call及ReAct的源码阅读与解析&#xff1a;Agent基类篇》中&#xff0c;我们已经了解了Agent基类的大体实现。这里我们就再详细学习一下FnCallAgent类和ReActChat的实现思路&#xff0c;从而对Agent的两条主流技术路径有更深刻的了解。同时&am…...

在docker中安装RocketMQ

第一步你需要有镜像包&#xff0c;这个2023年的时候docker就不能用pull拉取镜像了&#xff0c;需要你自己找 第二步我用的是FinalShell,用别的可视化界面也用&#xff0c; 在你自己平时放镜像包的地方创建一个叫rocketmq的文件夹&#xff0c;放入镜像包后&#xff0c;创建一个…...

Spring Boot + Kafka 消息队列从零到落地

背景 依赖 <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>2.8.1</version> </dependency> 发送消息 //示例&#xff1a; private final KafkaTemplate<St…...

《打破语言壁垒:bilingual_book_maker 让外文阅读更轻松》

在寻找心仪的外文电子书时&#xff0c;常常会因语言障碍而感到困扰。虽然可以将文本逐段复制到在线翻译工具中&#xff0c;但这一过程不仅繁琐&#xff0c;还会打断阅读的连贯性&#xff0c;让人难以沉浸其中。为了克服这一难题&#xff0c;我一直在寻找一种既能保留原文&#…...

JCR一区文章,壮丽细尾鹩莺算法Superb Fairy-wren Optimization-附Matlab免费代码

本文提出了一种新颖的基于群体智能的元启发式优化算法——壮丽细尾鹩优化算法&#xff08;SFOA&#xff09;,SFOA从精湛的神仙莺的生活习性中汲取灵感。融合了精湛的神仙莺群体中幼鸟的发育、繁殖后鸟类喂养幼鸟的行为以及它们躲避捕食者的策略。通过模拟幼鸟生长、繁殖和摄食阶…...

Kafka 如何实现 Exactly Once

Kafka 中实现 Exactly Once Semantics&#xff08;EOS&#xff0c;精确一次语义&#xff09;&#xff0c;是为了确保&#xff1a; 每条消息被处理一次且仅一次&#xff0c;既不会丢失&#xff0c;也不会重复消费。 这是一种在分布式消息系统中非常难实现的语义。Kafka 从 0.11 …...

在K8S中,内置的污点主要有哪些?

在Kubernetes (K8S)中&#xff0c;内置的污点&#xff08;Taints&#xff09;主要用于自动化的节点亲和性和反亲和性管理。当集群中的节点出现某种问题或满足特定条件时&#xff0c;kubelet会自动给这些节点添加内置污点。以下是一些常见的内置污点&#xff1a; node.kubernete…...

AI大模型:(二)2.1 从零训练自己的大模型概述

目录 1. 分词器训练 1.1 分词器概述 1.2 训练简述 2.预训练 2.1 预训练概述 2.2 预训练过程简介 3.微调训练 3.1 微调训练概述 3.2 微调过程简介 4.人类对齐 4.1 人类对齐概述 4.2 人类对齐训练过程简介 近年来,大语言模型(LLM)如GPT-4、Claude、LLaMA等…...

电动垂直起降飞行器(eVTOL)

电动垂直起降飞行器&#xff08;eVTOL&#xff09;的详细介绍&#xff0c;涵盖定义、技术路径、应用场景、市场前景及政策支持等核心内容&#xff1a; 一、定义与核心特性 eVTOL&#xff08;Electric Vertical Take-off and Landing&#xff09;即电动垂直起降飞行器&#xf…...

LM Studio本地部署大模型

现在的AI可谓是火的一塌糊涂, 看到使用LM Studio部署本地模型非常的方便, 于是我也想在自己的本地试试 LM Studio 简介 LM Studio 是一款专为本地运行大型语言模型&#xff08;LLMs&#xff09;设计的桌面应用程序&#xff0c;支持 Windows 和 macOS 系统。它允许用户在个人电…...

PyTorch 深度学习 || 6. Transformer | Ch6.1 Transformer 框架

1. Transformer 框架...

SLAM文献之-SLAMesh: Real-time LiDAR Simultaneous Localization and Meshing

SLAMesh 是一种基于 LiDAR 的实时同步定位与建图&#xff08;SLAM&#xff09;算法&#xff0c;其核心创新点在于将定位与稠密三维网格重建相结合&#xff0c;通过动态构建和优化多边形网格&#xff08;Mesh&#xff09;来实现高精度定位与环境建模。以下是其算法原理的详细解析…...

[Python] 位置相关的贪心算法-刷题+思路讲解版

位置贪心-题目目录 例题1 - 香蕉商人编程实现输入描述输出描述思路AC代码 例题2 - 分糖果编程实现输入描述输入样例输出样例思路AC代码 例题4 - 分糖果II编程实现输入描述输出描述输入样例思路AC代码 例题3 - 分糖果III编程实现输入描述输出描述输入样例输出样例思路AC代码 例题…...

练习题:125

目录 Python题目 题目 题目分析 需求理解 关键知识点 实现思路分析 代码实现 代码解释 导入 random 模块&#xff1a; 指定范围&#xff1a; 生成随机整数&#xff1a; 输出结果&#xff1a; 运行思路 结束语 Python题目 题目 生成一个指定范围内的随机整数。 …...

实战设计模式之迭代器模式

概述 与上一篇介绍的解释器模式一样&#xff0c;迭代器模式也是一种行为设计模式。它提供了一种方法来顺序访问一个聚合对象中的各个元素&#xff0c;而无需暴露该对象的内部表示。简而言之&#xff0c;迭代器模式允许我们遍历集合数据结构中的元素&#xff0c;而不必了解这些集…...

Spring-AOP详解(AOP概念,原理,动态代理,静态代理)

目录 什么是AOP&#xff1a;Spring AOP核心概念需要先引入AOP依赖&#xff1a;1.切点(Pointcut)&#xff1a;2.连接点&#xff1a;3.通知(Advice)&#xff1a;4.切面&#xff1a; 通知类型&#xff1a;Around:环绕通知&#xff0c;此注解标注的通知方法在目标方法前&#xff0c…...