CMake基础

1. 准备知识

1.1 c++编译过程

        之前我们用vs code一件编译生成c++的可执行文件,但是其实无论用什么工具编译C++文件到输出最后的可执行文件都会经过下面的步骤:预处理(Preprocess)、编译(Compile)、汇编(assemble)、链接(link)。

输入 g++ --help 可以看到对应命令:

-E                       Preprocess only; do not compile, assemble or link.
-S                       Compile only; do not assemble or link.
-c                       Compile and assemble, but do not link.
-o <file>                Place the output into <file>.

我们以一个简单的程序为例:

#include <iostream>

int main()
{
    std::cout << "Hello world!" << std::endl;
    return 0;
}

使用g++编译的过程为:

第一步:预处理:

C++ 中预处理指令以 # 开头。在预处理阶段,会对 #define 进行宏展开,处理 #if,#else 等条件编译指令,递归处理 #include。这一步需要我们添加所有头文件的引用路径。

# 将xx.cpp源文件预处理成xx.i文件(文本文件)
g++ -E main.cpp -o main.i

第二步:编译

检查代码的规范性和语法错误等,检查完毕后把代码翻译成汇编语言文件。

# 将xx.i文件编译为xx.s的汇编文件(文本文件)
g++ -S main.i -o main.s

第三步:汇编

基于汇编语言文件生成二进制格式的目标文件。

# 将xx.s文件汇编成xx.o的二进制目标文件
g++ -c main.s -o main.o

第四步:链接

将目标代码与所依赖的库文件进行关联或者组装,合成一个可执行文件。

# 将xx.o二进制文件进行链接,最终生成可执行程序
g++ main.o -o main

1.2. c++中的动态库与静态库

两者的区别就是链接的阶段不一样:

  • 静态链接库(static library)名称一般是 lib库名称.a.a 代表 archive library),其链接发生在编译环节。一个工程如果依赖一个静态链接库,其输出的库或可执行文件会将静态链接库 *.a 打包到该工程的输出文件中(可执行文件或库),因此生成的文件比较大,但在运行时也就不再需要库文件了。

  • 而动态链接库(shared library)的链接发生在程序的执行过程中,其在编译环节仅执行链接检查,而不进行真正的链接,这样可以节省系统的开销。动态库一般后缀名为 *.so.so 代表 shared object,Linux:lib库名称.so ,macOS:lib库名称.dylib)。动态链接库加载后,在内存中仅保存一份拷贝,多个程序依赖它时,不会重复加载和拷贝,这样也节省了内存的空间。

总结一下:静态链接库与动态链接库的主要区别在于:代码链接的阶段不一样,静态库的链接发生在编译环节,生成的可执行文件中包含静态库的可执行文件内容,虽然输出的执行文件占用内存比较大,但是运行文件的时候不再需要库文件;相反,动态库的链接发生在执行阶段,链接动态库的可执行文件不包含数据库的可执行文件内容,但是需要在执行的时候调用相关库文件,节约了内存。下面是一个简单的示例图:

1.3. 为什么需要CMake?

当我们每次使用g++进行编译,导入外部库的方法有二(以main函数为例,导入外部库gflags):

# 安装gflags
sudo apt-get install libgflags-dev libgflags2.2 

// -lgflags表示链接gflags库,-o main表示输出文件名为main
g++ main.cpp -lgflags -o main 

# 或者:

# 安装pkg-config
sudo apt-get install pkg-config

// pkg-config是一个工具,用于查找和管理安装在系统上的库文件,--cflags --libs gflags表示查找gflags库的头文件和库文件的路径,-o main表示输出文件名为main

g++ main.cpp `pkg-config --cflags --libs gflags`  -o main 


# 测试输出
./main --age 31 --name alice

        一些常用库我们也不用手动添加头文件或链接库路径,通常 g++ 能在默认查询路径中找到他们。当我们的项目文件变得多起来,引入的外部库也多起来时,命令行编译这种方式就会变得十分臃肿,也不方便调试和编辑。通常在测试单个文件时会使用命令行进行编译,但不推荐在一个实际项目中使用命令行编译。

2. CMake基础知识

2.1 在ubuntu安装CMake

命令行安装:

sudo apt install cmake -y

编译安装:

# 以v3.25.1版本为例
git clone -b v3.25.1 https://github.com/Kitware/CMake.git 
cd CMake
# 你使用`--prefix`来指定安装路径,或者去掉`--prefix`,安装在默认路径。
./bootstrap --prefix=<安装路径> && make && sudo make install

