CMake学习记录

一、准备知识

1.1 C++的编译过程

        C++的编译过程(即从源码生成最终的可执行文件) 一般有下列几个步骤:

                ①预处理(Preprocess)

                ②编译(Compile)

                ③汇编(assemble)

                ④链接(link)            

        下图所示为C++程序编译的过程。

以使用g++命令为例,输入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>.

第一步:预处理 。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 静态链接库和动态链接库 

        静态链接库和动态链接库的区别是链接的阶段不一样。

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

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

1.3 CMake的必要性

1.3.1 g++ 编译

        当我们使用g++进行编译的时候,一般来说是运行如下指令:

g++ main.cpp -o 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 

        对一些常用库,g++能在默认查询路径中找到他们,但是当我们引入的外部库越来越多时,命令行编译这种方式就会变得不方便。这时候就CMake就登场了。

1.3.2 CMake简介

CMake时用于构建、测试和软件打包的开源跨平台工具。

特性:

  • 自动搜索可能需要的程序、库和头文件的能力;
  • 独立的构建目录(如build),可以安全清理
  • 支持复杂的自定义命令(下载、生成各种文件)
  • 自定义配置可选组件
  • 从简单的文本文件(CMakeLists.txt)自动生成工作区和项目的能力
  • 在主流平台上自动生成文件依赖项并支持并行构建
  • 几乎支持所有的IDE

二、CMake基础知识

2.1 安装CMake

        ubuntu上执行下述命令

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

CMake 基本命令:

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

2.3 CMake语法基础

2.3.1 指定版本

cmake_minimum_required(VERSION 3.10)

2.3.2 设定项目

CMakeLists.txt的开头,都会使用project来指定本项目的名称、版本、介绍、与使用的语言

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

2.3.3 添加可执行文件

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

add_executable(Target src.cpp)

 

2.3.4 生成静态库并链接

#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

# 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")
# 添加目标链接库



# 编译后目录如下
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_link_libraries(test_account PRIVATE Account)
  • 这里我们用到add_library, 和add_executable一样,Account为最终生成的库文件名(lib库名称.a),第二个参数是用于指定链接库为动态链接库(SHARED)还是静态链接库(STATIC),后面的参数是需要用到的源文件。
  • 通过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)


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

链接的操作与静态链接库一样

2.3.6  CMake 中的 PUBLIC、PRIVATE、INTERFACE

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

target_link_libraries(A B)为例,从理解的角度来看

  • PRIVATE :依赖项B仅链接到目标A,如果有C 链接了AC不会链接B
  • INTERFACE :依赖项B并不链接到目标A,如果有C 链接了AC会链接B
  • PUBLIC :依赖项B链接到目标A,如果有C 链接了AC也会链接B

其实就是对象属性的传递,打个散烟的比方:

  • PRIVATE: 就是自己抽,不给别人抽
  • INTERFACE :就是自己不抽,给别人抽
  • PUBLIC :就是自己抽,也给别人抽

从使用的角度来说,如果有C链接了目标A

  • 如果B仅用于A的实现,且不在头文件中提供给C使用,使用PRIVATE
  • 如果B不用于A的实现,仅在头文件中作为借口给C使用,使用INTERFACE
  • 如果B既用于A的实现,也在头文件中提供给C使用,使用PUBLIC
    # 创建库
    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)

  • 因为CBPUBLIC依赖项,所以C会传播到A
  • 因为DBPRIVATE依赖性,所以D不会传播到A

2.3.7 变量

# 设置变量
set(VAR1 "变量1")
message("VAR1=" ${VAR1}) # 外部访问
message("输出变量VAR1:${VAR1}") # 内部拼接
message("\${VAR1}=${VAR1}") # 使用\转义
unset(VAR1) # 删除变量
message("\${VAR1}=${VAR1}") # 删除变量后,输出为空

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

2.3.8 include

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

2.3.9 条件控制

 

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 -S . -B build
$ cd 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
... rebuild_cache
... edit_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

2.3.11生成器表达式

# 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>:获取编译目标的文件名
4.输出相关表达式:用于在不同的环节使用不同参数,比如需要在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_func,有两个参数
function(second_func arg1 arg2)
    message("第一个参数:${arg1}, 第二个参数:${arg2}")
endfunction(second_func)

2.3.13 设置安装

当需要发布项目时你需要指定项目文件的安装路径。下面的代码片段中,使用install安装demo_test,并分别将可执行文件安装在bin中,动态链接库和静态链接库都安装在lib,公共头文件安装在include。这里的路径都将添加${CMAKE_INSTALL_PREFIX}作为前缀(如果不设置CMAKE_INSTALL_PREFIX,则会安装到/usr/local 目录下)。实现安装的功能在你需要发布你项目给其他人使用时,非常有用。

# 设置安装
install(TARGETS demo_test
        RUNTIME DESTINATION bin # 可执行文件
        LIBRARY DESTINATION lib # 动态库
        ARCHIVE DESTINATION lib # 静态库
        PUBLIC_HEADER DESTINATION include # 公共头文件
)

2.3.14 寻找依赖 find_package

# 使用find_package寻找<LibaryName>库,如果找到,一般都会有以下变量(库作者设置)
<LibaryName>_FOUND:表示是否找到
<LibaryName>_INCLUDE_DIR:表示头文件目录
<LibaryName>_LIBRARIES:表示库文件目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值