CMake学习篇[3]---CMake进阶+嵌套CMakeLists+多层级关系

文章目录

  • 前言
  • [1. 什么是模块?](#1. 什么是模块?)
  • [2. 嵌套CMakeLists](#2. 嵌套CMakeLists)
    • [2.1 项目结构](#2.1 项目结构)
    • [2.2 CMakeLists](#2.2 CMakeLists)
      • [2.2.1 根节点](#2.2.1 根节点)
      • [2.2.2 子节点](#2.2.2 子节点)
    • [2.3 命令说明](#2.3 命令说明)
  • 总结

前言

在第二篇中我们使用CMake做了一个比较完整的构建,涉及到了库的生成与链接,同时在VS中讲了VS的配置与CMakeGUI的一些对应设置关系。

看完第二篇,其实自己做一个简单的小项目其实没有太大的问题,但是在企业开发中,一般都是大型项目,就涉及到很多的模块之类的开发,同时还要引用这些模块,那么在构建项目的时候,如果都堆到一起,就会导致CMakeLists过大,同时难以阅读。

所以对于大型项目,将模块分开管理是一个更好的选择。

1. 什么是模块?

关于模块module这个概念,以前一直认为是C++的某一个专属名词,类似于动态库,静态库,C++里面应该是一个名叫module的一个关键字等。后来发现模块只是一个逻辑上的概念,而模块这个东西在开发和使用的时候,具体到物理层面是以"库文件"承载。

虽然C++20有了个模块化编程概念,它是为了解决:编译时间长、容易出现宏冲突、重复定义、依赖关系不明确等问题,这个不过多追求,现在来说,我们只需要知道模块这个东西,具体到实际使用上,就是使用库,封装库,这就是模块。

在CMake里面add_library的使用中,一般这样用

cmake 复制代码
add_library(库名称 库类型(静态,动态,模块) 源文件路径)

这里的库类型就可以指定为模块MODULE
MODULE 是一种特殊的库类型,主要用于创建 动态加载模块(Dynamic Loadable Modules),与我们常用的 STATIC(静态库)和 SHARED(动态库)在设计目的、使用场景和链接方式上有本质区别。

库类型 扩展名(平台差异) 核心用途 链接 / 加载方式 关键特征
STATIC Windows: .lib Linux/macOS: .a 编译时直接将库的代码 "打包" 到可执行文件中 编译链接阶段 静态链接(嵌入可执行文件) 可执行文件独立运行,不依赖外部文件;库更新需重新编译可执行文件
SHARED Windows: .dll+.lib(导入库)Linux: .so macOS: .dylib 编译时仅记录库的 "符号信息",运行时从外部加载库文件 编译链接阶段 动态链接(依赖导入库),运行时加载 .dll/.so 可执行文件体积小;库更新无需重新编译可执行文件;运行时必须找到库文件
MODULE Windows: .dllLinux: .so macOS: .bundle/.dylib 作为 "插件" 或 "扩展模块",由可执行文件在 运行时手动加载(非自动链接) 不参与编译链接,仅运行时通过代码加载 完全独立于可执行文件;可动态添加 / 移除功能(如插件);无需导入库

刚好提及到了模块的相关内容,所以这里做了一个阐述,这篇博客对于模块不做太深入的研究,后续如果实战有相关内容,考虑写一篇。

下面进入正题,先看一个例子

2. 嵌套CMakeLists

2.1 项目结构

demo5.cpp

cpp 复制代码
#include<iostream>

#include"headdouble.h" 
#include"headint.h" 

int main()
{
	int a=6,b=2;
	std::cout<<"sumInt(a,b)="<< SumInt(a,b)<<std::endl;
	std::cout<<"sumDouble(a,b)="<< SumInt(a,b)<<std::endl;
	return 0;
} 

headint.h

cpp 复制代码
#ifndef HEADINT_H
#define HEADINT_H

// 导出修饰符,用于动态库项目
#ifdef MY_DEMO5SHARED_EXPORTS
#define DEMO5SHARED_API __declspec(dllexport)
#else
#define DEMO5SHARED_API __declspec(dllimport)
#endif
DEMO5SHARED_API  int SumInt(int a,int b);

#endif

headdouble.h

cpp 复制代码
#ifndef HEADDOUBLE_H
#define HEADDOUBLE_H

double SumDouble(double a,double b);

#endif

sumDouble.cpp

cpp 复制代码
#include<iostream>
#include"headdouble.h" 

double SumDouble(double a,double b)
{
	return a+b;
}

sumInt.cpp

cpp 复制代码
#include<iostream>
#include"headint.h" 

int SumInt(int a,int b)
{
	return a+b;
}

2.2 CMakeLists

2.2.1 根节点

根CMakeLists

cmake 复制代码
#CMake最小要求的版本
cmake_minimum_required(VERSION 3.11.0)

#工程名称
project(Demo5)

set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)

#测试程序生成路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)

#头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)

#库名称
set(SUMINT_LIB sumint_static_lib)
set(SUMDOUBLE_LIB sumdouble_shared_lib)

#可执行程序名称
set(APP_NAME demo5exe)

#添加子目录
add_subdirectory(sumdouble)
add_subdirectory(sumint)
add_subdirectory(demo5)

这里我们定义了很多的变量,这些变量在根CMakeLists中设置后,在整个项目构建的CMakeLists中就相当于是全局变量,在它下面的子CMakeLists中,就可以访问到这些变量。

CMakeLists用树的方式进行组织,在下图的项目结构中,最外层的CMakeLists就是根节点,下面demo5/CMakeLists,sumint/CMakeLists,sumdouble/CMakeLists,均为子节点,它们中可以直接通过${变量名}的方式进行访问根节点定义的变量。

这样做的好处就是,可以使用统一的名称进行管理,名称背后的值可以随意更改。

这里我们相比于前两节,新增了一个命令add_subdirectory(xxxx),这里具体的下面会阐述,这里最基本的用法就是将指定目录下的CMakeLists作为当前CMakeLists的子节点,相当于构建树。

这个根节点的CMakeLists,它没有生成库,没有生成可执行文件,更像是一个统筹的管理者,定义了一些变量,作为下面子节点的一些规范,并将所有的子节点的生成内容,全部构建到project名为Demo5的项目中,在VS中是解决方案*.sln

2.2.2 子节点

sumdouble/CMakeLists

负责生成一个静态库,这里我们不使用project命令(主要是我试了也不生效),因为project其实是生成一个解决方案,但是实际上这是个子节点,一个具体的构建项目,只有一个解决方案,所以是以根节点中的project命令为准。

cmake 复制代码
#CMake最小要求的版本
cmake_minimum_required(VERSION 3.11.0)

#将当前目录下的所有文件(不包括CMakeLists)存储到SRC字段下
aux_source_directory(./ SRC)

#包含头文件路径,路径变量来自于父节点的CMakeLists
include_directories(${HEAD_PATH})

#LIBRARY_OUTPUT_PATH 是 CMake 的一个内置变量,用来指定编译生成的库文件(静态库 / 动态库)的存放目录
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

#生成静态库
add_library(${SUMDOUBLE_LIB} STATIC ${SRC})

sumint/CMakeLists

负责生成一个动态库

cmake 复制代码
#CMake最小要求的版本
cmake_minimum_required(VERSION 3.11.0)

#将当前目录下的所有文件(不包括CMakeLists)存储到SRC字段下
aux_source_directory(./ SRC)

#包含头文件路径,路径变量来自于父节点的CMakeLists
include_directories(${HEAD_PATH})

#LIBRARY_OUTPUT_PATH 是 CMake 的一个内置变量,用来指定编译生成的库文件(静态库 / 动态库)的存放目录
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})

#生成静态库
add_library(${SUMINT_LIB} SHARED ${SRC})

demo5/CMakeLists

负责生成可执行文件,该文件链接sumint和sumdouble库,并指定宏从而调用动态库,对于无法找到DLL的问题,在第二篇中已经讲过,这里不做赘述。

cmake 复制代码
#CMake最小要求的版本
cmake_minimum_required(VERSION 3.11.0)

#将当前目录下的所有文件(不包括CMakeLists)存储到SRC字段下
aux_source_directory(./ SRC)

#包含头文件路径,路径变量来自于父节点的CMakeLists
include_directories(${HEAD_PATH})

#包含库路径,并链接库
link_directories(${LIB_PATH})
link_libraries(${SUMINT_LIB} ${SUMDOUBLE_LIB})

#如果不指定名称会自动生成"项目名_EXPORTS"的宏
target_compile_definitions(${SUMINT_LIB} PUBLIC MY_DEMO5SHARED_EXPORTS)

#EXECUTABLE_OUTPUT_PATH是 CMake的一个内置变量,用来指定生成的可执行文件存放位置
set(EXECUTABLE_OUTPUT_PATH  ${EXEC_PATH})

#生成可执行文件
add_executable(${APP_NAME} ${SRC})

#将库文件拷贝到可执行文件夹中,处理找不到dll的问题
add_custom_command(TARGET ${APP_NAME} POST_BUILD
                   COMMAND ${CMAKE_COMMAND} -E copy_directory
                   ${LIB_PATH}/$<IF:$<CONFIG:Debug>,Debug,Release> $<TARGET_FILE_DIR:${APP_NAME}>
                   COMMENT "Copying dynamic library to executable directory"
                   VERBATIM)

2.3 命令说明

add_subdirectory

add_subdirectory 是 CMake 的一个核心命令,用来添加一个子目录,让 CMake 去处理那个目录下的 CMakeLists.txt 文件。它常用于多目录、多模块的大型项目,让每个子模块都能独立管理自己的构建规则。

语法:

cmake 复制代码
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

source_dir:必填参数,指定要添加的子目录的路径,这个路径是相对于当前CMakeLists.txt文件所在目录的相对路径,或者是一个完整的绝对路径。

binary_dir:可选参数,指定子目录对应的构建目录。如果不指定这个参数,CMake 默认会在与source_dir同级的目录下创建一个同名的构建目录(例如source_dir是src,那么默认构建目录就是build/src) 。你可以通过指定该参数来自定义构建目录,比如将构建目录设置到其他位置,或者给构建目录指定一个更具描述性的名称,如add_subdirectory(src build/my_src_build) ,这样构建目录就会是build/my_src_build 。在生成像 Visual Studio 等 IDE 项目时,binary_dir的最后一部分(目录名)会作为项目筛选器(Filter)的名称。

EXCLUDE_FROM_ALL:可选参数,当使用这个参数时,子目录中的目标(如通过add_executable、add_library创建的可执行文件、库等)默认不会被构建,除非显式地指定要构建它们。例如,在开发过程中,有些子目录可能是用于实验或测试的,你不希望每次构建整个项目时都构建它们,就可以使用EXCLUDE_FROM_ALL参数。

作用和使用场景
项目模块化:当项目规模较大时,可以将不同功能模块放在不同的子目录中,每个子目录都有自己的CMakeLists.txt文件来管理该模块的源文件、库依赖等。通过add_subdirectory,主项目的CMakeLists.txt可以方便地引入各个子模块,实现代码的模块化管理。例如,一个图形处理项目,可能有图像处理算法模块、用户界面模块等,分别放在image_processing和ui子目录中,主项目的CMakeLists.txt可以通过add_subdirectory(image_processing)和add_subdirectory(ui)来管理这两个模块。

构建目录分离:利用binary_dir参数,可以将源文件目录和构建目录分离开来,保持源文件目录的整洁,避免在源文件目录中生成大量的构建产物(如.o文件、可执行文件等)。例如,在根目录的CMakeLists.txt中设置add_subdirectory(src build/src),这样所有关于src子目录的构建产物都会生成到build/src目录下,而src目录本身只保留源文件。

选择性构建:结合EXCLUDE_FROM_ALL参数,对于一些不常用的子模块或者临时开发的功能,可以在需要时才进行构建,提高构建效率。比如在一个大型软件中,有一个用于特定硬件设备的驱动模块,在普通测试时不需要构建它,就可以在添加该子目录时带上EXCLUDE_FROM_ALL参数,当需要测试硬件功能时,再手动指定构建该模块。

总结

相比于一个CMakeLists走天下,嵌套CMakeLists的方式,可以把一个大型项目进行拆分,每一个CMakeLists分工明确,职责更加单一,便于维护,优点设计模式的味道了哈哈哈哈哈

这种情况一般来说对于大型项目式必修的,但是自己写小小的demo那真用不上。不过也总归不会一直写demo,所以还是得学。


代码以及项目文件已同步上传到gitee:https://gitee.com/Edwinwzy1/cmake-learn

相关推荐
江苏世纪龙科技8 小时前
新能源汽车故障诊断与排除虚拟实训软件——赋能职业教育新工具
学习
m0_7482402510 小时前
Windows编程+使用C++编写EXE加壳程序
开发语言·c++·windows
兔兔爱学习兔兔爱学习11 小时前
Spring Al学习7:ImageModel
java·学习·spring
LoveXming11 小时前
Chapter14—中介者模式
c++·microsoft·设计模式·中介者模式·开闭原则
江苏世纪龙科技14 小时前
新能源汽车动力系统拆装与检测实训MR软件介绍-比亚迪秦EV标准版
学习
im_AMBER14 小时前
数据结构 09 二叉树作业
数据结构·笔记·学习
杨筱毅15 小时前
【C++】【常见面试题】最简版带大小和超时限制的LRU缓存实现
c++·面试
陌路2015 小时前
C23构造函数与析构函数
开发语言·c++
_OP_CHEN16 小时前
C++进阶:(二)多态的深度解析
开发语言·c++·多态·抽象类·虚函数·多态的底层原理·多态面试题
wdfk_prog17 小时前
[Linux]学习笔记系列 -- [kernel][time]hrtimer
linux·笔记·学习