这里写目录标题
- 概述
- 简单使用
- 其他使用指令
- 嵌套的CMake
- 静态库中链接(使用)静态库
概述
CMake的作用
基本背景
首先,我们知道,一个源文件生成可执行文件的步骤为:
1、预处理,将源文件中的宏、头文件等展开,将注释删去
2、进行编译,将源文件进行编译。
3、进行汇编,汇编完成之后,就会根据源文件生成二进制文件,win平台下以".obj"为后缀,而linux平台下以".o"为后缀
4、链接,将.obj或者.o文件进行链接,最终生成可执行文件,win平台下为.exe,linux平台下为.out文件
问题产生
但是,如果我们有一两个源文件,那么使用gcc或者g++的基础命令,是可以完成这一系列操作的,而如果我们整个项目的源文件都要进行编译,我们每次编译都去使用gcc/g++来写编译命令,一是效率低下,二是不易维护
所以,我们产生了make/makefile,
但是make/makefile是要根据不同的平台去编写不同的makefile版本,以适应平台的需求,同样有一些高复用的操作
所以,CMake就产生了。
CMake的作用
他根据CMakeLists.txt脚本,使用cmake命令,就可以根据当前的平台,生成对应的makefile文件,我们拿到makefile后直接进行make即可
他可以适应不同的平台,且可以免去你自己手写makefile的痛苦
总结
CMake就是一个项目构建的工具,项目编译构建的工具
他根据CMakeLists.txt脚本,使用cmake命令,就可以根据当前的平台,生成对应平台的makefile文件,我们拿到makefile后直接进行make即可
他可以适应不同的平台,且可以免去你自己手写makefile的痛苦
补充
通过CMake、makefile,我们不仅可以生产可执行文件,还可以生成库文件,即动态库或者静态库文件,从而可以供其他第三方项目当做API来调用,这就是为什么说,C++可以用来造轮子的原因
而之所以给第三方项目使用生成的库文件而不使用源代码,原因如下:
1、为了保密性
2、为了方便维护和引用
简单使用
注释
单行注释:#
多行注释:#[ [ ] ]
指定版本、项目描述
第一行可以进行CMake最底版本的指定,一般就知道为当前主机的CMake版本即可,可以避免一些标准的混乱
之后是project命令。用于指定项目的名字
他还有一个重载版本,即多个属性参数,用于指定项目的名字、版本(如v1.0、v2.0)、项目描述、项目地址(如果有的话)、项目语言(如果指定了语言,则会按照指定的语言进行编写,如果不指定语言,则支持所有语言)
实际是 project命令就是写一段对项目的描述罢了
可执行文件设置、源文件设置
该命令用来定义项目会生成的可执行文件的名字,以及所依赖的源文件
如上图所示,先定义最终生成的可执行文件的名字,之后后面写所有的源文件
执行
将上面三步编写好之后,就可以执行cmake命令来生成makefile脚本文件了,cmake后面跟上CMakeLists.txt文件所在的目录路径即可
cmake使用
当CMakeLists.txt写完之后,使用cmake执行CMakeLists.txt,之后会产生一个自动化生成的makefile,之后再"make",即可生成对应的可执行文件。
补充:有个小细节:当我们在当前目录使用cmake,那么产生的一些构建文件都会到当前目录,而我们不需要关注所有的这些生成的文件,所以,这里大多数文件我们是不需要看到的,我们只需要关注makefile即可,所以,单纯在当前目录进行cmake,是不合适的
我们可以在cmake之前,在当前目录创建一个文件夹(例如命名为 build),用于存放这些生成的文件,而我们在cmake的时候,不要在当前目录进行camke,而是要进入到build文件夹,在build文件夹内进行cmake,但是由于CMakeLists.txt文件在上一级,所以,要加上路径,即cmake ...,这样所有产生的文件都在build文件夹内了,我们在该文件夹内make,生成可执行文件,这时我们返回到上一级目录,还是那些源文件,会很清爽
其他使用指令
set指令(用于定义变量、以及指定C++标准)
定义变量
上面我们在指定源文件时,因为将源文件依次指定了出来,而如果一个项目中的源文件很多,那么我们每次需要指定源文件时,都要书写一遍,或者他会很长,影响代码阅读,所以,我们可以使用set来设置变量,而CMake中的变量统一都是字符串。
使用方法:set(变量名 源文件...)
而之后的命令中,如果想要使用这些,就要使用${变量名}来取出变量的内容
指定c++标准
指定C++标准时有两种方式,一个是在CMakeLists.txt文件中,进行C++标准的指定,一个是在执行cmake命令时,进行C++标准的指定
1、在脚本文件中进行C++标准的设置,使用set命令。第一个参数是一个固定宏,CMAKE_CXX_STANDARD,表示C++的标准宏,第二个参数就是指定标准即可
2、在执行cmake命令时,设置C++的标准,在cmake CMakeLists.txt文件路径之后 加上-D选项,表示要进行宏的设置,而这个宏,就是C++标准宏,CMAKE_CXX_STANDARD=11即可
指定可执行文件的输出路径
我们可以使用set命令来设置可执行文件最后的产出位置,第一个参数还是一个宏,叫做EXECUTABLE_OUTPUT_PATH ,第二个是路径,这个路径可以是绝对路径或者相对路径,如果是相对路径的话,是相对于makefile的位置为起点的,而如果所设置的路径不存在,cmake会帮助我们创建出对应的目录。
注意,路径的设置可以使用${变量名}来拼接
使用
小tips1:
当我们在build目录下使用cmake ...的时候,如果CMakeLists.txt所在的目录下,已经存在了那些生成的文件,那么cmake不会进行执行,所以,要确保CMakeLists.txt目录内没有cmake会生成的文件,这样cmake才可以执行
小tips2:
由于我们指定了可执行文件的输出路径,所以,他会自动帮我们创建,且将可执行程序放到对应的路径,我们可以通过路径来运行他
搜索文件
方式1(aux_source_directory)
第一个参数,输入源文件所在的目录,或者说是要搜素的目录,第二个参数,是设置要将搜索到的源文件存入的变量的名字
该方法搜索时,默认搜索.c/.cpp文件,但是其不支持递归搜索
对于该方式,如果我们在一个目录里有多个文件夹都存放着源代码,该如何使用呢
我们可以使用两次aux_source_directory,分别设置其变量名为a 、b,之后再使用set,设置a、b为一个变量a,就相当于把原先a、b拼接在一起了
方式2(file)
第一个参数是设置搜索方式,GLDB是非递归搜索,GLDB_RECURSE是递归搜索,
第二个参数是设置要定义的变量名
第三个参数是要搜索的文件路径以及要搜索的文件的后缀,一般设置为*.cpp或者 *.c 或者 *.h
使用
tips:两个路径宏:
1、PROJECT_SOURCE_DIR,该宏存储的是CMakeLists.txt文件所在的路径
2、CMAKE_CURRENT_SOURCE_DIR,该宏存储的也是CMakeLists.txt文件所在的路径
3、注意宏取值之后,后面还可以继续用"/"来拼接路径,而如果使用file来查找文件,就要精确到源文件所在的目录之后,拼接上通配符+后缀
包含头文件
指定所有源文件的头文件目录
使用场景
在一个项目中 我们不可能将所有的源文件和头文件放在一起,更多的是将其分文件放置,这样的话,我们在搜索源文件时,就要指定到其所在的目录,具体为在宏后面再加一级目录
而如果这样的话,由于源码中,包含头文件时路径是按照之前的目录结构设置的,所以就会报错"找不到头文件"
此时有两种解决方案:
1、将源码中的头文件修正
2、使用CMake命令:include_directories(头文件所在目录路径,精确到目录即可)
一般情况下,我们都是确定了目录结构再去进行编译的,所以一般情况下,遇到问题都是采用方案1解决,但是如果特殊情况,我们有巨多的源文件,都需要更改,那么可以使用指定头文件命令
制作库文件
前期准备
我们需要将源文件所在文件的main.cpp源文件直接删掉,因为如果要制作动态库,则不再需要main来运行了
动态库和静态库的库名规范
动态库:
lib自定义名称.so(linux)
lib自定义名称.dill(Windows)
静态库:
lib自定义名称.a(linux)
lib自定义名称.lib(Windows)
制作静态库
使用命令add_library命令
第一个参数是我们上面那个命名规范中的中间部分--"自定义名称",第二个参数指定要制作静态库还是动态库,第三个参数是制作这些的源文件
制作动态库
将第6行设置可执行文件的输出路径可以注释掉
第8行的c++11标准最好带着
将第9行的"生成可执行程序"这个命令改为生成库文件,第一个参数指定库名,第二个参数指定为SHARED,表示生成动态库,第三个参数将源文件的变量取出来
可以看到,当cmake-make之后,生成了一个动态库文件,而动态库是有可执行权限的,所以,他是绿色的,静态库是没有可执行权限的
而当我们想要制作静态库时,只需要将add_library第二个参数改为STATIC,即可,然后cmake-make,会生成静态库文件,可以看到,他是黑色的,说明是普通文件,不具备可执行权限
发布
我们在生成了库文件之后,想要让别人使用,需要发布两部分文件:
一部分就是我们所生成的库文件,另一部分就是其所对应的头文件
原理:因为库文件也是源码,他是源码的二进制形式,他可以直接被计算机所识别使用,但是他的本质没有变,还是源码文件,里面有一些函数还是需要声明,所以,同样需要头文件才能正常运行
同时,现在我们的源代码已经从多个文本源文件被打包成了一个二进制源文件,而头文件也可以更好的说明当前这个二进制文件有哪些接口,方便第三方查看和使用
指定库文件的生成路径
我们可以对库文件的生成路径进行指定,使其生成在对应的路径下,同样是使用set方法,第一个参数是宏:LIBRARY_OUTPUT_PATH,第二个参数是你想要设置的路径
在程序中链接库文件
前期准备
首先,src目录可以删去了,他是之前存放源文件的目录,现在源文件都被打包成了一个库文件,所以,src就不再需要了
之后,我们在当前目录下创建两个文件夹,分别用于存放动态库和静态库
将之前生成的动态库和静态库都拷贝过来,同时准备一个main.cpp,用于当做第三方程序,测试使用库文件
在程序中链接静态库
介绍
想要在第三方程序中使用静态库,就要在CMakeLists.txt文件中,使用link_libraries命令,用于链接静态库,参数是静态库的名字,当然如果有多个静态库,可以在后面再添加静态库的名字即可
**注意:**如果所要链接的静态库不是系统的,而是自己制作或者第三方提供的,那么还要进行静态库存放路径的指定:
使用命令link_directories(库路径),这个命令也可以用于进行动态库路径的指定
使用
首先,将最后一行的"生成库文件"改为"生成可执行程序"
之后,在设置源文件的变量时,仍然是当前目录的*.cpp文件,不过这时指的就是仅仅一个main.cpp了,因为现在就这一个源文件,而如果源文件想要使用库文件,那么,需要使用第9行的命令去链接静态库,同时因为静态库不是系统自带的,所以,要指定静态库的路径,使用link_directories命令(精确到所在目录即可)
补充:动态库和静态库的加载区别
1、对于是否被打包进可执行文件:
静态库会与源码一起被打包进可执行文件(库文件和源文件只是被复制打包,而不是被拿走)
而动态库则不会被打包进可执行文件
2、何时加载到内存:
静态库由于被打包进了可执行程序,所以当程序一启动,静态库就被加载到内存了
而动态库只有在程序使用到动态库相关的函数时,才会被加载进内存
在程序中链接动态库
介绍
其命令就是在"链接静态库命令"的前面加了一个target,
第一个参数:指定动态库所要链接到的文件,(可以是源文件、动态库文件、可执行文件)
第二个参数:链接权限(默认是public)
第三个参数:动态库名字(去除lib以及后缀)
关于第二个参数:链接权限:(如果是动态库与动态库之间进行链接,会用到如下内容)
1、public:如果是public权限,那么可以实现动态库的传递,即,假如A public链接了B C,而之后D 链接 A,那么此时D可以调用B 和 C 库的库函数。
而如果A public链接了B 但是private链接了C,而之后D 链接 A,那么此时D可以调用B库的库函数,而不能调用C 库的库函数。
2、private:如果是private权限,那么无法进行动态库的传递,只能连接到直接指定的库,这也就是为什么上面第二个例子中,D无法调用C库,因为C库是private权限链接的,只有A能去调用C库
3、interface:如果是interface权限,那么如果A interface链接了B C,那么A可以调用B库和C库的函数(或者称为接口),但是并不知道调用的是B和C库,同样,interface不能被传递
细节:链接命令所在的位置
动态库的链接命令应该放到CMakeLists.txt文件的最后一行,即生成了可执行程序或者源文件或者动态库动态库这些命令之后。
主要还是由于动态库是在程序运行期间进行内存加载的原因
补充:动态库的特点
动态库是在程序运行起来之后,如果调用到了动态库的函数,此时对应的动态库才会去加载,因为动态库在整个系统中只有一份,只要是链接了他的可执行文件,执行起来成为一个进程之后,都可以调用该动态库,所以动态库在整个系统只有一份,可以被多个进程调用,所以动态库也被称为共享库
使用
与静态库链接一样的是:他也需要指定出所链接的库的路径,且如果有多个动态库,那么需要依次指定出其路径
与静态库链接不同的是:他链接库的命令要放到最后一行
日志
内容
实际上,本质上就是在cmake构建的时候,进行信息的打印,
用处1:可以打印任意一个字符串来查看cmake构建时,是否执行了message之前的命令
用处2:我们在CMakeLists.txt中有时会用到搜索文件然后放入一个变量中,我们可以使用message来打印变量的值,查看是否是我们预想的那样
使用
假如14行的代码没有被注释掉,那么这里就被标记为一个严重错误,执行到此处,打印出222xxxxxxxxxxxxxx之后,后面的命令就不会执行了,cmake就中断了
而其他的,例如9行的不加任何参数,表示是重要信息,会打印字符串
而12行的,加了STATUS,表示是非重要信息,打印时,会在字符串之前加上"-- ",表示不重要
如果想要打印变量的值,可见上面"内容"一栏中的例子
变量操作(字符串操作)
拼接(追加)
使用set
内容
可以使用set,用旧的变量内存储的字符串,对新的一个变量进行赋值,如上图,他会进行覆盖操作,也就是将变量名1 变量名2的内容拼接好,然后覆盖变量名,这样的话,很难实现追加操作
而想要实现追加,则可以使用上图紫色标记的方法,将所有的字符串都进行拼接,之后覆盖到第一个字符串,也就实现了追加
注意对变量名进行${}取值操作,不然会将变量名当做字符串来处理
应用
注意点:
1、可以看到,set拼接字符串时,字符串无需加双引号,上面初次介绍set的时候也是这样的,(注意与message的区分)
(实际上用不用双引号都行,因为CMakeLists默认是字符串,使用双引号的场景可能是"需要将一个包含空格的字符串视为一个字符串",如 "hello world")
2、不管是使用set来用字符串定义变量,还是拼接字符串,使用message打印时,他都没有空格分隔,但是其底层是有分隔符在的,只是message看不到
使用list
内容
使用list,同样可以做到set的效果,只需要使用list命令
第一个参数写 APPEND,之后的参数跟set不一样了(上图的文档写错了)。他会在第一个变量的后面进行其他后续变量的追加,会保留第一个变量原先的值,并在其基础上追加后续的变量的内容
注意点:他在拼接完字符串后,在其底层,会将每个字符串用分号分隔,但是如果我们直接打印拼接完之后的结果,是看不到分好的,会看到他们连在一起,但是实际上,他们并不连在一起,只是视觉上连在一起
应用
1、可以看到这里追加的时候,使用了双引号,所以,实际上不管是set 还是list 用不用双引号都可以,因为默认就是字符串,使用双引号的场景可能是"需要将一个包含空格的字符串视为一个字符串",如 "hello world"
2、使用message打印时,他都没有空格分隔,但是其底层是有分隔符在的,只是message看不到
移除
使用list
内容
有时当我们file搜索到所有的.cpp文件之后,其中有一些文件是我们不想要的,那么就可以用list将其删除掉
第一个参数是REMOVE_ITEM 第二个参数是待处理的变量 后面的参数都是想要删除的内容
应用
首先,我们直接删除main.cpp,打印出删除前后的SRC,我们会发现,删除失败了,并没有真正的删除掉
这是因为,逻辑上我们想要删除main.cpp,但是实际上,之前的SRC变量里,每个子字符串都是一个路径,而不是文件名,所以,不要忘记加上前面的路径
当我们加上了路径之后,就可以进行匹配了
list的其他操作
获取list长度、读取指定索引(一个或多个)
获取list长度:第一个参数是LENGTH,第二个参数是要操作的列表,第三个参数是一个传出参数,用于接收列表长度,类型仍然是字符串
读取指定索引:第一个参数设置为GET,第二个参数是要操作的列表,之后的参数是要索引的下标(从0开始),最后一个参数是传出参数,用于接收指定的索引所组成的列表
用连接符组成字符串
查找、追加
插入、删除
去重、列表的翻转
列表排序
其他更多操作,可见官方文档
https://cmake.org/cmake/help/latest/index.html
宏定义
背景
当我们要进行一个项目的测试时,会进行很多日志的输出,那么当最后项目要上线了,这些输出就要被注释掉,但是如果是一个很大的项目,这些都需要手动的注释,工作量很大,所以,我们可以使用自定义宏来控制某些语句是否生效,从而相当于日志的开关
如上图,在第8行的前后,加上了宏控制:
#ifdef DEBUG
...
#endif
表示,如果定义了DEBUG宏,那么在其之间的代码才会被执行,否则,在#ifdef和#endif之间的代码是无效的
而有了这段代码,我们在gcc/g++编译的时候,就可以在命令行灵活的决定是否定义DEBUG这个宏,也就灵活的决定是否进行相关代码的生效(使用-D+宏的名字)
CMake的宏定义
内容
可以看到,add_definitions,他末尾带了一个s,说明可以进行多个宏的定义,如果要定义多个宏,直接在后面再追加"-D宏名称",之间用空格分隔
例如:add_definitions(-D宏名称 -D宏名称 -D宏名称)
应用
嵌套的CMake
使用场景
对于一个大型项目来说,肯定不能仅仅通过一个CMakeLists.txt文件就把所有的源文件都协调好,所以,我们可以采用多个CMakeLists.txt文件来进行分块维护,所以,就有了嵌套的CMake
**注意:**一般只含头文件的文件夹内无需写CMakeLists.txt,因为他只需要被包含即可
**注意2:**不同级别的CMakeLists之间需要进行继承关系的建立,且父CMakeLists的变量可以被子CMakeLists所识别并使用,反之则不行
内容
在父目录内的CMakeLists中,使用该命令,指定当前CMakeLists的子CMakeLists是谁,第一个参数就是子CMakeLists所在目录的路径
后两个参数不用写,一般用不到
应用
需求分析
他们有的是可执行程序,有的则负责给可执行程序提供API,所以可以直接生成库文件,
对于库文件的生成:我们有两种选择,一个是生成静态库,一个是生成动态库,
如何选择:1、如果库的源文件多,那么就生成动态库,如果不多,就生成静态库
2、如果甲方要求可执行程序尽可能的小,那么就动态库,如果甲方要求使用简单,那么就静态库
3、注意,如果是生成了动态库,那么这些库无法被打包进可执行程序,所以,提供给甲方可执行程序的同时还要提供给甲方动态库文件
4、可能在发布时,还要带上库所对应的头文件
制作根结点CMakeLists.txt(以静态库为例)
首先,前期准备,就是定义出其子CMakeLists需要的各种路径,都定义在变量里,同时还可以定义出所生成的库/可执行文件的名字,也用变量存储,而子CMakeLists中均使用继承来的变量,这样一来,方便在父CMakeLists.txt中对整个项目进行维护,比如想要修改可执行程序的名字,直接在父CMakeLists中修改即可
制作clac目录下的CMakeLists(生成静态库)
更正:第7行,变量名字母拼错了,更正为LIBPATH
制作sort目录下CMakeLists(生成静态库)
更正:第7行,变量名字母拼错了,更正为LIBPATH
制作test1可执行程序的CMakeLists(链接第一个静态库)
这里进行源文件的编译以及静态库的链接,可以说明,同级别的CMakeLists是按照"先生成依赖再生成程序"顺序执行的(即自动的,先生成库文件,...然后生成了库文件之后,再用于链接可执行程序)
制作test2可执行程序的CMakeLists(链接第二个静态库)
与test1相比修改了,链接的静态库的库名(NO.7),修改了生成的可执行程序的名字(NO.9)
结构
静态库中链接(使用)静态库
场景
我们想在sort的某个算法API制作的过程中使用计算库(即我们自己制作的calc库),那么,打开其源码,进行编辑,
1、包含库的头文件
2、进行相关API的调用
CMakeLists的修改
在sort文件夹内的CMakeLists需要修改,因为他使用了自制的calc库,所以,需要加上上面第8、9行,进行静态库的指定
而test中的CMakeLists不用修改,因为这里是测试文件,即进行相关库的调用,而之前已经链接了sort库,现在我们在sort库中链接了calc库,但是此时这里不用再进行calc库的指定了,因为静态库的链接具有继承关系,即calc库已经被打包进sort库中了,所以,无需修改
如果想链接多个静态库,可以在后面添加参数,进行多个静态库的链接
总结
该过程具体演示了第三方源码如何使用静态库:
1、首先明确,库文件的使用,肯定是API的调用,而调用API的操作,只会在源码中进行,这点不要忘了
2、在源码中,需要包含静态库的头文件,之后进行API的调用
3、在使用了静态库的源码的CMakeLists中,无需指定静态库的头文件路径,只需要指定库文件即可
4、CMakeLists中的指定头文件路径的命令,仅仅是针对生成"可执行程序/库文件"的源码来讲的,但是因为使用静态库时,源码包含了静态库头文件,所以,静态库头文件也是该命令的管辖范围,所以,静态库头文件最好与源码之前的头文件放在同一目录,或者,就在源码中写明静态库头文件的相对位置