CMake
是一个开源的跨平台工具,可以构建、测试和打包软件。
它具有如下特性:
- 自动搜索可能需要的程序、库和头文件的能力;
- 独立的构建目录(如
build
),可以安全清理; - 支持复杂的自定义命令(下载、生成各种文件);
- 自定义配置可选组件;
- 从简单的文本文件(
CMakeLists.txt
)自动生成工作区和项目的能力; - 在主流平台上自动生成文件依赖项并支持并行构建;
- 几乎支持所有的
IDE
1. 安装CMake
sudo apt install cmake -y
安装完成后用查看版本指令cmake -version
检查CMake
是否安装成功。
出现以上提示代表安装成功。
2. 第一个CMake例子
我们首先新建两个文件,main.cpp
和 CMakeLists.txt
touch main.cpp CMakeLists.txt
main.cpp
中编写如下代码
#include <iostream>
using namespace std;
int main(){
cout << "第一个CMake程序" << endl;
return 0;
}
CMakeLists.txt
中编写如下代码
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.1)
# demo 代表项目名称
project (demo)
# 添加一个可执行程序,demo是可执行程序名称,main.cpp是源文件
add_executable(main main.cpp)
这里我们用到
add_executable
,其中第一个参数是最终生成的可执行文件名以及在CMake
中定义的Target
名。我们可以在CMake
中继续使用Target
的名字为Target
的编译设置新的属性和行为。命令中第一个参数后面的参数都是编译目标所使用到的源文件。
在准备好后我们依次执行下列指令:
# 第一步:配置,-S 指定源码目录,-B 指定构建目录
cmake -S . -B build
# 第二步:生成,--build 指定构建目录
cmake --build build
# 运行
./build/main
- 第一步,我们输入
cmake -S . -B build
- 第二步,我们输入
cmake --build build
此时会产生一个可执行文件 main
- 第三步,我们输入
./build/main
运行可执行文件main
3. 同一个目录下编译多个文件
接下来我们尝试在同一个目录下编译多个文件,我们首先在文件夹下新建如下
4
4
4 个文件,我们首先演示同时编译带有头文件和 cpp
文件的案例。
touch main.cpp Account.cpp Account.h CMakeLists.txt
mian.cpp
的内容如下:
# include "Account.h"
# include <iostream>
int main()
{
Account Q;
}
Account.cpp
的内容如下:
#include "Account.h"
#include <iostream>
Account::Account(/* args */)
{
std::cout << "构造函数Account::Account()" << std::endl;
}
Account::~Account()
{
std::cout << "析构函数Account::~Account()" << std::endl;
}
Account.h
的内容如下:
#ifndef Account_H
#define Account_H
class Account
{
private:
/* data */
public:
Account(/* args */);
~Account();
};
#endif // Account_H
CMakeLitsts.txt
的内容如下:
#account_dir/CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(Account)
# main 是可执行文件名称,后面是源文件
add_executable(main Account.cpp main.cpp Account.h)
接下来我们开始编译,依次输入以下指令:
cmake -S . -B build
cmake --build build
编译成功后文件结构如下所示
这个案例是想说明,如果在同一目录下有多个源文件,那么只要在
add_executable
里把所有源文件都添加进去就可以了,但是当我们源文件过多的时候就不太方便了,这时候我们用到了cmake
的另一个命令aux_source_directory(dir var)
第一个参数dir
是指定目录,第二个参数var
是用于存放源文件列表的变量。
所以我们可以改写一下 CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(Account)
aux_source_directory(. SRC_LIST)
add_executable(main ${SRC_LIST})
这段代码使用
aux_source_directory
把当前目录下的源文件存列表存放到变量SRC_LIST
里,然后在add_executable
里调用SRC_LIST
再次执行 cmake
后最终的得到的结果是完全一样的。
值得注意的是,aux_source_directory()
也存在弊端,它会把指定目录下的所有源文件都加进来,可能会加入一些我们不需要的文件,此时我们可以使用 set
命令去新建变量来存放需要的源文件,如下:
cmake_minimum_required (VERSION 3.10)
project (Account)
set( SRC_LIST
./main.cpp
./Account.cpp
./Account.h)
add_executable(main ${SRC_LIST})
set
命令是用于定义变量的,set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
EXECUTABLE_OUT_PATH
和PROJECT_SOURCE_DIR
是CMake
自带的预定义变量,其意义如下,
EXECUTABLE_OUTPUT_PATH
:目标二进制可执行文件的存放位置PROJECT_SOURCE_DIR
:工程的根目录
4. 不同目录下编译多个文件
接着上面的内容,我们来演示不同目录下编译多个文件的案例,我们将上面的文件按照如下的结构重构,内容不需要改变。
一般来说, 源文件都放到 src
目录下,把头文件放入到 include
文件下,生成的对象文件放入到 build
目录下,最终输出的 elf
文件会放到 bin
目录下
随后在最外层目录下新建 main.cpp
和 CMakeLists.txt
main.cpp
代码如下:
#include <iostream>
#include "Account.h"
int main()
{
Account alice_account;
std::cout << "test Account 的main函数" << std::endl;
return 0;
}
CMakeLists.txt
代码如下:
cmake_minimum_required (VERSION 3.10)
project (Account)
include_directories (include src)
aux_source_directory (include SRC_LIST)
aux_source_directory (src SRC_LIST1)
add_executable (main main.cpp ${SRC_LIST} ${SRC_LIST1})
这里出现了一个新的命令:
include_directories
。该命令是用来向工程添加多个指定头文件的搜索路径,路径之间用空格分隔。
因为main.cpp
里include
了Account.h
,如果没有这个命令来指定头文件所在位置,就会无法编译。当然,也可以在main.cpp
里使用include
来指定路径,如下
#include "include/Account.h""
完成后开始编译:
cmake -S . -B build
cmake --build build
./build/main
5. 静态库和动态库
5.1 什么是库文件?
库文件(Library files)是预先编译好的可重用代码和资源的集合,它们被用于简化软件开发过程并提供常见功能的封装。库文件包含一组函数、类、数据结构、变量、常量或其他可执行代码的实现。
库文件的主要目的是为了促进代码的重用和模块化开发,以提高开发效率、减少代码冗余,并使代码更易于维护。通过使用库文件,开发人员可以在自己的应用程序中调用库中提供的函数、方法和类,从而实现特定功能而无需从头开始编写代码。
库文件可以分为两种主要类型:
- 静态库(Static Library):静态库的代码在编译时被复制到可执行文件中,可执行文件独立于库文件运行。静态库提供了一种静态链接的方式,使得可执行文件可以在没有外部依赖的情况下运行。静态库通常以.lib (Dynamic-Link Libraries)(Windows)或.a(Unix/Linux)等文件扩展名表示。
- 动态库(Dynamic Library):动态库的代码在运行时由操作系统动态加载到内存中,并与可执行文件共享。动态库可以被多个应用程序共享使用,减少了内存占用和重复加载的开销。动态库通常以.dll(Windows)或.so (Shared Object)(Unix/Linux)等文件扩展名表示。
库文件可以提供各种功能,例如数学计算、文件操作、网络通信、图形界面、数据库访问、加密解密等。通过使用库文件,开发人员可以利用已经实现和测试过的功能,加速开发过程,减少错误和重复劳动。
5.2 静态库和动态库的区别?
-
链接方式:
- 静态库:在编译时,静态库的代码会被完整地复制到可执行文件中。链接时,编译器将库的代码与应用程序代码合并成一个独立的可执行文件。这意味着可执行文件在运行时不依赖于外部的库文件。
- 动态库:在编译时,动态库的代码不会被复制到可执行文件中。相反,可执行文件在运行时加载动态库,并在内存中共享库的代码和数据。这意味着可执行文件依赖于外部的库文件,并且可以与多个应用程序共享。
-
文件大小和内存占用:
-
静态库:静态库将代码复制到可执行文件中,因此可执行文件的大小会增加。每个使用该静态库的可执行文件都包含该库的完整副本,因此占用的内存空间也会较大。
-
动态库:动态库的代码不复制到可执行文件中,因此可执行文件的大小较小。多个应用程序可以共享同一个动态库的实例,因此内存占用可以被多个应用程序共享,减少了重复加载的开销。
-
-
更新和维护:
- 静态库:静态库一旦被编译到可执行文件中,更新库的代码需要重新编译可执行文件。这意味着每个使用该静态库的应用程序都需要重新构建以包含最新版本的库。
- 动态库:动态库可以被独立地更新,而不需要重新编译可执行文件。只需替换动态库文件即可,所有使用该库的应用程序都可以享受到更新的功能和修复的 bug。
-
可移植性:
- 静态库:静态库是平台相关的,需要为每个目标平台编译不同的库文件。可执行文件与特定平台上的静态库文件紧密耦合,不易在不同平台上移植。
- 动态库:动态库是平台独立的,可以在多个平台上共享使用。可执行文件只需要加载适应当前平台的动态库即可实现跨平台的移植性。
5.3 生成库文件
下面使用代码来演示一下生成静态库的过程,依然新建三个文件,Account.cpp
, Account.h
,CMakeLists.txt
Account.cpp
内容如下:
#include "Account.h"
#include <iostream>
Account::Account(/* args */)
{
std::cout << "构造函数Account::Account()" << std::endl;
}
Account::~Account()
{
std::cout << "析构函数Account::~Account()" << std::endl;
}
Account.h
内容如下:
#ifndef Account_H
#define Account_H
class Account
{
private:
/* data */
public:
Account(/* args */);
~Account();
};
#endif // Account_H
CMakeLists.txt
内容如下:
#account_dir/CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(Account)
# 添加静态库,Linux下会生成libAccount.a
# Account是库名,STATIC表示静态库,SHARED表示动态库,后面是源文件
add_library(Account STATIC Account.cpp Account.h)
add_library
生成动态库或静态库 (第 1 1 1 个参数指定库的名字;第 2 2 2 个参数决定是动态还是静态 (默认静态);第 3 3 3 个参数指定生成库的源文件)
准备完成后我们使用如下指定编译:
cmake -S . -B build
cmake --build build
成功编译后我们就可以在 build
文件夹下看到我们以 .a
结尾的库文件了
下面来演示更加复杂的例子,我们首先将上一步编译好的部分文件移除,下图高亮部分,也就是在build
目录下只留下 libAccount.a
文件:
随后在 test_account
文件夹下新建如下两个文件:
mian.cpp
内容如下:
#include <iostream>
#include "Account.h"
int main()
{
Account alice_account;
std::cout << "main函数被调用" << std::endl;
return 0;
}
CMakeLists.txt
内容如下:
# test_account/CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目名称
project(test_account)
# 添加执行文件
add_executable(test_account main.cpp)
# 指定目标包含的头文件目录
target_include_directories(test_account PUBLIC "../account_dir")
# 添加库文件目录,如果不添加,找不到库文件
target_link_directories(test_account PUBLIC "../account_dir/build")
# 指定目标链接的库
target_link_libraries(test_account PRIVATE Account)
通过
target_include_directories
,我们给test_account
添加了头文件引用路径"../account_dir"
。上面的关键词PUBLIC
,PRIVATE
用于说明目标属性的作用范围。
通过target_link_libraries
,将前面生成的静态库libAccount.a
链接给对象test_account
,但此时还没指定库文件的目录,CMake
无法定位库文件
再通过target_link_directories
,添加库文件的目录即可。
准备完成后结构如下:
进入 test_account
目录开始编译:
编译后结构如下所示:
6. CMake OpenCV 例子
安装 OpenCV
库:
sudo apt install libopencv-dev
main.cpp
代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
# 输入一张图像返回这张图象缩放后的灰度图
int main(int argc, char **argv)
{
if (argc != 2)
{
std::cout << "请输入图片路径" << std::endl;
return -1;
}
else
{
std::cout << "图片路径为:" << argv[1] << std::endl;
cv::Mat image = cv::imread(argv[1]);
cv::Mat image_resized;
cv::resize(image, image_resized, cv::Size(400, 400));
cv::cvtColor(image_resized, image_resized, cv::COLOR_BGR2GRAY);
cv::imwrite("resized.png", image_resized);
std::cout << "图片已处理完毕,并保存为resized.png" << std::endl;
}
return 0;
}
CMakeLists.txt
代码如下
cmake_minimum_required(VERSION 3.10)
project(demo_opencv)
find_package(OpenCV REQUIRED)
if (OpenCV_FOUND)
message(STATUS "OpenCV library status:")
message(STATUS " libraries: ${OpenCV_LIBS}")
message(STATUS " include path: ${OpenCV_INCLUDE_DIRS}")
else ()
message(FATAL_ERROR "Could not find OpenCV")
endif ()
add_executable(demo_opencv main.cpp)
target_include_directories(demo_opencv PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(demo_opencv ${OpenCV_LIBS})
左侧为原图,右侧为缩放后的图
知识点整理较为混乱
TODO:后面应该多做几个真实案例
参考文献
Linux下CMake简明教程