# 验证
cmake --version

注意:安装之后验证版本可能链接不到,这时候就需要将你的安装地址添加到系统路径中了:如果不使用--prefix=<安装路径>指定安装路径的话:

# 打开配置文件
vim ~/.bashrc
# 添加到文件末尾,指定路径
export PATH=/usr/local/bin:$PATH
# 保存配置
source ~/.bashrc

2.2 CMake编译示例:

一般运行一个编写完整的c++项目需要如下步骤:

# 第一步:配置,-S 指定源码目录,-B 指定构建目录
cmake -S . -B build 
# 第二步:生成,--build 指定构建目录
cmake --build build
# 运行
./build/MyProject

2.3 语法基础

2.3.1 一个最简单的CMakeLists.txt文件示例:

# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.10)

# first_cmake是项目名称,VERSION是版本号,DESCRIPTION是项目描述,LANGUAGES是项目语言
project(first_cmake 
        VERSION 1.0.0 
        DESCRIPTION "项目描述"
        LANGUAGES CXX) 

# 添加一个可执行程序,first_cmake是可执行程序名称,main.cpp是源文件
add_executable(first_cmake main.cpp)

命令cmake_minimum_required来指定当前工程所使用的 CMake 版本,不区分大小写的,通常用小写。VERSION 是这个函数的一个特殊关键字,版本的值在关键字之后。CMake 中的命令大多和 cmake_minimum_required 相似,不区分大小写,并有很多关键字来引导命令的参数输入(类似函数传参)。

2.3.2 设置项目

project(ProjectName 
        VERSION 1.0.0 
        DESCRIPTION "项目描述"
        LANGUAGES CXX) 

在 CMakeLists.txt 的开头,都会使用project来指定本项目的名称、版本、介绍、与使用的语言。在 project 中,第一个 ProjectName(例子中用的是 first_cmake)不需要参数,其他关键字都有参数。

2.3.3 添加可执行文件目标

add_executable(first_cmake main.cpp)

这里我们用到add_executable,其中第一个参数是最终生成的可执行文件名以及在 CMake 中定义的 Target 名。我们可以在 CMake 中继续使用 Target 的名字为 Target 的编译设置新的属性和行为。命令中第一个参数后面的参数都是编译目标所使用到的源文件。

2.3.4 生成静态库并链接

不论实现一个动态库还是静态库,必须包括库的头文、实现文件与编译文件:

头文件:

头文件包含函数、类、变量的声明。这些声明定义了库的接口,供使用者在编译时知道如何调用库中的函数或使用类。头文件不包含函数的具体实现,实际的实现代码放在源文件中(如 .cpp 文件)。

#ifndef Account_H
#define Account_H

class Account
{
private:
    /* data */
public:
    Account(/* args */);
    ~Account();
};


#endif // Account_H

源文件:

通过将接口声明放在头文件中,而将实现放在源文件中,可以实现接口与实现的分离。这种做法可以让库的使用者只需要包含头文件即可使用库提供的功能,而不需要关心具体的实现细节。

#include "Account.h"
#include <iostream>

Account::Account(/* args */)
{
    std::cout << "构造函数Account::Account()" << std::endl;
}
Account::~Account()
{
    std::cout << "析构函数Account::~Account()" << std::endl;
}

编译文件:

#account_dir/CMakeLists.txt

# 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目信息
project(Account)

# 添加静态库,Linux下会生成libAccount.a
add_library(Account STATIC Account.cpp Account.h)

# 编译静态库后,会在build下生成 build/libAccount.a 静态库文件
account_dir/
├── Account.cpp
├── Account.h
├── build
│   └── libAccount.a
└── CMakeLists.txt

这里我们用到add_library, 和 add_executable 一样,Account 为最终生成的库文件名(lib库名称.a),第二个参数是用于指定链接库为动态链接库(SHARED)还是静态链接库(STATIC),后面的参数是需要用到的源文件。

主函数链接:

# test_account/CMakeLists.txt

# 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目名称
project(test_account)

# 添加执行文件
add_executable(test_account test_account.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)

# 编译后目录如下
4.static_lib_test/
├── account_dir
│   ├── Account.cpp
│   ├── Account.h
│   ├── build
│   │   └── libAccount.a
│   └── CMakeLists.txt
└── test_account 
    ├── build
    │   └── test_account
    ├── CMakeLists.txt
    └── test_account.cpp

