整理自 视频 【CMake 保姆级教程【C/C++】】 https://www.bilibili.com/video/BV14s4y1g7Zj/?p=5&share_source=copy_web&vd_source=6eb8f46d194c5ef9f89d3331f623a9c3
1、cmake简介
源文件(.cpp / .c)要经过 工具链
1.1 工具链
1、预处理:把头文件展开,宏替换,把注释去掉
得到的还是源文件
2、编译器进行编译(gcc / g++),编译完得到 汇编文件
3、将汇编文件 通过 汇编器进行处理,就得到 二进制文件(.obj (win) / .o (linux))
4、链接器 对二进制文件进行 链接,打包之后 生成可执行文件(也是二进制的)
5、可以把CMake看成一款自动生成 Makefile的工具,其编译流程如下图:
1.2 生成可执行文件
文件少 可以通过命令 生成可执行文件;项目多:
1)makefile,创建一个脚本文件,脚本文件名字 就是makefile,在makefile里面执行 一系列的指令,告诉编译器怎么编译 源文件
脚本文件写好后,执行 一系列的批处理命令,批处理命令 就叫做make,makefile里面的若干个指令 执行了 make之后就可以把 makefile里面的指令 全部执行完毕,可执行程序 就被构建出来了
2)cmake,不依赖于 平台,根据不同的平台 生成对应的 makefile脚本文件,脚本文件名字 cmakelists.txt,在这个文件里面 执行一系列的指令,有了这些指令之后 再执行命令 cmake,生成 makefile文件,执行了 make之后就可以把 makefile里面的指令 全部执行完毕,可执行程序 就被构建出来了
除了 生成可执行文件 还可以生成库文件,库文件有两种:动态库 和 静态库。库文件 引入 第三方项目使用(对第三方保密,使用更直观(变成库第三方直接调用))
主要聚焦 cmakelists.txt 命令怎么写,大型项目自动化管理
2、编写简单的 cmakelists.txt
2.1 安装cmake
查看是否安装cmake
cmake --version
安装cmake
sudo apt install cmake
或者
官网下载 安装.tar.gz cmake v3.17
安装版本:cmake-3.17.0-rc2-Linux-x86_64(新版本无法 这样安装)
解压并进入目录下bin文件(tar -zxvf),加个软链
tar -zxvf cmake-3.17.0-rc2-Linux-x86_64.tar.gz
cd cmake-3.17.0-rc2-Linux-x86_64/bin
sudo ln -s /home/名字/cmake-3.17.0-rc2-Linux-x86_64/bin/cmake /usr/bin/cmake
检验一下
./cmake -version
不用虚拟机 也可以安装wsl解决
2.2 源文件和头文件
头文件声明,源文件具体实现,加减乘除,在main中对加减乘除函数 实现了调用
add.c
#include <stdio.h>
#include "head.h"
int add(int a, int b)
{
return a+b;
}
sub.c
#include <stdio.h>
#include "head.h"
int subtract(int a, int b)
{
return a-b;
}
mult.c
#include <stdio.h>
#include "head.h"
int multiply(int a, int b)
{
return a*b;
}
div.c
#include <stdio.h>
#include "head.h"
double divide(int a, int b)
{
return (double)a/b;
}
head.h
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif
main.c
#include <stdio.h>
#include "head.h"
int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
源文件比较少,可以直接 使用命令进行编译
转到代码所在目录下
g++ *.cpp -o app
./app 运行
2.3 初步使用 CMake
1、注释:
Make 使用 # 进行行注释
CMake 使用 #[[ ]] 形式进行块注释
#[[ 这是一个 CMakeLists.txt 文件。
这是一个 CMakeLists.txt 文件
这是一个 CMakeLists.txt 文件]]
cmake_minimum_required(VERSION 3.0.0)
2、创建文件 CMakeLists.txt 区分大小写
3、在创建的文件里面写命令:
cmake_minimum_required
:指定 使用的 cmake 的最低版本(可选,非必须,如果不加可能会有警告)
# PROJECT 指令的语法是:
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
与上面的字段 一一对应:定义工程名称,并可指定工程的版本、工程描述(字符串)、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可
add_executable
:定义工程会生成一个可执行程序
add_executable(可执行程序名 源文件名称)
# 样式1
add_executable(app add.cpp div.cpp main.cpp mult.cpp sub.cpp)
# 样式2
add_executable(app add.cpp; div.cpp; main.cpp; mult.cpp; sub.cpp)
这里的 可执行程序名 和 project中的项目名 没有任何关系
源文件名 可以是一个 也可以是多个,如 有多个可用空格 或; 间隔
本次的文件:
cmake_minimum_required(VERSION 3.15) # 指定版本
project(test) # 指定工程名字
add_executable(app add.cpp div.cpp main.cpp mult.cpp sub.cpp) # 指定可执行程序名字以及源文件
在当前目录里面 执行cmake命令,cmake后面跟上 CMakeLists.txt 所在的路径 cmake .
然后 会看到一系列的 日志输出
生成了一些新的文件 以及 目录, app 工程文件可以执行
多了一些暂时不关心的文件,可能会减慢搜索速度,可以把这些文件 放到对应的编译目录
删除文件,进入创建的编译目录,再执行cmake命令(路径是CMakeLists.txt对应的路径) cmake ..
只要 MakeFile 文件生成就大功告成了
还需要执行 make命令,在build文件夹里面 生成了 app工程文件
2.4 set
1、定义变量
通过set设置变量值的时候 都是字符串类型,变量名同一个文件中不能有重复
对变量进行取值:${ 变量名 }
# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
# VAR:变量名 VALUE:变量值
# 方式1: 各个源文件之间使用空格间隔
# set(SRC_LIST add.c div.c main.c mult.c sub.c)
# 方式2: 各个源文件之间使用分号 ; 间隔
set(SRC_LIST add.c;div.c;main.c;mult.c;sub.c)
add_executable(app ${SRC_LIST})
2、指定使用的C++标准
1)在编译的时候在编译命令中制定出要使用哪个标准
$ g++ *.cpp -std=c++11 -o app
2)C++标准对应有一宏叫做CMAKE_CXX_STANDARD。在CMake中想要指定C++标准有两种方式:
CMakeLists.txt 中通过 set 命令指定
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
在执行 cmake 命令的时候指定出这个宏的值
#增加-std=c++11,-D就是制定一个宏
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
3、指定输出的路径
在CMake中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH,它的值还是通过set命令进行设置
set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
指定输出路径时 要使用 绝对路径, 因为 如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录
除了给 可执行程序 指定路径,还可以 给动态库 指定路径
如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
4、例子
CMakeLists.txt
cmake_minimum_required(VERSION 3.15) # 指定版本
project(test) # 指定工程名字
set(SRC add.cpp div.cpp main.cpp mult.cpp sub.cpp)
set(EXECUTABLE_OUTPUT_PATH /home/ashergu/vscode_workpace/cmake_v1/out)
set(CMAKE_CXX_STANDARD 11)
add_executable(app ${SRC}) # 指定可执行程序名字以及源文件
运行结果
cmake命令是在build目录中执行的,但是CMakeLists.txt文件是build目录的上一级目录中,所以cmake 命令后指定的路径为…,即当前目录的上一级目录
当命令执行完毕之后,在build目录中会生成一个makefile文件
这样就可以在build目录中执行make命令编译项目,生成的相关文件自然也就被存储到build目录中了。这样通过cmake和make生成的所有文件就全部和项目源文件隔离开了
使用cmake命令时要保证上级目录里面不能有 之前执行cmake命令生成的文件,不然就不会生成新的文件了
给对应的宏设置值,宏不一样,对应的功能也就不一样
3、搜索文件
1、如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,在CMake中为 提供了搜索相应目录文件的命令,可以使用aux_source_directory命令或者file命令
2、这个变量是不需要提前定义的,使用时 只要把名字指定到 参数位置就行了,命令执行完之后,这个名字对应的变量 也就初始化完成了
3.1 方式一:aux_source_directory
1、可以查找某个路径下的所有源文件(包括.c / .cpp)
aux_source_directory(< dir > < variable >)
dir:要搜索的目录(可以指定字符串(不需要加" ")或者 可以通过宏来进行指定)
宏有两个(宏存储的值相同):
1)PROJECT_SOURCE_DIR:对应cmake命令 后面跟随的路径(就是CMakeLists.txt文件所在的路径),有两个文件路径,可以写两个PROJECT_SOURCE_DIR,然后通过set进行合并 set(a, a, b)
把a,b全部放进a中
2)CMAKE_CURRENT_SOURCE_DIR:宏表示当前访问的 CMakeLists.txt 文件所在的路径
variable:将从dir目录下搜索到的源文件列表存储到该变量中
CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 搜索 src 目录下的源文件
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
add_executable(app ${SRC_LIST})
cmake生成了MakeFile文件,只要make就行了
CMakeLists.txt
cmake_minimum_required(VERSION 3.15) # 指定版本
project(test) # 指定工程名字
# set(SRC add.cpp div.cpp main.cpp mult.cpp sub.cpp)
aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
set(EXECUTABLE_OUTPUT_PATH /home/ashergu/vscode_workpace/cmake_v1/out)
set(CMAKE_CXX_STANDARD 11)
add_executable(app ${SRC}) # 指定可执行程序名字以及源文件
运行结果
3.2 方式二:file
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
搜索当前目录的src目录下所有的源文件,并存储到变量中
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
关于要搜索的文件路径和类型可加双引号,也可不加:
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
CMakeLists.txt
cmake_minimum_required(VERSION 3.15) # 指定版本
project(test) # 指定工程名字
# set(SRC add.cpp div.cpp main.cpp mult.cpp sub.cpp)
# aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR})
set(EXECUTABLE_OUTPUT_PATH /home/ashergu/vscode_workpace/cmake_v1/out)
set(CMAKE_CXX_STANDARD 11)
add_executable(app ${SRC}) # 指定可执行程序名字以及源文件
运行结果
4、包含头文件
1、头文件和源文件不在一个目录内:把源文件放在src目录中,头文件放在include目录中
基于脚本文件(CMakeLists.txt)来编译src里面的源文件 最终生成可执行程序
更改目录并构建
将源文件对应的头文件路径指定出来
#include "head.h" // 相当于在./head.h
include_directories(headpath) // 参数是头文件对应的目录,建议使用绝对路径
5、制作动态库或静态库
有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些库文件(包括 静态库 或 动态库)提供给第三方使用
先复制一份
制作库就不需要main.cpp了,主要是用来测试的,把main.cpp移动出来
5.1 制作静态库
add_library(库名称 STATIC 源文件1 [源文件2] ...)
在Linux中,静态库名字分为三部分:lib+库名字+.a(windows中是.lib),此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充
工程名字需要跟之前不同,cmake和make之后
CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
add_library(calc STATIC ${SRC_LIST})
最终就会生成对应的静态库文件libcalc.a,没有可执行权限
5.2 制作动态库
add_library(库名称 SHARED 源文件1 [源文件2] ...)
就后缀名称变了(linux中为.so,Windows中为.dll),动态库有另一个名字 共享库,所以参数改为 SHARED(唯一改动)
cmake_minimum_required(VERSION 3.15) # 指定版本
project(test2) # 指定工程名字
# set(SRC add.cpp div.cpp main.cpp mult.cpp sub.cpp)
# aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
set(EXECUTABLE_OUTPUT_PATH /home/ashergu/vscode_workpace/cmake_v1/out)
set(CMAKE_CXX_STANDARD 11)
include_directories(${PROJECT_SOURCE_DIR}/include)
# add_executable(app ${SRC}) # 指定可执行程序名字以及源文件
add_library(calc SHARED ${SRC})
动态库有可执行权限,所以是绿色
发布给使用者时,除了库文件之外,还需要对应的头文件
不论静态库还是动态库 都是源代码,只不过是二进制的(.cpp源代码是文本格式的)
5.3 指定输出的路径
1、适用于动态库:
由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
对于这种方式来说,其实就是通过set命令给EXECUTABLE_OUTPUT_PATH宏设置了一个路径,这个路径就是可执行文件生成的路径
2、都适用:使用LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件都适用
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
6、Linux 静态库和动态库
整理自 https://subingwen.cn/linux/library/
1、不管是Linux还是Windows中的库文件 其本质和工作模式都是相同的, 只不过在不同的平台上库对应的文件格式和文件后缀不同。程序中调用的库有两种 静态库和动态库,不管是哪种库文件本质是还是源文件
2、使用库一般有两个目的,一个是为了使程序更加简洁不需要在项目中维护太多的源文件,另一方面是为了源代码保密
3、拿到了库文件(动态库、静态库)之后 要想使用还必须有这些库中提供的API函数的声明,也就是头文件,把这些都添加到项目中
6.1 生成静态链接库
在Linux中静态库由程序 ar 生成,以lib作为前缀, 以.a作为后缀, 中间是库的名字自己指定
1、生成静态库,需要先对源文件进行汇编操作 (使用参数 -c) 得到二进制格式的目标文件 (.o 格式), 然后在通过 ar工具将目标文件打包就可以得到静态库文件了 (libxxx.a)
参数c:创建一个库,不管库是否存在,都将创建。
参数s:创建目标文件索引,这在创建较大的库时能加快时间。
参数r:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块
2、具体步骤:
1、第一步: 将源文件add.c, div.c, mult.c, sub.c 进行汇编, 得到二进制目标文件 add.o, div.o, mult.o, sub.o
# 1. 生成.o
$ gcc add.c div.c mult.c sub.c -c
sub.c:2:18: fatal error: head.h: No such file or directory
compilation terminated.
# 提示头文件找不到, 添加参数 -I 重新头文件路径即可
$ gcc add.c div.c mult.c sub.c -c -I ./include/
# 查看目标文件是否已经生成
$ tree
.
├── add.c
├── add.o # 目标文件
├── div.c
├── div.o # 目标文件
├── include
│ └── head.h
├── main.c
├── mult.c
├── mult.o # 目标文件
├── sub.c
└── sub.o # 目标文件
2、第二步: 将生成的目标文件通过 ar工具打包生成静态库
# 2. 将生成的目标文件 .o 打包成静态库
$ ar rcs libcalc.a a.o b.o c.o # a.o b.o c.o在同一个目录中可以写成 *.o
# 查看目录中的文件
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│ └── `head.h ===> 和静态库一并发布
├── `libcalc.a ===> 生成的静态库
├── main.c
├── mult.c
├── mult.o
├── sub.c
└── sub.o
3、第三步: 将生成的的静态库 libcalc.a和库对应的头文件head.h一并发布给使用者就可以了
# 3. 发布静态库
1. head.h => 函数声明
2. libcalc.a => 函数定义(二进制格式)
6.2 静态库的使用
# 1. 首先拿到了发布的静态库
`head.h` 和 `libcalc.a`
# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试
.
├── head.h # 函数声明
├── libcalc.a # 函数定义(二进制格式)
└── main.c # 函数测试
编译测试程序, 得到可执行文件
在编译的时将静态库的路径和名字都指定出来
-L: 指定库所在的目录(相对或者绝对路径)
-l: 指定库的名字, 需要掐头(lib)去尾(.a) 剩下的才是需要的静态库的名字
# 4. 编译的时候指定库信息
-L: 指定库所在的目录(相对或者绝对路径)
-l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc
# -L -l, 参数和参数值之间可以有空格, 也可以没有 -L./ -lcalc
$ gcc main.c -o app -L ./ -l calc
# 查看目录信息, 发现可执行程序已经生成了
$ tree
.
├── app # 生成的可执行程序
├── head.h
├── libcalc.a
└── main.c
编译的源文件中包含了头文件 head.h, 这个头文件中声明的函数对应的定义(也就是函数体实现)在静态库中,程序在编译的时候没有找到函数实现
6.3 生成动态链接库
动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态链接库也可称之为共享库
动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是 按照特殊方式形成的。库中函数和变量的地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是 在应用程序加载动态库时形成的
在Linux中动态库以lib作为前缀, 以.so作为后缀, 中间是库的名字自己指定即可, 即: libxxx.so
1、生成动态链接库是直接使用gcc命令并且需要添加-fPIC(-fpic) 以及-shared 参数
-fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置。
-shared参数的作用是告诉编译器生成一个动态链接库
2、生成动态链接库的具体步骤如下:
1)将源文件进行汇编操作, 需要使用参数 -c, 还需要添加额外参数 -fpic / -fPIC
# 得到若干个 .o文件
$ gcc 源文件(*.c) -c -fpic
2)将得到的.o文件打包成动态库, 还是使用gcc, 使用参数 -shared 指定生成动态库(位置没有要求)
$ gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
3)发布动态库和头文件
# 发布
1. 提供头文件: xxx.h
2. 提供动态库: libxxx.so
第一步: 使用gcc将源文件进行汇编(参数-c), 生成与位置无关的目标文件, 需要使用参数 -fpic或者-fPIC
第二步: 使用gcc将得到的目标文件打包生成动态库, 需要使用参数 -shared
第三步: 发布生成的动态库和相关的头文件
- head.h
- libcalc.so
6.4 动态库的使用
1、拿到发布的动态库
head.h libcalc.so
2、基于头文件编写测试程序, 测试动态库中提供的接口是否可用
main.cpp
include <stdio.h>
#include "head.h"
int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
.
├── head.h ==> 函数声明
├── libcalc.so ==> 函数定义
└── main.c ==> 函数测试
和使用静态库一样, 在编译的时候需要指定库相关的信息: 库的路径 -L和 库的名字 -l
在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l
执行生成的可执行程序, 错误提示:可执行程序执行的时候找不到动态库