CMake学习笔记(1)
1. CMake概述
CMake 是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有Makefile(通过 make 命令进行项目的构建),大多是IDE软件都集成了make,比如:VS 的 nmake、linux 下的 GNU make、Qt 的 qmake等,如果自己动手写 makefile,会发现,makefile 通常依赖于当前的编译平台,而且编写 makefile 的工作量比较大,解决依赖关系时也容易出错。
而 CMake 恰好能解决上述问题, 其允许开发者指定整个工程的编译流程,在根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需make编译即可,所以可以把CMake看成一款自动生成 Makefile的工具,其编译流程如下图:
- 蓝色虚线表示使用makefile构建项目的过程
- 红色实线表示使用cmake构建项目的过程
介绍完CMake的作用之后,再来总结一下它的优点:
- 跨平台
- 能够管理大型项目
- 简化编译构建过程和编译过程
- 可扩展:可以为 cmake 编写特定功能的模块,扩充 cmake 功能
2. CMake的使用
CMake支持大写、小写、混合大小写的命令。如果在编写CMakeLists.txt文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。
2.1 注释
2.1.1 注释行
CMake 使用 # 进行行注释,可以放在任何位置。
# 这是一个 CMakeLists.txt 文件
cmake_minimum_required(VERSION 3.0.0)
2.1.2 注释块
CMake 使用 #[[ ]] 形式进行块注释。
#[[ 这是一个 CMakeLists.txt 文件。
这是一个 CMakeLists.txt 文件
这是一个 CMakeLists.txt 文件]]
cmake_minimum_required(VERSION 3.0.0)
2.2 只有源文件
2.2.1 共处一室
1、准备工作,为了方便测试,在我本地电脑准备了这么几个测试文件
- 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;
}
2、上述文件的目录结构如下:
$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
3、添加 CMakeLists.txt 文件
在上述源文件所在目录下添加一个新文件 CMakeLists.txt,文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)
接下来依次介绍一下在 CMakeLists.txt 文件中添加的三个命令:
cmake_minimum_required
:指定使用的 cmake 的最低版本
- 可选,非必须,如果不加可能会有警告
project
:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。
# 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>...])
add_executable
:定义工程会生成一个可执行程序
add_executable(可执行程序名 源文件名称)
-
这里的可执行程序名和project中的项目名没有任何关系
-
源文件名可以是一个也可以是多个,如有多个可用空格或;间隔
# 样式1
add_executable(app add.c div.c main.c mult.c sub.c)
# 样式2
add_executable(app add.c;div.c;main.c;mult.c;sub.c)
4、执行CMake 命令
万事俱备只欠东风,将 CMakeLists.txt 文件编辑好之后,就可以执行 cmake命令了。
# cmake 命令原型
$ cmake CMakeLists.txt文件所在路径
$ tree
.
├── add.c
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c0 directories, 7 files
robin@OS:~/Linux/3Day/calc$ cmake .
当执行cmake命令之后,CMakeLists.txt 中的命令就会被执行,所以一定要注意给cmake 命令指定路径的时候一定不能出错。
执行命令之后,看一下源文件所在目录中是否多了一些文件:
$ tree -L 1
.
├── add.c
├── CMakeCache.txt # new add file
├── CMakeFiles # new add dir
├── cmake_install.cmake # new add file
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── Makefile # new add file
├── mult.c
└── sub.c
我们可以看到在对应的目录下生成了一个makefile
文件,此时再执行make
命令,就可以对项目进行构建得到所需的可执行程序了。
$ make
Scanning dependencies of target app
[ 16%] Building C object CMakeFiles/app.dir/add.c.o
[ 33%] Building C object CMakeFiles/app.dir/div.c.o
[ 50%] Building C object CMakeFiles/app.dir/main.c.o
[ 66%] Building C object CMakeFiles/app.dir/mult.c.o
[ 83%] Building C object CMakeFiles/app.dir/sub.c.o
[100%] Linking C executable app
[100%] Built target app# 查看可执行程序是否已经生成
$ tree -L 1
.
├── add.c
├── app # 生成的可执行程序
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── Makefile
├── mult.c
└── sub.c
最终可执行程序app
就被编译出来了(这个名字是在CMakeLists.txt
中指定的)。
2.2.2 VIP 包房
通过上面的例子可以看出,如果在CMakeLists.txt
文件所在目录执行了cmake
命令之后就会生成一些目录和文件(包括 makefile 文件
),如果再基于makefile
文件执行make
命令,程序在编译过程中还会生成一些中间文件和一个可执行文件,这样会导致整个项目目录看起来很混乱,不太容易管理和维护,此时我们就可以把生成的这些与项目源码无关的文件统一放到一个对应的目录里边,比如将这个目录命名为build
:
$ mkdir build
$ cd build
$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/Linux/build
现在cmake
命令是在build
目录中执行的,但是CMakeLists.txt
文件是build
目录的上一级目录中,所以cmake
命令后指定的路径为…,即当前目录的上一级目录。
当命令执行完毕之后,在build
目录中会生成一个makefile
文件
$ tree build -L 1
build
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
└── Makefile1 directory, 3 files
这样就可以在build
目录中执行make
命令编译项目,生成的相关文件自然也就被存储到build
目录中了。这样通过cmake
和make
生成的所有文件就全部和项目源文件隔离开了,各回各家,各找各妈。
2.3 私人订制
2.3.1 定义变量
在上面的例子中一共提供了5个源文件,假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在cmake里定义变量需要使用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.3.2 指定使用的C++标准
在编写C++程序的时候,可能会用到C++11、C++14、C++17、C++20等新特性,那么就需要在编译的时候在编译命令中制定出要使用哪个标准:
$ g++ *.cpp -std=c++11 -o app
上面的例子中通过参数-std=c++11
指定出要使用c++11标准编译程序,C++标准对应有一宏叫做DCMAKE_CXX_STANDARD
。在CMake中想要指定C++标准有两种方式:
1、在 CMakeLists.txt 中通过 set 命令指定
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)
2、在执行 cmake 命令的时候指定出这个宏的值
#增加-std=c++11
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
#增加-std=c++14
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=14
#增加-std=c++17
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=17
- 在上面例子中 CMake 后的路径需要根据实际情况酌情修改。
2.3.3 指定输出的路径
在CMake中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH
,它的值还是通过set
命令进行设置:
set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
- 第一行:定义一个变量用于存储一个绝对路径
- 第二行:将拼接好的路径值设置给EXECUTABLE_OUTPUT_PATH宏
- 如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录。
2.4 搜索文件
如果一个项目里边的源文件很多,在编写CMakeLists.txt
文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦也不现实。所以,在CMake中为我们提供了搜索文件的命令,可以使用aux_source_directory
命令或者file
命令。
2.4.1 方式1
在 CMake 中使用aux_source_directory
命令可以查找某个路径下的所有源文件,命令格式为:
aux_source_directory(< dir > < variable >)
dir
:要搜索的目录variable
:将从dir
目录下搜索到的源文件列表存储到该变量中
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})
2.4.2 方式2
如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以,在CMake中为我们提供了搜索文件的命令,他就是file(当然,除了搜索以外通过 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)
-
CMAKE_CURRENT_SOURCE_DIR
宏表示当前访问的CMakeLists.txt
文件所在的路径。 -
关于要搜索的文件路径和类型可加双引号,也可不加:
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
2.5 包含头文件
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories
:
include_directories(headpath)
举例说明,有源文件若干,其目录结构如下:
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
└── src├── add.cpp├── div.cpp├── main.cpp├── mult.cpp└── sub.cpp3 directories, 7 files
CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALC)
set(CMAKE_CXX_STANDARD 11)
set(HOME /home/robin/Linux/calc)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin/)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
add_executable(app ${SRC_LIST})
其中,第六行指定就是头文件的路径,PROJECT_SOURCE_DIR宏对应的值就是我们在使用cmake命令时,后面紧跟的目录,一般是工程的根目录。
2.6 制作动态库或静态库
有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,下面来讲解在cmake
中生成这两类库文件的方法。
2.6.1 制作静态库
在cmake
中,如果要制作静态库,需要使用的命令如下:
add_library(库名称 STATIC 源文件1 [源文件2] ...)
在Linux中,静态库名字分为三部分:lib
+库名字
+.a
,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。
下面有一个目录,需要将src
目录中的源文件编译成静态库,然后再使用:
.
├── build
├── CMakeLists.txt
├── include # 头文件目录
│ └── head.h
├── main.cpp # 用于测试的源文件
└── src # 源文件目录├── add.cpp├── div.cpp├── mult.cpp└── sub.cpp
根据上面的目录结构,可以这样编写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
。
2.6.2 制作动态库
在cmake
中,如果要制作动态库,需要使用的命令如下:
add_library(库名称 SHARED 源文件1 [源文件2] ...)
在Linux中,动态库名字分为三部分:lib
+库名字
+.so
,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。
根据上面的目录结构,可以这样编写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 SHARED ${SRC_LIST})
这样最终就会生成对应的动态库文件libcalc.so
。
2.6.3 指定输出的路径
- 方式1 - 适用于动态库
对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录:
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
# 设置动态库生成路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
add_library(calc SHARED ${SRC_LIST})
对于这种方式来说,其实就是通过set
命令给EXECUTABLE_OUTPUT_PATH
宏设置了一个路径,这个路径就是可执行文件生成的路径。
- 方式2 - 都适用
由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用EXECUTABLE_OUTPUT_PATH
宏了,而应该使用LIBRARY_OUTPUT_PATH
,这个宏对应静态库文件和动态库文件都适用。
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 生成动态库
#add_library(calc SHARED ${SRC_LIST})
# 生成静态库
add_library(calc STATIC ${SRC_LIST})
2.7 包含库文件
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令。
2.7.1 链接静态库
src
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp
现在我们把上面src
目录中的add.cpp
、div.cpp
、mult.cpp
、sub.cpp
编译成一个静态库文件libcalc.a
。通过命令制作并使用静态链接库
- 测试目录结构如下:
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
├── lib
│ └── libcalc.a # 制作出的静态库的名字
└── src└── main.cpp4 directories, 4 files
在cmake中,链接静态库的命令如下:
link_libraries(<static lib> [<static lib>...])
用于设置全局链接库,这些库会链接到之后定义的所有目标上。
- 参数1:指定出要链接的静态库的名字
- 可以是全名 libxxx.a
- 也可以是掐头(lib)去尾(.a)之后的名字 xxx
- 参数2-N:要链接的其它静态库的名字
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:
link_directories(<lib path>)
这样,修改之后的CMakeLists.txt
文件内容如下:
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 链接静态库
link_libraries(calc)
add_executable(app ${SRC_LIST})
添加了第8行的代码,就可以根据参数指定的路径找到这个静态库了。
2.7.2 链接动态库
在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的,在此不再过多赘述,如有疑惑请参考Linux 静态库和动态库
在cmake中链接动态库的命令如下:
target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>... [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
用于指定一个目标(如可执行文件或库)在编译时需要链接哪些库。它支持指定库的名称、路径以及链接库的顺序。
-
target
:指定要加载的库的文件的名字- 该文件可能是一个源文件
- 该文件可能是一个动态库/静态库文件
- 该文件可能是一个可执行文件
-
PRIVATE|PUBLIC|INTERFACE
:动态库的访问权限,默认为PUBLIC
-
如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
-
动态库的链接具有传递性,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。
-
target_link_libraries(A B C)
target_link_libraries(D A)
PUBLIC
:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用。PRIVATE
:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库INTERFACE
:在interface后面引入的库不会被链接到前面的target中,只会导出符号。
2.7.3链接系统动态库
动态库的链接和静态库是完全不同的:
- 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
- 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存
因此,在cmake
中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后
:
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 添加并指定最终生成的可执行程序名
add_executable(app ${SRC_LIST})
# 指定可执行程序要链接的动态库名字
target_link_libraries(app pthread)
在target_link_libraries(app pthread)
中:
app
: 对应的是最终生成的可执行程序的名字pthread
:这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为libpthread.so,在指定的时候一般会掐头(lib)去尾(.so)。
2.7.4 链接第三方动态库
现在,自己生成了一个动态库,对应的目录结构如下:
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h # 动态库对应的头文件
├── lib
│ └── libcalc.so # 自己制作的动态库文件
└── main.cpp # 测试用的源文件3 directories, 4 files
假设在测试文件main.cpp
中既使用了自己制作的动态库libcalc.so
又使用了系统提供的线程库,此时CMakeLists.txt
文件可以这样写:
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(app ${SRC_LIST})
target_link_libraries(app pthread calc)
在第六行
中,pthread
、calc
都是可执行程序app
要链接的动态库的名字。当可执行程序app
生成之后并执行该文件,会提示有如下错误信息:
$ ./app
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
这是因为可执行程序启动之后,去加载calc这个动态库,但是不知道这个动态库被放到了什么位置解决动态库无法加载的问题,所以就加载失败了,在 CMake 中可以在生成可执行程序之前,通过命令指定出要链接的动态库的位置,指定静态库位置使用的也是这个命令:
link_directories(path)
所以修改之后的CMakeLists.txt
文件应该是这样的:
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
# 指定源文件或者动态库对应的头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 指定要链接的动态库的路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 添加并生成一个可执行程序
add_executable(app ${SRC_LIST})
# 指定要链接的动态库
target_link_libraries(app pthread calc)
通过link_directories
指定了动态库的路径之后,在执行生成的可执行程序的时候,就不会出现找不到动态库的问题了。
2.7.5 总结
温馨提示:target_link_libraries 和 link_libraries 是 CMake 中用于链接库的两个命令,都可以用于链接动态库和静态库,但它们的使用场景和功能有所不同。下面是关于二者的总结:
1、target_link_libraries
-
功能:
target_link_libraries
用于指定一个目标(如可执行文件或库)在编译时需要链接哪些库。它支持指定库的名称、路径以及链接库的顺序。 -
语法:
target_link_libraries(target_name [item1 [item2 [...]]][<debug|optimized|general> <lib1> [<lib2> [...]]])
-
优点:
- 更精确地控制目标的链接库。
- 可以指定库的不同链接条件(如调试版本、发布版本)。
- 支持多个目标和多个库之间的复杂关系。
- 更加灵活和易于维护,特别是在大型项目中。
-
示例:
add_executable(my_executable main.cpp)
target_link_libraries(my_executable PRIVATE my_dynamic_library)
2、link_libraries
-
功能: link_libraries 用于设置全局链接库,这些库会链接到之后定义的所有目标上。它会影响所有的目标,适用于全局设置,但不如 target_link_libraries 精确。
-
语法:
link_libraries(lib1 lib2 [...])
-
缺点:
- 缺乏针对具体目标的控制,不适合复杂的项目结构。
- 容易导致意外的依赖关系,因为它对所有目标都生效。
- 一旦设置,全局影响可能导致难以追踪的链接问题。
-
示例:
link_libraries(my_static_library)
add_executable(my_executable main.cpp)
3、总结
target_link_libraries
是更推荐的方式,因为它允许更精确的控制和管理链接库的依赖,特别是在大型项目中,它能够避免全局设置可能带来的问题。
link_libraries
虽然简单,但在复杂的项目中可能会导致意外的问题,通常适用于简单的项目或临时设置。
建议在CMake
项目中优先使用target_link_libraries
。
2.8 日志
在CMake中可以用用户显示一条消息,该命令的名字为message
:
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
(无)
:重要消息STATUS
:非重要消息WARNING
:CMake 警告, 会继续执行AUTHOR_WARNING
:CMake 警告 (dev), 会继续执行SEND_ERROR
:CMake 错误, 继续执行,但是会跳过生成的步骤FATAL_ERROR
:CMake 错误, 终止所有处理过程
CMake的命令行工具会在stdout
上显示STATUS
消息,在stderr上显示其他所有消息。CMake的GUI会在它的log区域显示所有消息。
CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")
2.9 变量操作
2.9.1 追加
有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过file
命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set
命令也可以使用list
命令。
1、使用set拼接
如果使用set进行字符串拼接,对应的命令格式如下:
set(变量名1 ${变量名1} ${变量名2} ...)
关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
set(SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
message(STATUS "message: ${SRC_1}")
2、使用list拼接
如果使用list进行字符串拼接,对应的命令格式如下:
list(APPEND <list> [<element> ...])
list
命令的功能比set
要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND
表示进行数据追加,后边的参数和set
就一样了。
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cpp)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cpp)
# 追加(拼接)
list(APPEND SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
message(STATUS "message: ${SRC_1}")
在CMake中,使用set
命令可以创建一个list
。一个在list
内部是一个由分号;分割的一组字符串。例如,set(var a b c d e)
命令将会创建一个list:a;b;c;d;e
,但是最终打印变量值的时候得到的是abcde
。
set(tmp1 a;b;c;d;e)
set(tmp2 a b c d e)
message(${tmp1})
message(${tmp2})
输出的结果:
abcde
abcde
2.9.2 字符串移除
我们在通过file
搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:
$ tree
.
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp0 directories, 5 files
在当前这么目录有五个源文件,其中main.cpp
是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要add.cpp
、div.cp
、mult.cpp
、sub.cpp
这四个源文件就可以了。此时,就需要将main.cpp从搜索到的数据中剔除出去,想要实现这个功能,也可以使用list
list(REMOVE_ITEM <list> <value> [<value> ...])
通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了REMOVE_ITEM
。
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/*.cpp)
# 移除前日志
message(STATUS "message: ${SRC_1}")
# 移除 main.cpp
list(REMOVE_ITEM SRC_1 ${PROJECT_SOURCE_DIR}/main.cpp)
# 移除后日志
message(STATUS "message: ${SRC_1}")
可以看到,在第8行
把将要移除的文件的名字指定给list就可以了。但是一定要注意通过 file
命令搜索源文件的时候得到的是文件的绝对路径(在list中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。
关于list
命令还有其它功能,但是并不常用,在此就不一一进行举例介绍了。
1、获取 list 的长度。
list(LENGTH <list> <output variable>)
LENGTH
:子命令LENGTH
用于读取列表长度<list>
:当前操作的列表<output variable>
:新创建的变量,用于存储列表的长度。
2、读取列表中指定索引的的元素,可以指定多个索引
list(GET <list> <element index> [<element index> ...] <output variable>)
-
<list>
:当前操作的列表 -
<element index>
:列表元素的索引- 从0开始编号,索引0的元素为列表中的第一个元素;
- 索引也可以是负数,-1表示列表的最后一个元素,-2表示列表倒数第二个元素,以此类推
- 当索引(不管是正还是负)超过列表的长度,运行会报错
-
<output variable>
:新创建的变量,存储指定索引元素的返回结果,也是一个列表。
3、将列表中的元素用连接符(字符串)连接起来组成一个字符串
list (JOIN <list> <glue> <output variable>)
<list>
:当前操作的列表<glue>
:指定的连接符(字符串)<output variable>
:新创建的变量,存储返回的字符串
4、查找列表是否存在指定的元素,若果未找到,返回-1
list(FIND <list> <value> <output variable>)
<list>
:当前操作的列表<value>
:需要再列表中搜索的元素<output variable>
:新创建的变量- 如果列表中存在,那么返回在列表中的索引
- 如果未找到则返回-1。
5、将元素追加到列表中
list (APPEND <list> [<element> ...])
6、在list中指定的位置插入若干元素
list(INSERT <list> <element_index> <element> [<element> ...])
7、将元素插入到列表的0索引位置
list (PREPEND <list> [<element> ...])
8、将列表中最后元素移除
list (POP_BACK <list> [<out-var>...])
9、将列表中第一个元素移除
list (POP_FRONT <list> [<out-var>...])
10、将指定的元素从列表中移除
list (REMOVE_ITEM <list> <value> [<value> ...])
11、将指定索引的元素从列表中移除
list (REMOVE_AT <list> <index> [<index> ...])
12、移除列表中的重复元素
list (REMOVE_DUPLICATES <list>)
13、列表翻转
list(REVERSE <list>)
14、列表排序
list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
- COMPARE:指定排序方法。有如下几种值可选:
- STRING:按照字母顺序进行排序,为默认的排序方法
- FILE_BASENAME:如果是一系列路径名,会使用basename进行排序
- NATURAL:使用自然数顺序排序
- CASE:指明是否大小写敏感。有如下几种值可选:
- SENSITIVE: 按照大小写敏感的方式进行排序,为默认值
- INSENSITIVE:按照大小写不敏感方式进行排序
- ORDER:指明排序的顺序。有如下几种值可选:
- ASCENDING:按照升序排列,为默认值
- DESCENDING:按照降序排列
2.10 宏定义
在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示:
#include <stdio.h>
#define NUMBER 3int main()
{int a = 10;
#ifdef DEBUGprintf("我是一个程序猿, 我不会爬树...\n");
#endiffor(int i=0; i<NUMBER; ++i){printf("hello, GCC!!!\n");}return 0;
}
在程序的第七行对DEBUG
宏进行了判断,如果该宏被定义了,那么第八行就会进行日志输出,如果没有定义这个宏,第八行就相当于被注释掉了,因此最终无法看到日志输入出(上述代码中并没有定义这个宏)。
为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在gcc/g++
命令中去指定,如下:
$ gcc test.c -DDEBUG -o app
在gcc/g++
命令中通过参数 -D
指定出要定义的宏的名字,这样就相当于在代码中定义了一个宏,其名字为DEBUG
。
在CMake
中我们也可以做类似的事情,对应的命令叫做add_definitions
:
add_definitions(-D宏名称)
针对于上面的源文件编写一个CMakeLists.txt
,内容如下:
cmake_minimum_required(VERSION 3.0)
project(TEST)
# 自定义 DEBUG 宏
add_definitions(-DDEBUG)
add_executable(app ./test.c)
通过这种方式,上述代码中的第八行日志就能够被输出出来了。
3. 预定义宏
下面的列表中为大家整理了一些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_NAME | 返回通过PROJECT指令定义的项目名称 |
CMAKE_BINARY_DIR | 项目实际构建路径,假设在build目录进行的构建,那么得到的就是这个目录的路径 |
相关文章:

CMake学习笔记(1)
1. CMake概述 CMake 是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有Makefile(通过 make 命令进行项目的构建),大多是IDE软件都集成了make,比如:VS 的 nmake、linux 下的 GNU make、Qt …...

cursor+deepseek构建自己的AI编程助手
文章目录 准备工作在Cursor中添加deepseek 准备工作 下载安装Cursor (默认安装在C盘) 注册deepseek获取API key 在Cursor中添加deepseek 1、打开cursor,选择设置 选择Model,添加deepseek-chat 注意这里去掉其他的勾选项&…...
Kotlin实现DataBinding结合ViewModel的时候,提示找不到Unresolved reference: BR解决方案
在用Kotlin语言实现DataBinding结合ViewModel的代码的时候,如下所示: class UserModel(private val userName: String, private val userAge: Int) : BaseObservable() {get:Bindablevar name: String userNameset (value) {field valuenotifyPropert…...
java项目启动时,执行某方法
1. J2EE项目 在Servlet类中重写init()方法,这个方法会在Servlet实例化时调用,即项目启动时调用。 import javax.servlet.ServletException; import javax.servlet.http.HttpServlet;public class MyServlet extends HttpServlet {Overridepublic void …...

详解如何自定义 Android Dex VMP 保护壳
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/ 前言 Android Dex VMP(Virtual Machine Protection,虚拟机保护)壳是一种常见的应用保护技术,主要用于保护 And…...

Grails应用http.server.requests指标数据采集问题排查及解决
问题 遇到的问题:同一个应用,Spring Boot(Java)和Grails(Groovy)混合编程,常规的Spring Controller,可通过Micromete Pushgateway, 采集到http.server.requests指标数据,注意下面的指标名称是点号&#…...

开源临床试验软件OpenClinica的安装
本文是为帮网友 A萤火虫 解决安装问题做的记录; 简介 什么是 OpenClinica ? OpenClinica 是世界上第一个商业开源临床试验软件,主要用于电子数据捕获(EDC)和临床数据管理(CDM)。它的设计旨在优…...

网络安全 | 网络安全法规:GDPR、CCPA与中国网络安全法
网络安全 | 网络安全法规:GDPR、CCPA与中国网络安全法 一、前言二、欧盟《通用数据保护条例》(GDPR)2.1 背景2.2 主要内容2.3 特点2.4 实施效果与影响 三、美国《加利福尼亚州消费者隐私法案》(CCPA)3.1 背景3.2 主要内…...
深入学习 Python 爬虫:从基础到实战
深入学习 Python 爬虫:从基础到实战 前言 Python 爬虫是一个强大的工具,可以帮助你从互联网上抓取各种数据。无论你是数据分析师、机器学习工程师,还是对网络数据感兴趣的开发者,爬虫都是一个非常实用的技能。在本文中ÿ…...

element plus 使用 upload 组件达到上传数量限制时隐藏上传按钮
最近在重构项目,使用了 element plus UI框架,有个功能是实现图片上传,且限制只能上传一张图片,结果,发现,可以限制只上传一张图片,但是上传按钮还在,如图: 解决办法&…...
音频DSP的发展历史
音频数字信号处理(DSP)的发展历史是电子技术、计算机科学和音频工程共同进步的结果。这个领域的进展不仅改变了音乐制作、音频后期制作和通信的方式,也影响了音频设备的设计和功能。以下是对音频DSP发展历史的概述: 早期概念和理论…...

2025低代码与人工智能AI新篇
在当今数字化浪潮汹涌澎湃的时代,低代码开发与人工智能(AI)犹如两颗璀璨的星辰,正逐渐交汇融合,为企业解锁前所未有的智能业务解决方案。今天,咱们就深入探讨一下低代码平台是如何集成 AI 技术,…...
【HarmonyOS Next NAPI 深度探索1】Node.js 和 CC++ 原生扩展简介
【HarmonyOS Next NAPI 深度探索1】Node.js 和 CC 原生扩展简介 如果你用过 Node.js,应该知道它强大的地方在于能处理各种场景,速度还很快。但你有没有想过,Node.js 的速度秘密是什么?今天我们来聊聊其中一个幕后英雄——原生扩展…...

redis的学习(四)
13. 渐进式遍历 通过渐进式遍历能够获取当前所有的key,又不会讲当前的服务器卡死。不是一个命令将所有的key获取,而是每执行一次命令,只获取到其中的一部分。所以想要获取到所有的key就需要多次遍历,即化整为零的思想。 渐进式遍历…...
C# winform 多线程 UI更新数据 报错:无法访问已释放的对象。
System.ObjectDisposedException HResult0x80131622 Message无法访问已释放的对象。 ObjectDisposed_ObjectName_Name SourceSystem.Windows.Forms StackTrace: at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, …...

error: linker `link.exe` not found
开始学习rust,安装好rust的环境,开始从hello world开始,结果用在win10环境下,使用vs code或cmd窗口编译rust报错: PS E:\study_codes\rust-demo\chart01> rustc hello.rs error: linker link.exe not found| note:…...
Vue.js组件开发-如何使用moment.js
在Vue.js组件开发中,需要处理日期和时间,moment.js 是一个非常有用的库。moment.js 提供了丰富的API来解析、验证、操作和显示日期和时间。 步骤: 1. 安装moment.js 首先,需要通过npm或yarn安装moment.js。在项目根目录下运行以…...

Linux第二课:LinuxC高级 学习记录day01
0、大纲 0.1、Linux 软件安装,用户管理,进程管理,shell 命令,硬链接和软连接,解压和压缩,功能性语句,结构性语句,分文件,make工具,shell脚本 0.2、C高级 …...
《DOM NodeList》
《DOM NodeList》 介绍 DOM(文档对象模型)是HTML和XML文档的编程接口,它允许开发者在JavaScript等编程语言中操作文档的结构、样式和内容。在DOM中,NodeList是一个重要的接口,它表示一个包含节点(如元素、…...

Nginx代理同域名前后端分离项目的完整步骤
前后端分离项目,前后端共用一个域名。通过域名后的 url 前缀来区别前后端项目。 以 vue php 项目为例。直接上 server 模块的 nginx 配置。 server{ listen 80; #listen [::]:80 default_server ipv6onlyon; server_name demo.com;#二配置项目域名 index index.ht…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...

全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...

uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...

Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...