target_include_directories,我们给 test_account 添加了头文件引用路径 "../account_dir"。上面的关键词 PUBLIC,PRIVATE 用于说明目标属性的作用范围,更多介绍参考下节。

target_link_libraries,将前面生成的静态库 libAccount.a 链接给对象 test_account,但此时还没指定库文件的目录,CMake 无法定位库文件

target_link_directories,添加库文件的目录即可。

2.3.5 生成动态库并连接

#account_dir/CMakeLists.txt

# 添加动态库,Linux下会生成libAccount.so
add_library(Account SHARED Account.cpp Account.h)

当然,也可以用一个 CMakeLists.txt 来一次性编译:

#6.build_together/CMakeLists.txt`

# 最低版本要求
cmake_minimum_required(VERSION 3.10)

# 项目信息
project(test_account)

# 添加动态库
add_library(Account SHARED "./account_dir/Account.cpp" "./account_dir/Account.h")

# 添加可执行文件
add_executable(test_account "./test_account/test_account.cpp")

# 添加头文件
target_include_directories(test_account PUBLIC "./account_dir")
# 添加链接库
target_link_libraries(test_account Account)

2.3.6 CMake 中的 PUBLIC、PRIVATE、INTERFACE

CMake 中经常使用 target_...() 类似的命令,一般这样的命令支持通过 PUBLICPRIVATEINTERFACE 关键字来控制传播。

举例:

# 创建库
add_library(C c.cpp)
add_library(D d.cpp)
add_library(B b.cpp)

# C是B的PUBLIC依赖项
target_link_libraries(B PUBLIC C)
# D是B的PRIVATE依赖项
target_link_libraries(B PRIVATE D)

# 添加可执行文件
add_executable(A a.cpp)

# 将B链接到A
target_link_libraries(A B)
  • 因为 C 是 B 的 PUBLIC 依赖项,所以 C 会传播到 A
  • 因为 D 是 B 的 PRIVATE 依赖性,所以 D 不会传播到 A

2.3.7 变量

像其他编程语言一样,我们应该将 CMake 理解为一门编程语言。我们也需要设定变量来储存我们的选项,信息。有时候我们通过变量来判断我们在什么平台上,通过变量来判断我们需要编译哪些 Target,也通过变量来决定添加哪些依赖。

1.设置变量:

# 设置变量
set(VAR1 "666")   # 设置变量
message("VAR1=" ${VAR1}) # 外部访问
message("输出变量VAR1:${VAR1}  正确") # 内部拼接(可以随时连接空格)
message("输出变量VAR1:"${VAR1}  正确) # 外部拼接(在连接中间空格不被识别)
message("\${VAR1}=${VAR1}") # 使用\转义
unset(VAR1) # 删除变量
message("\${VAR1}=${VAR1}") # 删除变量后,输出为空

打印:

VAR1=666
输出变量VAR1:666  正确
输出变量VAR1:666正确
${VAR1}=666
${VAR1}=

2. 设置变量缓存:

# 设置变量缓存,可以在命令行中修改(-D参数)
set(CACHE_VARIABLE_TEST "原始值" CACHE STRING "变量缓存的描述")
set(CACHE_VARIABLE_TEST "修改值" CACHE STRING "变量缓存的描述")
message("变量缓存的值:${CACHE_VARIABLE_TEST}")
set(CACHE_VARIABLE_TEST "修改值" CACHE STRING "变量缓存的描述" FORCE)  # 与变量一致,基本不用
message("变量缓存的值:${CACHE_VARIABLE_TEST}")

打印:

变量缓存的值:原始值
变量缓存的值:修改值

使用命令行传参:

# 设置变量缓存,可以在命令行中修改(-D参数)
set(CACHE_VARIABLE_TEST "原始值" CACHE STRING "变量缓存的描述")
message("变量缓存的值:${CACHE_VARIABLE_TEST}")

打印:

root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/7.message_var_demo# cmake -S . -B build
变量缓存的值:原始值
-- Configuring done
-- Generating done
-- Build files have been written to: /root/autodl-tmp/src/7.message_var_demo/build
root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/7.message_var_demo# cmake -S . -B build -DCACHE_VARIABLE_TEST=修改值
变量缓存的值:修改值
-- Configuring done
-- Generating done
-- Build files have been written to: /root/autodl-tmp/src/7.message_var_demo/build

3. 系统变量

