CMake快速上手:编译、构建与变量管理(包含示例)
CMake概述
首先来进行浅谈一下对于CMake的认识,很多人都说CMake是比makefile更加高级的构建工具,但是事实是这样吗?
那既然如此,为啥还要进行折腾一圈使用CMake生成器呢,直接使用makefile不就好了吗?
CMake其实在我看来仅仅就是生成器,生成构建系统必须的文件,比如makefile。
CMake的诞生其实就是想要进行解决makefile进行构建的项目的跨平台不方便的问题。makefile要想进行跨平台必须根据平台制定不同的脚本。
.c .cpp → toolchain (预处理>编译>汇编>连接)→ exe可执行文件
如果是单个或者少个源文件,我们直接使用手动命令 gcc/g++ -o main main.cpp 类似的命令进行编译即可,但是当在进行构建一个项目时(源文件非常多)此时进行手动编译会非常复杂,或者使用makefile。CMake根据不同的平台帮助我们生成对应的makefile.
CMake除了可以进行构建可执行程序,也可以进行构建库文件(静态库、动态库),这样就可以将库文件用于第三方程序(保密库文件源码,源文件太多不方便进行管理)。
Cmake的使用
无论是window还是Linux下都要进行安装CMake这个工具。
见见Cmake文件
首先进行展示一下手动进行编译
//math.h文件template<class T>
T add(T& a,T& b)
{return a+b;
}template<class T>
T subtract(T& a,T& b)
{return a-b;
}template<class T>
T multiply(T& a,T& b)
{return a*b;
}template<class T>
T divide(T& a,T& b)
{return a/b;
}
//main.cpp文件
#include<iostream>
#include"math.h"int main()
{int a=1;int b=2;std::cout<<add(a,b)<<std::endl;std::cout<<subtract(a,b)<<std::endl;std::cout<<multiply(a,b)<<std::endl;std::cout<<divide(a,b)<<std::endl;return 0;
}
文件的目录结构
手动进行编译的结果
使用cmake文件进行构建
构建camke文件时有基本的三部曲如下
- cmake_minimum_required:指定使用的 cmake 的最低版本
cmake_minimun_required(版本)
- project:定义工程名称,还可以制定工程的属性(工程版本、工程描述、web主页、支持的语言--默认情况下是支持所有的语言的)
# PROJECT 指令的语法是:project(工程名称,工程版本、工程描述、web主页地址、支持的语言)
如果不需要定义工程的属性,直接只进行定义工程名称即可。
- add_executable:添加一个可执行的程序
add_executable(可执行程序名 源文件名称)
注:这里的可执行程序名称和工程名称没有任何关系,并且源文件可以有多个,中间用空格进行间隔即可。
实操cmake进行编译可执行程序
1.编写CMakeLists.txt文件
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#可执行程序的生成
add_executable(main main.cpp)
2.在 main 函数同级目录下进行创建 build目录,进入目录并进行执行
cmake ../
其中cmake后面的路径是CMakeFists.txt 的路径。
执行完cmake命令后我们可以发现是生成了一个makefile文件的
3.最后直接在build的目录下直接进行make命令直接可以生成可执行文件。
上面我们已经完成了一个cmake文件的编写了,但是上面的那个cmake文件是最基本的,比较粗糙,当我们在进行构建大型项目时,源文件非常多此时上面的那种方式就比较吃力,下面的内容就开始进行打磨cmake文件。
注释
注释一行
这和python注释相似,直接通过 # 将该行后面的内容进行注释
注释一块
#[[ ]] 通过这种方式将[[ ]]中的内容进行注释
CMake中set的使用
add_executable(main main.cpp .......)
当进行构建项目的时候这里的源文件太多,会加大维护的成本,出错的概率比较高。
我们应该如何进行解决这种问题呢??
定义变量
其实cmake中是可以进行设置变量的,可以把所有的源文件进行放到变量中,语法如下:
set(变量名 变量值)
#变量值如果有多个的话中间用空格或者分号来进行间隔
括号中的默认类型是string类型的,如果我们要使用的是其他类型,需要我们先进行手动转化。
但是这里我们可能就会有疑问了,这样将所有的源文件进行设置成变量的话,本质上和直接将所有的源文件进行写到add_executable的参数的本质是一样的,只是增加了cmake代码的可读性,一定程度上改变出错的概率,但是呢当源文件非常多时不还是容易出错吗?是的,这里先卖个关子,在搜索文件的模块进行解释。
使用变量
set(SRC x.cpp xx.cpp xxx.cpp)
add_executable(main ${SRC}) #必须通过${变量名}这种方式进行取变量值
定义使用的C++标准
在进行编写C++程序的时候,我们有可能会使用现代C++(C++11、17、20等),这时候就需要进行向编译器指定C++标准。
这时候使用想要使用cmake进行指定C++标准的时候就有两种方式
- 在cmake文件中进行定义C++标准
set(CMAKE_CXX_STANDARD 11/14/17)
- 不在cmake文件中进行定义C++标准,而是在执行cmake指令的时候声明
cmake CMakeLists的路径 -DCMAKE_CXX_STANDERD=11/14/17
其实这个-D就是指定宏。
定义输出的路径
在cmake中有一个宏叫做EXECUTABLE_OUTPUT_PATH,这个宏用是默认生成可执行程序的路径,并且可以通过set进行对该宏进行设置。
set(变量名称 路径)
set(EXECUTABLE_OUTPUT_PATH ${变量名称})
在进行设置的时候,如果没有该路径,则会被创建。
实战
通过set的使用进行重新编译之前的示例
1.更新cmake文件
#cmake文件内容#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#set
set(SRC main.cpp)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.6code/exe/)#可执行程序的生成
add_executable(main ${SRC})
2.删除上一次的cmake构建结果
3.执行make命令观察结果
通过执行cmake命令我们发现进行创建了一个exe的文件目录,然后执行make命令后直接在exe文件路径下进行创建了一个main的可执行程序,运行可执行程序的结果和我们进行手动编译出来的可执行程序的结果是一摸一样的。
搜索文件
前面我们提及到,将所有的源文件进行设置成变量确实可以增加cmake文件的可读性,但是呢,在一个大型的项目中,源文件的数量是非常的多的,这时候要是通过手动进行添加难免是有些困难的,这时候我们就可以进行使用搜索文件的方式进行添加到变量中。
在camke中进行搜索文件的命令是有两种的,分别如下
- aux_source_directory (要搜索的目录的路径 想要进行存储到的变量)
- 这里搜索的路径可以手动设置;也可以通过宏进行设置:PROJECT_SOURCE_DIR(执行cmake时后面跟随的路径)宏必须通过取变量的方式进行使用、CMAKE_CURRENT_SOURCE_DIR这个宏是cmake文件所在的目录。
#cmake文件#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#set
#set(SRC main.cpp)
aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.6code/exe/)#可执行程序的生成
add_executable(main ${SRC})
这里如果是需要在多个源文件目录下进行搜索源文件,直接使用对应次数的aux_source_directory即可,那么如何进行合并呢?
还是通过set命令进行合并,举个例子:假如我们的项目的源文件分别在两个目录下,调用了两次aux_source_directory 进行分别将两个目录下的源文件放到了a b 这两个变量中,然后使用下面的set命令:set(a a b),将a b 这两个变量的内容进行合并放到a中即可。
- file(搜索的方式 变量名 搜索的文件路径和文件类型)
- 搜索的方式有两种:GLOB/GLOB_RECURSE,其中GLOB是将搜索到满足条件的所有文件名进行生成一个列表,并进行存储的到变量中;GLOB_RECURSE是进行递归搜索指定的目录,将搜索到满足条件的所有文件名进行生成一个列表,并进行存储的到变量中。
- 文件类型指的就是文件的后缀
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#set
#set(SRC main.cpp)
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.6code/exe/)#可执行程序的生成
add_executable(main ${SRC})
上面执行出现了两次的错误分别是
第一次:
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}*cpp)
漏掉的/ .
第二次:
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}*.cpp)
漏掉的/
指定头文件路径
我们上面的那个示例之所以能够进行编译成功是因为头文件和源文件在同一目录下,在进行编译的过程中默认就是在源文件同一目录下进行查找头文件,但是真实的项目结构是头文件和源文件分离的,最简单的结构如下:
这时候我们在进行使用cmake进行构建项目,并通过make进行编译。
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#set
#set(SRC main.cpp)
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) #这里进行增加了src的路径
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.6code/exe/)#可执行程序的生成
add_executable(main ${SRC})
执行cmake进行构建项目是成功的,但是通过make进行编译时我们发现报错没有找到math.h库中函数方法,这里是我设计的 .h 库的名称不好,math.h C++标准库中也有,将自定义的math.h库进行命令成Math.h重新展示
这样我们就发现在进行make编译的时候找不到Math库,至于原因我们上面已经介绍过了。
那么如何进行解决呢?
两种方式进行解决
- 在main.cpp中使用绝对路径的方式或者相对路径的方式
- 这种方式,如果头文件非常多的话是很麻烦的,自己手动写每一个头文件的路径也容易出错
- 在cmake文件中进行指定头文件的路径,以便cmake在进行构建项目的可执行程序时能够顺利找到对应的头文件。
- 命令 :include_directories(头文件路径) 注意:这个路径建议写成相对路径,防止绝对路径在不同的电脑环境下出现编译找不到头文件。
#声明cmake的最低版本号
cmake_minimum_required(VERSION 3.15) #这行的版本是可以写也可以不写的,如果不写可能会有警告#命名项目
project(test)#set
#set(SRC main.cpp)
#aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
set(CMAKE_CXX_STANDARD 11)
set(EXECUTABLE_OUTPUT_PATH /home/ys/code/9.6code/exe/)#声明头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include) #增加了这行
#可执行程序的生成
add_executable(main ${SRC})
通过声明头文件直接成功进行编译了。