云计算百科
云计算领域专业知识百科平台

CMake 入门:从一个可执行程序到多目录 C++ 工程

刚开始写 C++ 时,项目里只有一个 main.cpp,直接执行一条 g++ 命令就能完成编译。等源文件、头文件和第三方库逐渐增多,编译命令也会越来越长。这时继续手写命令不仅麻烦,还容易漏掉文件或链接选项。

CMake 的作用就是把这些构建规则统一写在 CMakeLists.txt 中,再根据当前平台生成 Makefile、Ninja 或 IDE 工程。它本身通常不直接完成编译,而是先生成构建系统,再由对应的编译工具执行真正的编译和链接。

一、CMake 的基本工作流程

一个 CMake 项目通常经历三个阶段:

CMakeLists.txt
↓ 配置和生成
构建系统(Makefile、Ninja 等)
↓ 编译和链接
可执行程序或库文件

推荐把构建产物放在单独的 build 目录,不要和源码混在一起:

cmake -S . -B build
cmake –build build

这两条命令分别表示:

  • -S .:源码目录是当前目录;
  • -B build:生成文件写入 build 目录;
  • cmake –build build:调用当前生成器完成构建。

这种写法不依赖底层使用的是 Make 还是 Ninja,也比手动进入 build 目录再执行 cmake .. 更直观。

需要重新构建时,通常直接再次执行:

cmake –build build

如果修改了 CMakeLists.txt,CMake 会在构建前检查并重新生成必要的文件。

二、第一个 CMakeLists.txt

先准备一个最简单的目录:

hello_cmake/
├── CMakeLists.txt
└── main.cpp

main.cpp:

#include <iostream>

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

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)

project(HelloCMake LANGUAGES CXX)

add_executable(hello main.cpp)

这里出现了三个最基础的命令。

1. cmake_minimum_required

cmake_minimum_required(VERSION 3.16)

它声明项目要求的最低 CMake 版本,同时确定一组与该版本对应的策略行为。一般放在文件开头,不建议省略。

2. project

project(HelloCMake LANGUAGES CXX)

HelloCMake 是工程名,LANGUAGES CXX 表示项目使用 C++。还可以同时声明版本号:

project(HelloCMake VERSION 1.0 LANGUAGES CXX)

3. add_executable

add_executable(hello main.cpp)

这条命令创建名为 hello 的可执行目标,后面列出参与编译的源文件。目标名不必与 project() 中的工程名相同。

在项目根目录执行:

cmake -S . -B build
cmake –build build

Linux 下可以运行:

./build/hello

如果使用 Visual Studio 这类多配置生成器,可执行文件可能位于 build/Debug 或 build/Release 中。

三、指定 C++ 标准和构建类型

项目使用 C++17 时,可以在 CMakeLists.txt 中设置:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

三行配置分别表示:

  • 使用 C++17;
  • 编译器不支持时直接报错,不自动退回更旧的标准;
  • 尽量使用标准模式,而不是编译器特有的 GNU 扩展模式。

也可以只给某个目标声明它需要的语言特性:

target_compile_features(hello PRIVATE cxx_std_17)

对于单配置生成器,可以在配置阶段指定 Debug 或 Release:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug

切换为发布构建:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

Visual Studio、Xcode 等多配置生成器通常在构建阶段选择配置:

cmake –build build –config Release

四、变量和列表

CMake 使用 set() 定义变量:

set(APP_NAME calculator)
set(APP_SOURCES main.cpp add.cpp sub.cpp)

add_executable(${APP_NAME} ${APP_SOURCES})

${APP_NAME} 表示读取变量的值。CMake 的列表本质上由分号分隔,因此下面两种写法都能创建列表:

set(SOURCES main.cpp add.cpp sub.cpp)
set(SOURCES main.cpp;add.cpp;sub.cpp)

需要追加或删除元素时,可以使用 list():

list(APPEND SOURCES mul.cpp)
list(REMOVE_ITEM SOURCES sub.cpp)

五、源文件应该手写还是自动搜索

源文件较少时,直接列出来最稳妥:

add_executable(calculator
main.cpp
add.cpp
sub.cpp
)

新增或删除源文件时,构建规则的变化在代码审查中一目了然。

也可以使用 file(GLOB) 搜索目录中的文件:

file(GLOB APP_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
)

add_executable(calculator ${APP_SOURCES})

CONFIGURE_DEPENDS 会让 CMake 在构建时检查匹配结果是否发生变化。不过对于长期维护的工程,仍然更推荐显式列出源文件;自动搜索更适合示例项目或文件数量较多、结构固定的场景。

常见路径变量有:

  • PROJECT_SOURCE_DIR:最近一次 project() 对应的源码根目录;
  • PROJECT_BINARY_DIR:该工程对应的构建目录;
  • CMAKE_CURRENT_SOURCE_DIR:当前 CMakeLists.txt 所在的源码目录;
  • CMAKE_CURRENT_BINARY_DIR:当前目录对应的构建目录。

六、添加头文件目录

假设头文件放在 include 目录:

project/
├── include/
│ └── calc.h
├── src/
│ └── calc.cpp
└── main.cpp

