文章目录
- 一、基于CMake,对文件/目录组织规范
- 1.推荐的目录组织方式
- 2.划分子项目
- 3.根项目的 CMakeLists.txt 配置
- 4.子项目的 CMakeLists.txt 配置
- 5.子项目的头文件
- 6.子项目的源文件
- 补充:GLOB 和 GLOB_RECRUSE 的区别
- 7.头文件和源文件的一一对应关系
- 8.只有头文件,没有源文件的情况
- 9.每新增一个功能模块,需要创建两个文件
- 10.一个模块依赖其他模块,则应导入他的头文件
- 11.以项目名为名字空间(namsepace),避免符号冲突
- 12.依赖另一个子项目,则需要链接他
- 13.CMake 的 include 功能
- 补充:macro 和 function 的区别
一、基于CMake,对文件/目录组织规范
1.推荐的目录组织方式
目录组织格式:
- 项目名/include/项目名/模块名.h
- 项目名/src/模块名.cpp
CMakeLists.txt 中写:
- target_include_directories(项目名 PUBLIC include)
源码文件中写:
#include <项目名/模块名.h>
项目名::函数名();
头文件(项目名/include/项目名/模块名.h)中写:
#pragma once
namespace 项目名 {
void 函数名();
}
实现文件(项目名/src/模块名.cpp)中写:
#include <项目名/模块名.h>
namespace 项目名 {
void 函数名() { 函数实现 }
}
2.划分子项目
eg:
大型的项目,往往会划分为几个子项目。
即使你只有一个子项目,也建议你先创建一个子目录,方便以后追加新的子项目。
上图的案例中,我们在根目录下,创建了两个子项目 biology 和 pybmain,他们分别在各自的目录下有自己的 CMakeLists.txt。
3.根项目的 CMakeLists.txt 配置
cmake_minimum_required(VERSION 3.18)
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}")
project(CppCMakeDemo LANGUAGES CXX)
include(MyUsefulFuncs)
add_subdirectory(pybmain)
add_subdirectory(biology)
- 在根项目的 CMakeLists.txt 中,设置了默认的构建模式,设置了统一的 C++ 版本等各种选项。
- 然后通过 project 命令初始化了根项目。
- 随后通过 add_subdirectory 把两个子项目 pybmain 和 biology 添加进来(顺序无关紧要),这会调用 pybmain/CMakeLists.txt 和 biology/CMakeLists.txt。
4.子项目的 CMakeLists.txt 配置
file(GLOB_RECURSE srcs CONFIGURE_DEPENDS src/*.cpp include/*.h)
add_library(biology STATIC ${srcs})
target_include_directories(biology PUBLIC include)
-
子项目的 CMakeLists.txt 就干净许多,只是创建了 biology 这个静态库对象,并通过 GLOB_RECRUSE 为他批量添加了所有位于 src 和 include 下源码和头文件。
-
根项目的 CMakeLists.txt 负责处理全局有效的设定。而子项目的 CMakeLists.txt 则仅考虑该子项目自身的设定,比如他的头文件目录,要链接的库等等。
5.子项目的头文件
- 这里我们给 biology 设置了头文件搜索路径 include。
- 因为子项目的 CMakeLists.txt 里指定的路径都是相对路径,所以这里指定 include 实际上是:根/biology/include。
- 注意我们用了 PUBLIC 修饰符,这是为了让链接 biology 的 pybmain 也能够共享 根/biology/include 这个头文件搜索路径。
6.子项目的源文件
- 这里我们给 biology 批量添加了 src/*.cpp 下的全部源码文件。
- 明明只有 .cpp 需要编译,为什么还添加了 include/.h?为了头文件也能被纳入 VS 的项目资源浏览器,方便编辑。(Linux则不需要添加)
- 因为子项目的 CMakeLists.txt 里指定的路径都是相对路径,所以这里指定 src 实际上是:根/biology/src。
补充:GLOB 和 GLOB_RECRUSE 的区别
file (GLOB myvar CONFIGURE_DEPENDS src/*.cpp)
file (GLOB_RECURSE myvar CONFIGURE_DEPENDS src/*.cpp)
-
疑问1:都是按照通配符批量匹配文件,有什么区别?
GLOB: src/main.cpp(√) src/test/main.cpp(×)
GLOB_RECURSE: src/main.cpp(√) src/test/main.cpp(√)
区别在于 GLOB_RECURSE 允许 * 匹配嵌套的目录。 -
疑问2:加了 CONFIGURE_DEPENDS 这个选项有什么区别?
如果不加,在你创建新文件时,myvar 不会自动更新,还是旧的那几个文件,可能出现 undefined symbol,需要重新运行 cmake -B build 才能更新。
加了,则每次 cmake --build 时自动检测目录是否更新,如果目录有新文件了,CMake 会自动帮你重新运行 cmake -B build 更新 myvar 变量。
注:如果脚本每次都执行cmake -B build && cmake --build build则不需要关心
7.头文件和源文件的一一对应关系
-
通常每个头文件都有一个对应的源文件,两个文件名字应当相同(方便我们理解,也方便 IDE 跳转),只有后缀名不一样。
-
如果是一个类,则文件名应和类名相同,方便查找(Animal.cpp)。
-
头文件中包含函数和类的声明,源文件则包含他们的实现。
8.只有头文件,没有源文件的情况
- 有时我们会直接把实现直接写在头文件里,这时可以没有与之对应的源文件,只有一个头文件。
- 注意:在头文件里直接实现函数时,要加 static 或 inline 关键字
9.每新增一个功能模块,需要创建两个文件
- 添加一个新功能模块 Carer 时,同时添加同名的源文件和头文件。
- 头文件中的声明和源文件中的实现一一对应。
10.一个模块依赖其他模块,则应导入他的头文件
如果模块 Carer 的头文件 Carer.h 虽然引用了其他模块中的 Animal 类,但是他里面并没有解引用 Animal,只有源文件 Carer.cpp 解引用了 Animal。那么这个头文件是不需要导入 Animal.h 的,只需要一个前置声明 struct Animal,只有实际调用了 Animal 成员函数的源文件需要导入 Animal.h。
- 好处:加快编译速度,防止循环引用。
11.以项目名为名字空间(namsepace),避免符号冲突
在声明和定义外面都套一层名字空间,例如此处我的子项目名是 biology,那我就 biology::Animal。避免暴露全局的 Animal。
- 这是因为万一有个“不拘一格”的第三方库也暴露个全局的 Animal,两个符号就会发生冲突,由于类符号都具有 weak 属性,链接器会随机选择一个覆盖掉,非常危险!
12.依赖另一个子项目,则需要链接他
- 让 pybmain 链接上 biology:target_link_libraries(pybmain PUBLIC biology)
- 由于 PUBLIC 属性具有传染性,根/biology/include 现在也加入 pybmain 的头文件搜索路径了,因此 pybmain 里可以 #include 到 biology 的头文件。
- 同理如果又有一个 target_link_libraries(zxxpig PUBLIC pybmain) 那么 zxxpig 也有 pybmain 和 biology 的所有头文件搜索路径了。
13.CMake 的 include 功能
和 C/C++ 的 #include 一样,CMake 也有一个 include 命令。
你写 include(XXX),则他会在 CMAKE_MODULE_PATH 这个列表中的所有路径下查找 XXX.cmake 这个文件。
- 这样你可以在 XXX.cmake 里写一些你常用的函数,宏,变量等。
补充:macro 和 function 的区别
macro 相当于直接把代码粘贴过去,直接访问调用者的作用域。这里写的相对路径 include 和 src,是基于调用者所在路径。
function 则是会创建一个闭包,优先访问定义者的作用域。这里写的相对路径 include 和 src,则是基于定义者所在路径。
- cmake-function
- cmake-macro
include 相当于直接把代码粘贴过去,直接访问调用者的作用域。这里创建的变量和外面共享,直接 set(key val) 则调用者也有 ${key} 这个变量了。
function 中则是基于定义者所在路径,优先访问定义者的作用域。这里需要 set(key val PARENT_SCOPE) 才能修改到外面的变量。
参考:
- parallel101/course