CMake学习笔记
cmake学习
为什么需要cmake:
C/CPP复杂的编译依赖关系需要通过特定文件指定,方便大型项目管理;
Cmake学习成本远低于Makefile文件;
本文主要参考两篇cmake教程良作:CMake 保姆级教程(上)和CMake 保姆级教程(下),写得比较详细,可直接移步学习;
另外,和原作不同,本文主要基于windows环境验证,基本也是大同小异.
准备工作
windows:检查cmake安装、mingw编译器安装并添加了相关环境变量
linux:cmake安装、gcc -v
、g++ -v
、make -v
均有对应版本输出;
多文件编译
假设存在g++环境,编译以下若干示例文件方法:g++ main.cpp div.cpp multi.cpp sub.cpp add.cpp -o test.exe
1
2
3
4
5
6//add.cpp
int add(int a,int b){
return a+b;
}
1 | //sub.cpp |
1 | //multi.cpp |
1 | //div.cpp |
1 | //main.h |
1 | //main.cpp |
cmake基本使用
以Windows为验证,需要考虑编译器问题,Linux则没有这些问题;
源文件夹同级目录创建CMakeLists.txt
:
cmake_minimum_required
:指定cmake最低版本要求project
:cmake项目命名,还支持其他少用字段;add_executable(目标输出 源文件...)
:输出可执行程序此处因为我的Windows下还有MSVC编译器,直接cmake默认使用MSVC生成sln解决方案等一系列文件而不是Makefile,因此需要简单指明编译器路径;1
2
3
4
5cmake_minimum_required(VERSION 3.20)
set(CMAKE_C_COMPILER "C:/Program Files (x86)/mingw64/bin/gcc.exe") #仅Windows,须在project前
set(CMAKE_CXX_COMPILER "C:/Program Files (x86)/mingw64/bin/g++.exe")
project(MyTest)
add_executable(test add.cpp sub.cpp multi.cpp div.cpp main.cpp) #会自动绑定可执行程序类型test.exe
新建并进入新文件夹build,用于存放中间文件,执行: 1
2
3cmake .. -G "MinGW Makefiles" #windows
cmake .. #linux,..指向txt所在文件 1
2
3make #linux
cmake --build . #windows使用此代替make
可见exe或linux可执行程序已经成功编译;
cmake优化使用
变量
可使用变量存储长字段,再引用就无需键入长字段了:
1
2
3
4#set(Var Value)
set(SRC_LIST add.c div.c main.c mult.c sub.c) #设置
add_executable(test ${SRC_LIST}) #引用
指定C++标准
1 | set(CMAKE_CXX_STANDARD 11) |
指定可执行程序输出路径
1 | set(EXECUTABLE_OUTPUT_PATH "D:/Documents/Desktop/cmake_test/build/test") #绝对路径 |
头文件/源文件处理
当文件数量很多,就需要分文件夹处理,cmake
支持自动搜索文件夹下的源文件和头文件,就无需逐个键入文件名了.
整理后的目录结构: 1
2
3
4
5
6
7
8
9
10
11
12D:.
│ CMakeLists.txt
├─bin #可执行文件目录
├─build
├─include #头文件
│ main.h
└─src #源文件
add.cpp
div.cpp
main.cpp
multi.cpp
sub.cpp
cmake写法如下:使用include_directories
指定头文件目录,aux_source_directory
自动搜索源文件并添加到变量
1
2
3
4
5
6
7
8
9
10cmake_minimum_required(VERSION 3.20)
set(CMAKE_C_COMPILER "C:/Program Files (x86)/mingw64/bin/gcc.exe")
set(CMAKE_CXX_COMPILER "C:/Program Files (x86)/mingw64/bin/g++.exe")
project(MyTest)
set(PROJECT_PATH .) #这里路径是相对txt文件夹
include_directories(${PROJECT_PATH}/include)
aux_source_directory(${PROJECT_PATH}/src SOURCE)
set(EXECUTABLE_OUTPUT_PATH ../bin) #输出路径是相对执行的路径,即build文件夹,而不是txt文件夹
add_executable(test ${SOURCE})
cmake打印
方便调测:执行cmake时会在命令行出现相关字段,也可重定向到log:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15message("This is a message") #打印字符串
message(${output}) #打印变量法一
set(MY_VAR "Hello") #打印变量法二
cmake_print_variables(MY_VAR)
message(${CMAKE_CURRENT_SOURCE_DIR}) #CMakeListss所在的目录绝对路径
message(${PROJECT_SOURCE_DIR}) #cmake的根目录
file(WRITE output.txt "This is written to a file\n") #输出到txt的目录
#一般信息\警告\致命错误:
message(STATUS "status messgae")
message(WARNING "warning messgae")
message(FATAL_ERROR "error messgae")
file文件搜索
aux_source_directory
以外,file方法也能搜索/递归搜索某个目录下符合要求文件:
1
2
3
4
5
6
7#file(标志 变量 搜索条件)
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
#GLOB:搜索满足条件的文件形成列表,存储到变量中
#GLOB_RECURSE:递归地搜索末级目录
静态库与动态库
在代码中我们需要各种各样的库,例如exp、pow算数运算等,但是我们的源文件并没有这个实现,而到别的头文件复制这个实现也是很繁琐的,所以系统提供了一种库的格式,在遇到未定义函数时编译器会从链接的库中查找相关函数,这类库又分为静态库和动态库,将库拷贝到我们代码的过程称为链接;
库格式说明:以windows和linux划分格式并不准确,因为本文的环境(windows+mingw
)最后编译的静态库也是对Unix兼容,因此cmake编译出的是.a
和.dll
,而windows的MSVC才是输出.lib/.dll
,但以下还是按惯用说法说明。
静态库
在windows中是.lib
格式,在linux中是.a
格式;对应静态链接过程,静态链接会将二进制目标文件和静态库代码“拼接”起来,生成最终的可执行文件,这个过程是将静态库的代码完整拷贝(或优化掉未使用部分)到可执行文件中,因此体积一般较大;
动态库
在windows中常见是.dll
格式,在linux中常见是.so
格式,动态链接不会完整把相应代码重定位到目标文件,而是仅添加足够运行的标识名称,在运行时遇到未定义函数,才从动态库中加载相关代码到目标文件,因此文件体积一般较小,内存利用率更高,但牺牲了一些调用性能开销;具体链接方法和静态库是一致的。
链接过程
linux中,当库命名为libxxx.a/libxxx.so
,那么取其xxx,完整的编译命令是:
gcc -o out main.c -L dir -lxxx
,后缀就是链接到该静态库;该命令实际上可以拆分成gcc -c main.c
和gcc -o out main.o -L dir -lxxx
,前者是将我们的源文件编译成二进制目标文件,后者是将该目标文件和库链接并生成可执行文件(-L
指定库所在目录);
这里提出了两个注意问题,其一调用函数不一定就引起静态链接,例如exp(2)等常数运算在编译时就被优化为值的替代,即C++
11的constexpr
特性,常量表达式都可能先被编译器优化掉;其二-lxxx
须放置语句末尾,这和语法分析相关。
静态库适用于那种比较固定、长久不更新的库,否则每次更新都需要重新生成可执行程序,比较繁琐。
g++构建静态库
基于windows环境,linux请注意格式差异: 文件结构: 1
2
3
4
5
6
7
8
9D:.
├─include
| main.h
├─lib
└─src #删掉main.cpp
add.cpp
div.cpp
multi.cpp
sub.cpp
1 | g++ -c *.cpp -I ../include |
可见lib文件夹生成了.lib
文件;
静态库应用
1 | D:. |
其中myCal.cpp
: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(){
int a,b;
std::cout<<"Please Input:"<<std::endl;
std::cin>>a;
std::cin>>b;
std::regex pattern("^\\d+$");
if(!std::regex_match(std::to_string(a),pat tern)||!std::regex_match(std::to_string(b),pattern)){
std::cerr<<"parameter fault!"<<std::endl;
return -1;
}
std::cout<<"sum:"<<add(a,b)<<std::endl;
std::cout<<"sub:"<<sub(a,b)<<std::endl;
std::cout<<"multi:"<<multi(a,b)<<std::endl;
std::cout<<"divide:"<<divi(a,b)<<std::endl;
system("pause");
return 0;
}g++ myCal.cpp -o test -L ./lib -lmath
,即能成功编译出test.exe
;库函数接口必须在main.h
中定义,且头文件路径要么在myCal.cpp
定义,要么编译时使用-I
参数指定。
g++动态库构建
构建动态库需要额外指定gcc参数-fpic
,打包时使用gcc
而非ar
:
1
2g++ *.cpp -c -fpic -I ../include #构建.o
g++ -shared *.o -o ../lib/libmath.dll #打包成.dll
无法找到dll
动态库使用方法是和静态库一致的,然而编译完双击运行,会发现test.exe
提示没有找到dll,因为该程序是运行时链接,编译时只加载了动态库名称,真正的代码没有载入,所以运行时报错,这就是设置环境变量的必要性,使得系统程序能够找到对应的运行库路径,linux下可以参考原文的这里;
windows下没有必要为此设置环境变量,只需要将.dll
拖拽到和test.exe
同级目录,即可自动搜索得到,也可正常运行了。
cmake制作静态库/动态库
对gcc而言,通过编译参数可生成动态库、静态库;对MSVC而言,VS本身支持创建库项目;cmake也支持编译和链接相应的库;
1
2
3
4
5add_library(库名称 STATIC 源文件1 [源文件2] ...) #静态库
add_library(库名称 SHARED 源文件1 [源文件2] ...) #动态库
add_library(math STATIC sub.cpp add.cpp...) #编译libmath.a/libmath.lib
add_library(math SHARED sub.cpp add.cpp...) #编译libmath.so/libmath.dll
指定静态库/动态库输出路径
EXECUTABLE_OUTPUT_PATH
除了适用于可执行程序,也可适用于动态库路径;LIBRARY_OUTPUT_PATH
可适用于静态库和动态库:
1
2set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
add_library(...)
cmake构建与链接静态库
文件结构: 1
2
3
4
5
6
7
8
9
10
11D:.
│ CMakeLists.txt
├─build
├─include
│ main.h
├─lib
└─src
add.cpp
div.cpp
multi.cpp
sub.cppCMakeLists.txt
如下:
1 | cmake_minimum_required(VERSION 3.20) |
完成后lib将出现.a
格式静态库,且根目录出现编译的可执行程序;
cmake构建与链接动态库
只有一些差别:
编译
add_library
使用参数SHARED
;链接使用
target_link_libraries
而不是link_libraries
,第一个参数为目标库或者可执行程序,且放置在add_executable之后,因为其运行后才加载;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15cmake_minimum_required(VERSION 3.20)
set(CMAKE_C_COMPILER "C:/Program Files (x86)/mingw64/bin/gcc.exe")
set(CMAKE_CXX_COMPILER "C:/Program Files (x86)/mingw64/bin/g++.exe")
project(MyTest)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
file(GLOB SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_library(math SHARED ${SOURCE})
link_directories(${PROJECT_SOURCE_DIR}/lib) #动态库路径(非环境变量
file(GLOB MAIN ${CMAKE_CURRENT_SOURCE_DIR}/myCal.cpp) #测试cpp
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) #执行程序输出路径
add_executable(test ${MAIN}) #生成可执行程序
target_link_libraries(test math) #链接动态库
PUBLIC/PRIVATE/INTERFACE
常用的target_link_libraries
实际上缺省了链接参数,完整表示应为target_link_libraries(A PUBLIC B PUBLIC C)
;
target_include_directories
和target_link_libraries
常常被放在一起讨论,因为这三个权限是类似的;
PUBLIC:表示
PUBLIC
后的库均以二进制链接到前面目标,并且导出该符号给第三方使用;例如target_link_libraries(A PUBLIC B PUBLIC C)
,B、C链接到A库,任何库再PUBLIC A,意味着新动态库都链接到A、B、C且可使用三者的接口;PRIVATE:
PRIVATE
后的库会链接到目标,但是无法导出其符号给第三方使用;上述例子,D PRIVATE A
,D能使用A的接口,但是无法使用B、C接口;INTERFACE:
INTERFACE
后的库仅导出符号,而不会直接链接到目标,D INTERFACE A
,D不会链接到A,但是A、B、C接口均会暴露到D,因此如果有对象PUBLIC D,就能使用这些接口,D本身仅传递信息,并没有使用这些接口;
因此,这里提到了三种实践:
如果源文件(例如CPP)中包含第三方头文件,但是头文件(例如hpp)中不包含该第三方文件头,采用PRIVATE。
如果源文件和头文件中都包含该第三方文件头,采用PUBLIC。
如果头文件中包含该第三方文件头,但是源文件(例如CPP)中不包含,采用 INTERFACE。
所谓头文件包含第三方头,实际上就是需要把接口向上层再暴露,因此需要PUBLIC
或INTERFACE
,如果该库没有在源文件使用,那就是INTERFACE
;如果头文件都没有第三方头,那就可以考虑PRIVATE
,因为上层无需这个接口。
字符操作
拼接
1 | set(TEMP "tempdir") |
筛选
1 | file(GLOB FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h") |
list方法
移除FILE第一个元素,
list(POP_FRONT FILE)
移除FILE最后一个元素,
list(POP_BACK FILE)
移除重复元素,
list(REMOVE_DUPLICATES FILE)
移除某序号元素,
list(REMOVE_AT MY_LIST x)
,x从0开始;list(APPEND FILE XXX)
,尾插XXXlist(PREPEND FILE XXX)
,头插XXX
宏调试add_definitions(-DDEBUG)
c++使用代码#if NAME
时,可以使用-D
指定该宏生效,在bash中g++ main.cpp -DNAME
,cmake中使用add_definitions(-DNAME)
;
cmake中的常用宏
宏 | 功能 |
---|---|
PROJECT_SOURCE_DIR | 使用cmake命令后紧跟的目录,一般是工程的根目录 |
PROJECT_BINARY_DIR | 执行cmake命令的目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的CMakeLists.txt所在的路径 |
CMAKE_CURRENT_BINARY_DIR | target 编译目录 |
EXECUTABLE_OUTPUT_PATH | 重新定义目标二进制可执行文件的存放位置 |
LIBRARY_OUTPUT_PATH | 重新定义目标链接库文件的存放位置 |
PROJECT_NAM | 返回通过PROJECT指令定义的项目名称 |
CMAKE_BINARY_DIR | 项目实际构建路径,假设在build目录进行的构建,那么得到的就是这个目录的路径 |
嵌套Cmake
这里的源码来自上述源码拆分,例如分成加减、乘除,仅作实验比较简单,文件结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22D:.
│ CMakeLists.txt
├─bin
├─build
├─include
│ cal_add_sub.h
│ cal_mul_div.h
├─lib
├─src1
│ add.cpp
│ sub.cpp
│ CMakeLists.txt
├─src2
│ div.cpp
│ multi.cpp
│ CMakeLists.txt
├─test1
│ test1.cpp
│ CMakeLists.txt
└─test2
test2.cpp
CMakeLists.txt
目标: 根据一个根目录的父CMakeLists和四个子目录的子CMakeLists,分别生成一个静态库、一个动态库、两个可执行文件,分别存储在lib和bin文件夹;
子CMakeLists可以访问父CMakeLists的变量,因此一般将路径定义在父CMakeLists,并且使用add_subdirectory
确定子目录对象,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#根目录
cmake_minimum_required(VERSION 3.20)
set(CMAKE_C_COMPILER "C:/Program Files (x86)/mingw64/bin/gcc.exe")
set(CMAKE_CXX_COMPILER "C:/Program Files (x86)/mingw64/bin/g++.exe")
project(ALL)
#头文件依赖
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
include_directories(${HEAD_PATH})
#静态库、动态库、可执行输出路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
set(DLL_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
set(BIN_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
#静态库、动态库、可执行文件名称(直观统计目标文件)
set(ASLIB_NAME addsub)
set(MDDLL_NAME muldiv)
set(TEST1 test1)
set(TEST2 test2)
#子目录名
add_subdirectory(src1)
add_subdirectory(src2)
add_subdirectory(test1)
add_subdirectory(test2)
1 | #src1 |
1 | #src2 |
1 | #test1 |
1 | #test2 |
在build目录运行 1
2cmake .. -G "MinGW Makefiles"
cmake --build .