不建议一上来使用影响整个目录的 include_directories(),更推荐把头文件路径绑定到具体目标:

add_executable(calculator
main.cpp
src/calc.cpp
)

target_include_directories(calculator PRIVATE
${PROJECT_SOURCE_DIR}/include
)

这样可以明确看出 include 是 calculator 目标的编译需求,不会无意间影响其他目标。

七、生成静态库和动态库

使用 add_library() 可以创建库目标:

add_library(calc STATIC
src/add.cpp
src/sub.cpp
)

STATIC 表示静态库。在 Linux 下通常生成 libcalc.a,在 Windows 下通常生成 .lib 文件。

把 STATIC 改为 SHARED 即可生成动态库:

add_library(calc SHARED
src/add.cpp
src/sub.cpp
)

Linux 下通常生成 libcalc.so,Windows 下通常生成 .dll 及相应的导入库。

如果希望由构建选项决定库类型,可以不写 STATIC 或 SHARED:

add_library(calc
src/add.cpp
src/sub.cpp
)

配置时通过 BUILD_SHARED_LIBS 选择:

cmake -S . -B build -DBUILD_SHARED_LIBS=ON

八、链接库以及 PUBLIC、PRIVATE、INTERFACE

创建库后,可以通过目标名直接链接:

add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE calc)

如果 calc 是同一个 CMake 工程中的目标,不需要手动写它生成后的绝对路径,也不需要调用 link_directories()。CMake 能识别目标之间的依赖关系,并按正确顺序完成构建。

target_link_libraries() 和 target_include_directories() 中经常出现三个关键字:

  • PRIVATE:只供当前目标使用;
  • PUBLIC:当前目标使用,同时传递给依赖它的目标;
  • INTERFACE:当前目标自己不用,只传递给使用者。

以库的头文件为例:

target_include_directories(calc
PUBLIC ${PROJECT_SOURCE_DIR}/include
)

calc 编译时需要该目录,链接 calc 的程序也需要通过该目录找到公开头文件,因此这里使用 PUBLIC。

而应用程序链接 calc 时通常写:

target_link_libraries(calculator PRIVATE calc)

应用程序本身不会再被其他目标当作库来使用,所以没有必要向外传递这个依赖。

九、用多级 CMakeLists.txt 管理工程

下面把计算器拆成库和应用程序两个部分:

calculator/
├── CMakeLists.txt
├── include/
│ └── calc/
│ └── calc.h
├── src/
│ ├── CMakeLists.txt
│ └── calc.cpp
└── app/
├── CMakeLists.txt
└── main.cpp

头文件 include/calc/calc.h:

#pragma once

namespace calc
{
int add(int left, int right);
int sub(int left, int right);
}

实现文件 src/calc.cpp:

#include "calc/calc.h"

namespace calc
{
int add(int left, int right)
{
return left + right;
}

int sub(int left, int right)
{
return left right;
}
}

程序入口 app/main.cpp:

#include <iostream>
#include "calc/calc.h"

int main()
{
std::cout << "20 + 10 = " << calc::add(20, 10) << '\\n';
std::cout << "20 – 10 = " << calc::sub(20, 10) << '\\n';
return 0;
}

根目录的 CMakeLists.txt 负责工程级配置和添加子目录:

cmake_minimum_required(VERSION 3.16)

project(Calculator VERSION 1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)

add_subdirectory(src)
add_subdirectory(app)

src/CMakeLists.txt 创建静态库:

add_library(calc STATIC
calc.cpp
)

target_include_directories(calc
PUBLIC ${PROJECT_SOURCE_DIR}/include
)

app/CMakeLists.txt 创建可执行程序并链接库:

add_executable(calculator
main.cpp
)

target_link_libraries(calculator PRIVATE calc)

构建并运行:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake –build build
./build/bin/calculator

输出:

20 + 10 = 30
20 – 10 = 10

这个结构的关键不是把一个很长的 CMakeLists.txt 拆成几个短文件,而是让每个子目录只负责定义自己的目标。根目录负责组织,库目录负责公开使用要求,应用目录负责组合目标,依赖关系会更清楚。

十、添加宏定义和日志

如果只想给某个目标定义 DEBUG 宏,可以使用:

target_compile_definitions(calculator PRIVATE DEBUG)

对应的 C++ 代码:

#ifdef DEBUG
std::cout << "debug mode" << std::endl;
#endif

相比全局的 add_definitions(-DDEBUG),目标级命令不会影响无关的库或程序。

排查配置问题时,可以使用 message() 输出变量:

message(STATUS "source dir: ${PROJECT_SOURCE_DIR}")
message(STATUS "binary dir: ${PROJECT_BINARY_DIR}")

需要主动终止配置时,可以写:

if(NOT EXISTS "${PROJECT_SOURCE_DIR}/include")
message(FATAL_ERROR "include directory not found")
endif()

STATUS 用于普通提示,WARNING 会显示警告但继续配置,FATAL_ERROR 会立即终止配置。

赞(0)
未经允许不得转载:网硕互联帮助中心 » CMake 入门:从一个可执行程序到多目录 C++ 工程
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!