文章目录
- 前言
- [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
