Cmake介绍与基本用法

Posted by MZ Blog on May 27, 2025

1.CMake概述

CMake 是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有Makefile(通过 make 命令进行项目的构建),大多是IDE软件都集成了make,如果自己动手写 makefile,会发现,makefile 通常依赖于当前的编译平台,而且编写 makefile 的工作量比较大,解决依赖关系时也容易出错。

而 CMake 恰好能解决上述问题, 其允许开发者指定整个工程的编译流程,在根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需make编译即可,所以可以把CMake看成一款自动生成 Makefile的工具,其编译流程如下图:

介绍完CMake的作用之后,再来总结一下它的优点:

  • 跨平台
  • 能够管理大型项目
  • 简化编译构建过程和编译过程
  • 可扩展:可以为 cmake 编写特定功能的模块,扩充 cmake 功能

2.CMake使用

2.1CMake使用流程

CMake支持大写、小写、混合大小写的命令。如果在编写CMakeLists.txt文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。

测试准备

测试文件

首先,为了方便测试,准备几个测试文件:

  • add.cpp
#include <stdio.h> #include "head.h" int add(int a, int b) {    return a+b; }
  • sub.cpp
#include <stdio.h>
#include "head.h"

// 你好
int subtract(int a, int b)
{
    return a-b;
}
  • mult.cpp
#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
    return a*b;
}
  • div.cpp
#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.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;
}

上述文件的目录结构如下:

$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c

添加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(可执行程序名 源文件名称)
    • 源文件名可以是一个或者多个,用空格或者;分开
# 样式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)

执行CMake命令

cmake命令执行格式:

# cmake 命令原型
$ cmake CMakeLists.txt文件所在路径
$ tree
.
├── add.c
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c

0 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中指定的)。

更清晰的构建

通过上面的例子可以看出,如果在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
└── Makefile

1 directory, 3 files

这样就可以在build目录中执行make命令编译项目,生成的相关文件自然也就被存储到build目录中了。这样通过cmake和make生成的所有文件就全部和项目源文件隔离开了。

2.1注释

**CMake**使用**#**进行**行注释**

# 这是一个 CMakeLists.txt 文件
cmake_minimum_required(VERSION 3.0.0)

**CMake**使用**#[[]]**进行**块注释**

#[[ 这是一个 CMakeLists.txt 文件。
这是一个 CMakeLists.txt 文件
这是一个 CMakeLists.txt 文件]]
cmake_minimum_required(VERSION 3.0.0)

2.2自定义变量

在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.3.1C++标准定义

C++标准对应有一宏叫做**DCMAKE_CXX_STANDARD**

#增加-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.3.2输出路径的指定

在CMake中指定可执行程序输出的路径,也对应一个宏,叫做**EXECUTABLE_OUTPUT_PATH**,它的值还是通过set命令进行设置:

set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)

2.4搜索文件

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦也不现实。所以,在CMake中为我们提供了搜索文件的命令,可以使用**aux_source_directory**命令或者**file**命令。

2.4.1aux_source_directory

aux_source_directory(< dir > < variable >)
  • dir:要搜索的路径
  • variable:将从dir目录下搜索到的源文件都添加到variable变量中
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.2file

如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦了。所以,在CMake中为我们提供了搜索文件的命令,他就是**file**(当然,除了搜索以外通过 file 还可以做其他事情)。

file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
  • GLOB:在指定目录中搜索符合条件的源文件列表添加到变量中
  • GLOB_RECURSE:在指定目录中递归的搜索符合条件的源文件列表添加到变量中。
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.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.cpp

3 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})

2.6制作动态库和静态库

制作库命令

在cmake中,如果要制作静态库或者动态库,需要使用的命令如下:

add_library(库名称 STATIC/SHARED 源文件1 [源文件2] ...) 
  • STATIC:制作静态库
  • SHARED:制作动态库

库名称为中间那段,比如有一个库为libcalc.so,那么这里的库名称为calc。

指定输出路径

在学习可执行文件的输出路径时,有一个EXECUTABLE_OUTPUT_PATH 宏,可以指定动态库的输出路径,但是静态库不可以,因为动态库生成的也是可执行的文件,而静态库不是。

有一个通用的库路径输出方式:**LIBRARY_OUTPUT_PATH**,这个适用于静态库和动态库

2.7包含库文件

在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令。

2.7.1链接静态库

命令:

link_libraries(<static lib> [<static lib>...])

static lib是静态库的名称,可以是全称,也可以是中间一部分,也就是可以是calc或者libcalc.a.

如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:

link_directories(<lib path>)

2.7.2链接动态库

命令:

target_link_libraries(
    <target> 
    <PRIVATE|PUBLIC|INTERFACE> <item>... 
    [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
  • target:指定要加载的库的文件的名字

    • 该文件可能是一个源文件
    • 该文件可能是一个动态库/静态库文件
    • 该文件可能是一个可执行文件
  • 访问权限:

    • PUBLIC:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用。
    • PRIVATE:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库
    • INTERFACE:在interface后面引入的库不会被链接到前面的target中,只会导出符号。

动态库的链接和静态库是完全不同的:

  • 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
  • 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存

如果动态库不是系统提供的,此时可以将动态库的路径也指定出来:

link_directories(<lib path>)

2.8日志

在CMake中可以用用户显示一条消息,该命令的名字为**message**

message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)

2.9变量操作

两个主要操作,set和list

set拼接

set(变量名1 ${变量名1} ${变量名2} ...)

list拼接

list(APPEND <list> [<element> ...])

list为拼接后的变量

list其他操作

1.移除

list(REMOVE_ITEM <list> <value> [<value> ...])

2.获取长度

list(LENGTH <list> <output variable>)

3.读取列表中指定索引的的元素,可以指定多个索引

list(GET <list> <element index> [<element index> ...] <output variable>)

4.查找指定元素

list(FIND <list> <value> <output variable>)

2.10宏定义

#include <stdio.h>
#define NUMBER  3

int main()
{
    int a = 10;
#ifdef DEBUG
    printf("我是一个程序猿, 我不会爬树...\n");
#endif
    for(int i=0; i<NUMBER; ++i)
    {
        printf("hello, GCC!!!\n");
    }
    return 0;
}

DEBUG宏并没有定义,可以在cmake中定义:

add_definitions(-D宏名称)

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指令定义的项目名称
MAKE_BINARY_DIR 项目实际构建路径,假设在build目录进行的构建,那么得到的就是这个目录的路径