常见的内置的变量,更多访问:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-that-provide-information

第一类:提供信息的变量

# 第一类:提供信息的变量
message("${PROJECT_NAME}") # 项目名称
message("${CMAKE_SOURCE_DIR}") # 源码目录
message("${CMAKE_BINARY_DIR}") # 编译目录
message("${CMAKE_CURRENT_LIST_FILE}") # 当前CMakeLists.txt文件路径

打印:

root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/7.message_var_demo# cmake -S . -B build
message_var_demo
/root/autodl-tmp/src/7.message_var_demo
/root/autodl-tmp/src/7.message_var_demo/build
/root/autodl-tmp/src/7.message_var_demo/CMakeLists.txt
-- Configuring done
-- Generating done
-- Build files have been written to: /root/autodl-tmp/src/7.message_var_demo/build

第二类:控制CMake运行的变量

# 第二类:控制CMake运行的变量,更多:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-that-change-behavior
set(BUILD_SHARED_LIBS ON) # 设置是否构建动态库,默认为OFF,即构建静态库,设置为ON后,构建动态库
# 生成库
add_library(${PROJECT_NAME} Account.cpp Account.h)

打印:

root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/7.message_var_demo# cmake --build build
[ 50%] Building CXX object CMakeFiles/message_var_demo.dir/Account.cpp.o
[100%] Linking CXX shared library libmessage_var_demo.so
[100%] Built target message_var_demo

# 第三类:描述系统的变量

# 第三类:描述系统的变量,更多:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-that-describe-the-system
message("是否是Windows系统:${WIN32}")
message("是否是Linux系统:${UNIX}")
message("系统名称:${CMAKE_SYSTEM_NAME}")

打印:

root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/7.message_var_demo# cmake -S . -B build
是否是Windows系统:
是否是Linux系统:1
系统名称:Linux
-- Configuring done
-- Generating done
-- Build files have been written to: /root/autodl-tmp/src/7.message_var_demo/build

2.3.8 使用include调用其他代码

cmake_minimum_required(VERSION 3.10)

project(include_demo)

message("调用include前的信息")

# include,引用一次就导入一次
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/module_1.cmake")
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/module_1.cmake")

message("调用include后的信息")

在module_1.cmake中只打印一段消息

message("模块内部被调用")

最后编译输出:

调用include前的信息
模块内部被调用
模块内部被调用
调用include后的信息

2.3.9 条件控制

正如前面所讲,应该把 CMake 当成编程语言,除了可以设置变量以外,CMake 还可以写条件控制。

if(variable)
    # 为true的常量:ON、YES、TRUE、Y、1、非0数字
else()
    # 为false的常量:OFF、NO、FALSE、N、0、空字符串、NOTFOUND
endif()

可以和条件一起使用的关键词有:

NOT, TARGET, EXISTS (file), DEFINED等
STREQUAL, AND, OR, MATCHES (regular expression), VERSION_LESS, VERSION_LESS_EQUAL等

2.3.10 CMake分步编译

root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/10.steps_demo/build# cmake --build . --target help
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... edit_cache
... rebuild_cache
... steps_demo
... main.o
... main.i
... main.s
# 1.预处理
$ cmake --build . --target main.i
# 输出:Preprocessing CXX source to CMakeFiles/steps_demo.dir/main.cpp.i
# 可以打开滑到底部

# 2.编译
$ cmake --build . --target main.s
# 输出汇编代码:Compiling CXX source to assembly CMakeFiles/steps_demo.dir/main.cpp.s

# 3.汇编
$ cmake --build . --target main.o
# 输出二进制文件:Building CXX object CMakeFiles/steps_demo.dir/main.cpp.o

# 链接
$ cmake --build .
Scanning dependencies of target steps_demo
[ 50%] Linking CXX executable steps_demo
[100%] Built target steps_demo

# 运行
./steps_demo

分布编译过程;

root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/10.steps_demo/build# cmake --build . --target main.i
Preprocessing CXX source to CMakeFiles/steps_demo.dir/main.cpp.i
root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/10.steps_demo/build# cmake --build . --target main.s
Compiling CXX source to assembly CMakeFiles/steps_demo.dir/main.cpp.s
root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/10.steps_demo/build# cmake --build . --target main.o
Building CXX object CMakeFiles/steps_demo.dir/main.cpp.o
root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/10.steps_demo/build# cmake --build .
[ 50%] Linking CXX executable steps_demo
[100%] Built target steps_demo
root@autodl-container-63904cb222-5aac8750:~/autodl-tmp/src/10.steps_demo/build# ./steps_demo 
Hello, World!

