cmake学习

为什么需要cmake:

  • C/CPP复杂的编译依赖关系需要通过特定文件指定,方便大型项目管理;

  • Cmake学习成本远低于Makefile文件;

本文主要参考两篇cmake教程良作:CMake 保姆级教程(上)CMake 保姆级教程(下),写得比较详细,可直接移步学习;

另外,和原作不同,本文主要基于windows环境验证,基本也是大同小异.

准备工作

windows:检查cmake安装mingw编译器安装并添加了相关环境变量

linuxcmake安装gcc -vg++ -vmake -v均有对应版本输出;

多文件编译

假设存在g++环境,编译以下若干示例文件方法:g++ main.cpp div.cpp multi.cpp sub.cpp add.cpp -o test.exe

1
2
3
4
5
6
//add.cpp
#include <iostream>
#include "main.h"
int add(int a,int b){
return a+b;
}

1
2
3
4
5
6
//sub.cpp
#include <iostream>
#include "main.h"
int sub(int a,int b){
return a-b;
}
1
2
3
4
5
6
//multi.cpp
#include <iostream>
#include "main.h"
long multi(int a,int b){
return a*b;
}
1
2
3
4
5
6
//div.cpp
#include <iostream>
#include "main.h"
int divi(int a,int b){
return a/b;
}
1
2
3
4
5
6
7
8
9
10
//main.h
#ifndef MAIN_H
#define MAIN_H

int add(int a,int b);
int divi(int a,int b);
int sub(int a,int b);
long multi(int a,int b);

#endif
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
//main.cpp
#include <iostream>
#include <regex>
#include "main.h"

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),pattern)||!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;
}

cmake基本使用

Windows为验证,需要考虑编译器问题,Linux则没有这些问题;

源文件夹同级目录创建CMakeLists.txt

  • cmake_minimum_required:指定cmake最低版本要求

  • project:cmake项目命名,还支持其他少用字段;

  • add_executable(目标输出 源文件...):输出可执行程序

    1
    2
    3
    4
    5
    cmake_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
    此处因为我的Windows下还有MSVC编译器,直接cmake默认使用MSVC生成sln解决方案等一系列文件而不是Makefile,因此需要简单指明编译器路径;

新建并进入新文件夹build,用于存放中间文件,执行:

1
2
3
cmake .. -G "MinGW Makefiles"    #windows

cmake .. #linux,..指向txt所在文件
可见build下生成了Makefile,执行:
1
2
3
make #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
2
set(EXECUTABLE_OUTPUT_PATH "D:/Documents/Desktop/cmake_test/build/test") #绝对路径
set(EXECUTABLE_OUTPUT_PATH ..) #输出到build的上级目录(txt的目录)

头文件/源文件处理

当文件数量很多,就需要分文件夹处理,cmake支持自动搜索文件夹下的源文件头文件,就无需逐个键入文件名了.