2.3.11 生成器表达式

生成器表达式简单来说就是在 CMake 生成构建系统的时候根据不同配置动态生成特定的内容。有时用它可以让代码更加精简,我们介绍几种常用的。

需要注意的是,生成表达式被展开是在生成构建系统的时候,所以不能通过解析配置 CMakeLists.txt 阶段的 message 命令打印,可以用类似 file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<BOOL:TRUE>:TEST>") 生成文件的方式间接测试。

在其最一般的形式中,生成器表达式是 $<...>,尖括号中间可以是如下几种类型:

  • 条件表达式
  • 变量查询(Variable-Query)
  • 目标查询(Target-Query)
  • 输出相关的表达式
# 1.条件表达式:$<condition:true_string>,当condition为真时,返回true_string,否则返回空字符串
$<0:TEST>  
$<1:TEST>  
$<$<BOOL:TRUE>:TEST>

# 2.变量查询(Variable-Query)
$<TARGET_EXISTS:target>:判断目标是否存在
$<CONFIG:Debug>:判断当前构建类型是否为Debug

# 3.目标查询(Target-Query)
$<TARGET_FILE:target>:获取编译目标的文件路径
$<TARGET_FILE_NAME:target>:获取编译目标的文件名

输出相关表达式:用于在不同的环节使用不同参数,比如需要在 install 和 build 环节分别用不同的参数,我们可以这样写:

add_library(Foo ...)
target_include_directories(Foo
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
        $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

其中 $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> 仅在 build 环节生效;而 $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> 仅在 install 环节生效。通过设定不同阶段不同的参数,我们可以避免路径混乱的问题。

2.3.12 函数和宏

# 定义一个宏,宏名为my_macro,没有参数
macro(my_macro)
    message("宏内部的信息")
    set(macro_var "宏内部变量test")
endmacro(my_macro)

# 定义一个宏,宏名为second_macro,有两个参数
macro(second_macro arg1 arg2)
    message("第一个参数:${arg1}, 第二个参数:${arg2}")    
endmacro(second_macro)

# 定义一个函数,函数名为my_func,没有参数
function(my_func)
    message("函数内部的信息")
    set(func_var "变量test")
endfunction(my_func)

# 定义一个函数,函数名为second_func,有两个参数
function(second_func arg1 arg2)
    message("第一个参数:${arg1}, 第二个参数:${arg2}")
endfunction(second_func)

这么看,宏和函数非常相似,但是两这其实是有区别的,主要是作用域不一样,我们尝试调用宏和函数:

# 调用宏
my_macro()
my_macro()
# 输出宏内部的信息,也能访问到变量,理解为代码替换
message(${macro_var})

# 调用函数
my_func()
my_func()
# 访问不了函数内部的变量,因为函数是一个独立的作用域
message(${func_var})

2.3.13 设置intall

cmake_minimum_required(VERSION 3.10)

project(instal_demo)

# 添加头文件
include_directories(include)

# 添加静态库
add_library(slib STATIC src/slib.cpp include/slib.h)
# 添加动态库
add_library(dlib SHARED src/dlib.cpp include/dlib.h)

# 设置RPATH,否则install后,运行时找不到动态库
SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) 
SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")

# 添加可执行文件
add_executable(instal_demo main.cpp)
# 链接库
target_link_libraries(instal_demo slib dlib)

# 设置公共头文件,以便install时,将头文件一起安装,或者使用install(DIRECTORY include/ DESTINATION include)
set_target_properties(slib PROPERTIES PUBLIC_HEADER include/slib.h)
set_target_properties(dlib PROPERTIES PUBLIC_HEADER include/dlib.h)

# 安装头文件
# install(DIRECTORY include/ DESTINATION include)

# 设置安装
install(TARGETS instal_demo slib dlib
        RUNTIME DESTINATION bin # 可执行文件
        LIBRARY DESTINATION lib # 动态库
        ARCHIVE DESTINATION lib # 静态库
        PUBLIC_HEADER DESTINATION include # 公共头文件
        )
        
#[[
如果不设置DCMAKE_INSTALL_PREFIX ,则会安装到 /usr/local 目录下
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=./installed
或者其他目录
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=~/Documents/install_demo
cmake --build build
cmake --install build
]]       

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值