整理后的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
D:.
│ 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
10
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(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
15
message("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.cgcc -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
9
D:.
├─include
| main.h
├─lib
└─src #删掉main.cpp
add.cpp
div.cpp
multi.cpp
sub.cpp

1
2
g++ -c *.cpp -I ../include
ar rcs ../lib/libmath.lib *.o #ar工具将.o打包成.lib

可见lib文件夹生成了.lib文件;

静态库应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:.
│ myCal.cpp
├─include
│ main.h
├─lib
│ libmath.lib
└─src
add.cpp
add.o
div.cpp
div.o
multi.cpp
multi.o
sub.cpp
sub.o

其中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
#include <iostream>
#include <regex>
#include "./include/main.h"

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
2
g++ *.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
5
add_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
2
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
add_library(...)

cmake构建与链接静态库

文件结构:

1
2
3
4
5
6
7
8
9
10
11
D:.
│ CMakeLists.txt
├─build
├─include
│ main.h
├─lib
└─src
add.cpp
div.cpp
multi.cpp
sub.cpp
CMakeLists.txt如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(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 STATIC ${SOURCE})

#使用
link_directories(${PROJECT_SOURCE_DIR}/lib) #静态库路径(若有系统环境变量则无需
link_libraries(math) # 链接静态库
file(GLOB MAIN ${CMAKE_CURRENT_SOURCE_DIR}/myCal.cpp)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(test ${MAIN})

完成后lib将出现.a格式静态库,且根目录出现编译的可执行程序;

cmake构建与链接动态库

只有一些差别:

  1. 编译add_library使用参数SHARED

  2. 链接使用target_link_libraries而不是link_libraries第一个参数目标库或者可执行程序,且放置在add_executable之后,因为其运行后才加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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(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_directoriestarget_link_libraries常常被放在一起讨论,因为这三个权限是类似的;

  • PUBLIC:表示PUBLIC后的库均以二进制链接到前面目标,并且导出该符号给第三方使用;例如target_link_libraries(A PUBLIC B PUBLIC C),B、C链接到A库,任何库再PUBLIC A,意味着新动态库都链接到A、B、C且可使用三者的接口

  • PRIVATEPRIVATE后的库会链接到目标,但是无法导出其符号给第三方使用;上述例子,D PRIVATE A,D能使用A的接口,但是无法使用B、C接口

  • INTERFACEINTERFACE后的库仅导出符号而不会直接链接到目标D INTERFACE A,D不会链接到A,但是A、B、C接口均会暴露到D,因此如果有对象PUBLIC D,就能使用这些接口,D本身仅传递信息,并没有使用这些接口

因此,这里提到了三种实践:

  • 如果源文件(例如CPP)中包含第三方头文件,但是头文件(例如hpp)中不包含该第三方文件头,采用PRIVATE。

  • 如果源文件和头文件中都包含该第三方文件头,采用PUBLIC。

  • 如果头文件中包含该第三方文件头,但是源文件(例如CPP)中不包含,采用 INTERFACE。

所谓头文件包含第三方头,实际上就是需要把接口向上层再暴露,因此需要PUBLICINTERFACE,如果该库没有在源文件使用,那就是INTERFACE;如果头文件都没有第三方头,那就可以考虑PRIVATE,因为上层无需这个接口。

字符操作

拼接

1
2
3
set(TEMP "tempdir")
set(DIR ${CMAKE_CURRENT_SOURCE_DIR} "/" ${TEMP} ${TEMP})
message(${DIR}) #D:/Documents/Desktop/cmake_test/tempdirtempdir

筛选

1
2
3
file(GLOB FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
list(REMOVE_ITEM FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp) #移除main.cpp
message(${FILE})

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),尾插XXX

  • list(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
22
D:.
│ 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
2
3
4
5
6
7
8
9
10
#src1
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(aslib)

include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
file(GLOB SOURCE ./*.cpp)
add_library(${ASLIB_NAME} STATIC ${SOURCE})
1
2
3
4
5
6
7
8
9
10
#src2
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(mddll)

include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${DLL_PATH})
file(GLOB SOURCE ./*.cpp)
add_library(${MDDLL_NAME} SHARED ${SOURCE})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#test1
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(test1)

include_directories(${HEAD_PATH})

link_directories(${LIB_PATH})
link_libraries(${ASLIB_NAME})

set(EXECUTABLE_OUTPUT_PATH ${BIN_PATH})
file(GLOB SOURCE ./test1.cpp)
add_executable(${TEST1} ${SOURCE})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#test2
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(test2)

##test2既用到动态库,也用到静态库
include_directories(${HEAD_PATH})

link_directories(${LIB_PATH})
link_libraries(${ASLIB_NAME})
link_directories(${DLL_PATH})

set(EXECUTABLE_OUTPUT_PATH ${BIN_PATH})
file(GLOB SOURCE ./test2.cpp)
add_executable(${TEST2} ${SOURCE})
target_link_libraries(${TEST2} ${MDDLL_NAME}) #链接动态库

在build目录运行

1
2
cmake .. -G "MinGW Makefiles"
cmake --build .
所有目标文件成功生成。