跳转至

CMake与工程实践

🎯 学习目标

完成本章学习后,你将能够: - 掌握CMake的核心语法与现代CMake的目标导向(target-based)实践 - 理解并使用PUBLIC/PRIVATE/INTERFACE控制依赖传播 - 熟练使用find_package、FetchContent管理第三方依赖 - 集成Google Test/Catch2进行单元测试 - 配置CI/CD流水线实现自动化构建与测试 - 回顾智能指针与RAII模式,掌握现代C++内存管理最佳实践 - 能够从零搭建一个规范的C++工程项目

CMake工程化构建流程

上图展示了 configure/build/test/install 到 CI/CD 的完整链路,对应本章工程实践主线。


一、CMake基础

1.1 CMakeLists.txt 基本结构

CMake
# 指定CMake最低版本
cmake_minimum_required(VERSION 3.20)

# 定义项目名称、版本、使用的语言
project(MyProject
    VERSION 1.0.0
    DESCRIPTION "A sample C++ project"
    LANGUAGES CXX
)

# 设置C++标准
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)  # 生成compile_commands.json供IDE使用

# 添加可执行文件
add_executable(my_app
    src/main.cpp
    src/utils.cpp
)

# 添加库
add_library(my_lib STATIC
    src/math_utils.cpp
    src/string_utils.cpp
)

# 链接库到可执行文件
target_link_libraries(my_app PRIVATE my_lib)

# 设置头文件搜索路径
target_include_directories(my_lib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include
)

1.2 构建流程

Bash
# 标准的out-of-source构建流程
mkdir build && cd build
cmake ..                      # 配置(生成构建系统文件)
cmake --build .               # 编译
cmake --build . --target test # 运行测试
cmake --install .             # 安装

# 指定生成器
cmake -G "Ninja" ..           # 使用Ninja(推荐,更快)
cmake -G "Unix Makefiles" ..  # 使用Make

# 指定构建类型
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake -DCMAKE_BUILD_TYPE=Debug ..

# CMake预设(CMake 3.19+,推荐)
cmake --preset release
cmake --build --preset release

二、现代CMake实践

2.1 Target-Based 方法(现代CMake核心理念)

现代CMake的核心思想:一切都围绕target操作,不使用全局变量。

CMake
# ❌ 旧式CMake(不推荐)
include_directories(${CMAKE_SOURCE_DIR}/include)  # 全局头文件路径
add_definitions(-DDEBUG)                           # 全局宏定义
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")   # 全局编译选项

# ✅ 现代CMake(推荐)
add_library(my_lib src/my_lib.cpp)
target_include_directories(my_lib PUBLIC include)      # 针对target
target_compile_definitions(my_lib PRIVATE DEBUG)       # 针对target
target_compile_options(my_lib PRIVATE -Wall -Wextra)   # 针对target

2.2 PUBLIC / PRIVATE / INTERFACE

这三个关键字控制属性传播行为

关键字 含义 使用场景
PRIVATE 仅当前target使用 内部实现依赖
PUBLIC 当前target + 链接它的target 接口中暴露的依赖
INTERFACE 仅链接它的target使用 纯头文件库
CMake
# 示例:JSON解析库
add_library(json_parser
    src/json_parser.cpp
)

# PUBLIC:json_parser的头文件中用了nlohmann/json.hpp
# 所以链接json_parser的target也需要这个头文件路径
target_include_directories(json_parser PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/include     # 对外头文件
)
target_include_directories(json_parser PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src         # 内部头文件
)

# PRIVATE:只有json_parser自己需要链接zlib
target_link_libraries(json_parser PRIVATE ZLIB::ZLIB)

# PUBLIC:json_parser的接口暴露了Boost.Asio类型
target_link_libraries(json_parser PUBLIC Boost::asio)

# INTERFACE:纯头文件库
add_library(header_only INTERFACE)
target_include_directories(header_only INTERFACE include)
target_compile_features(header_only INTERFACE cxx_std_20)

2.3 Generator Expressions

生成器表达式在构建时求值(非配置时),用于条件配置:

CMake
add_library(my_lib src/my_lib.cpp)

# 根据构建类型设置不同编译选项
target_compile_options(my_lib PRIVATE
    $<$<CONFIG:Debug>:-O0 -g -fsanitize=address>
    $<$<CONFIG:Release>:-O3 -DNDEBUG>
)

# 根据编译器类型设置选项
target_compile_options(my_lib PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra -Wpedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
)

# 安装时区分BUILD_INTERFACE和INSTALL_INTERFACE
target_include_directories(my_lib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

三、依赖管理

3.1 find_package

CMake
# 查找系统安装的库
find_package(Threads REQUIRED)           # 线程库
find_package(Boost 1.75 REQUIRED COMPONENTS filesystem system)
find_package(OpenCV 4.5 REQUIRED)
find_package(Protobuf REQUIRED)

add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE
    Threads::Threads
    Boost::filesystem
    Boost::system
    ${OpenCV_LIBS}
    protobuf::libprotobuf
)

3.2 FetchContent(CMake 3.14+,推荐)

自动拉取并编译第三方源码,无需手动安装。

CMake
include(FetchContent)

# 拉取Google Test
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

# 拉取JSON库
FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.3
)

# 拉取spdlog日志库
FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG v1.13.0
)

# 一次性下载并可用
FetchContent_MakeAvailable(googletest json spdlog)

add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE
    nlohmann_json::nlohmann_json
    spdlog::spdlog
)

3.3 vcpkg / Conan 包管理器

vcpkg 集成

Bash
# 安装包
vcpkg install fmt spdlog boost-asio

# 使用vcpkg工具链文件配置CMake
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[vcpkg_root]/scripts/buildsystems/vcpkg.cmake
CMake
# CMakeLists.txt中直接find_package即可
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)

target_link_libraries(my_app PRIVATE fmt::fmt spdlog::spdlog)

Conan 集成

INI
# conanfile.txt
[requires]
fmt/10.2.0
spdlog/1.13.0
boost/1.84.0

[generators]
CMakeDeps
CMakeToolchain
Bash
conan install . --output-folder=build --build=missing
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake

四、编译选项管理

4.1 调试/发布配置

CMake
# 方式1:CMake变量
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG")

# 方式2:现代CMake target方式(推荐)
target_compile_options(my_app PRIVATE
    $<$<CONFIG:Debug>:-O0 -g>
    $<$<CONFIG:Release>:-O3>
)

target_compile_definitions(my_app PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
)

4.2 编译器警告

CMake
# 封装为函数,方便复用
function(set_project_warnings target_name)
    set(MSVC_WARNINGS /W4 /WX /permissive-)
    set(CLANG_WARNINGS
        -Wall -Wextra -Wpedantic
        -Wshadow -Wnon-virtual-dtor
        -Wold-style-cast -Wcast-align
        -Wunused -Woverloaded-virtual
        -Wconversion -Wsign-conversion
        -Wnull-dereference -Wdouble-promotion
        -Wformat=2
    )
    set(GCC_WARNINGS ${CLANG_WARNINGS}
        -Wmisleading-indentation
        -Wduplicated-cond
        -Wduplicated-branches
        -Wlogical-op
    )

    if(MSVC)
        set(PROJECT_WARNINGS ${MSVC_WARNINGS})
    elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
        set(PROJECT_WARNINGS ${CLANG_WARNINGS})
    elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        set(PROJECT_WARNINGS ${GCC_WARNINGS})
    endif()

    target_compile_options(${target_name} PRIVATE ${PROJECT_WARNINGS})
endfunction()

# 使用
set_project_warnings(my_app)

4.3 Sanitizer集成

CMake
option(ENABLE_SANITIZERS "Enable sanitizers" OFF)

if(ENABLE_SANITIZERS)
    # Address Sanitizer: 检测内存错误(越界、use-after-free等)
    set(SANITIZER_FLAGS "-fsanitize=address,undefined -fno-omit-frame-pointer")

    add_compile_options(${SANITIZER_FLAGS})
    add_link_options(${SANITIZER_FLAGS})
endif()

# 使用方式:
# cmake -B build -DENABLE_SANITIZERS=ON
# cmake --build build
# ./build/my_app  (运行时自动检测内存错误)

五、单元测试集成

5.1 Google Test

CMake
# CMakeLists.txt
include(FetchContent)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(my_tests
    test/test_math.cpp
    test/test_string.cpp
)

target_link_libraries(my_tests PRIVATE
    GTest::gtest_main
    my_lib
)

include(GoogleTest)
gtest_discover_tests(my_tests)
C++
// test/test_math.cpp
#include <gtest/gtest.h>
#include "math_utils.h"

TEST(MathUtilsTest, Addition) {
    EXPECT_EQ(add(2, 3), 5);
    EXPECT_EQ(add(-1, 1), 0);
    EXPECT_EQ(add(0, 0), 0);
}

TEST(MathUtilsTest, Division) {
    EXPECT_DOUBLE_EQ(divide(10.0, 3.0), 10.0 / 3.0);
    EXPECT_THROW(divide(1.0, 0.0), std::invalid_argument);
}

// 参数化测试
class FibonacciTest : public ::testing::TestWithParam<std::pair<int, int>> {};

TEST_P(FibonacciTest, ComputesCorrectly) {
    auto [input, expected] = GetParam();
    EXPECT_EQ(fibonacci(input), expected);
}

INSTANTIATE_TEST_SUITE_P(
    FibValues, FibonacciTest,
    ::testing::Values(
        std::make_pair(0, 0),
        std::make_pair(1, 1),
        std::make_pair(5, 5),
        std::make_pair(10, 55)
    )
);

5.2 Catch2

CMake
FetchContent_Declare(
    Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v3.5.2
)
FetchContent_MakeAvailable(Catch2)

add_executable(tests test/test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain my_lib)

include(CTest)
include(Catch)
catch_discover_tests(tests)
C++
// test/test_main.cpp
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "math_utils.h"

TEST_CASE("Math addition", "[math]") {
    REQUIRE(add(2, 3) == 5);

    SECTION("negative numbers") {
        REQUIRE(add(-1, -2) == -3);
    }

    SECTION("zero") {
        REQUIRE(add(0, 0) == 0);
    }
}

TEST_CASE("Division with tolerance", "[math]") {
    using Catch::Matchers::WithinRel;
    REQUIRE_THAT(divide(10.0, 3.0), WithinRel(3.3333, 0.001));
}

5.3 CTest 运行

Bash
cd build
ctest                          # 运行所有测试
ctest -V                       # 详细输出
ctest -R "MathUtils"           # 按名称过滤
ctest -j$(nproc)               # 并行运行
ctest --output-on-failure      # 失败时显示输出

六、交叉编译配置

CMake
# toolchain-aarch64.cmake(工具链文件)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)

# sysroot(可选)
set(CMAKE_SYSROOT /opt/aarch64-sysroot)
set(CMAKE_FIND_ROOT_PATH /opt/aarch64-sysroot)

# 查找策略
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  # 工具用宿主的
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)   # 库用目标平台的
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
Bash
# 使用工具链文件构建
cmake -B build-aarch64 -S . \
    -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-aarch64.cmake \
    -DCMAKE_BUILD_TYPE=Release

cmake --build build-aarch64

七、CI/CD集成

7.1 GitHub Actions

YAML
# .github/workflows/ci.yml
name: C++ CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        build_type: [Debug, Release]
        compiler:
          - { cc: gcc-13, cxx: g++-13 }
          - { cc: clang-17, cxx: clang++-17 }
        exclude:
          - os: windows-latest
            compiler: { cc: gcc-13, cxx: g++-13 }
          - os: macos-latest
            compiler: { cc: gcc-13, cxx: g++-13 }

    steps:
    - uses: actions/checkout@v4

    - name: Configure
      run: |
        cmake -B build \
          -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
          -DCMAKE_C_COMPILER=${{ matrix.compiler.cc }} \
          -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cxx }} \
          -DENABLE_TESTING=ON

    - name: Build
      run: cmake --build build --config ${{ matrix.build_type }} -j

    - name: Test
      run: ctest --test-dir build -C ${{ matrix.build_type }} --output-on-failure

  sanitize:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Configure with sanitizers
      run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_SANITIZERS=ON
    - name: Build
      run: cmake --build build -j
    - name: Test
      run: ctest --test-dir build --output-on-failure

7.2 GitLab CI

YAML
# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  CMAKE_BUILD_TYPE: Release

build:
  stage: build
  image: gcc:13
  script:
    - apt-get update && apt-get install -y cmake ninja-build
    - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=$CMAKE_BUILD_TYPE
    - cmake --build build
  artifacts:
    paths:
      - build/

test:
  stage: test
  image: gcc:13
  needs: [build]
  script:
    - cd build && ctest --output-on-failure

sanitizer_test:
  stage: test
  image: gcc:13
  script:
    - apt-get update && apt-get install -y cmake
    - cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_SANITIZERS=ON
    - cmake --build build
    - cd build && ctest --output-on-failure

八、智能指针与RAII模式总结

8.1 RAII原则

RAII(Resource Acquisition Is Initialization)——资源获取即初始化,是C++最重要的编程范式之一。核心思想:将资源的生命周期与对象的生命周期绑定。

C++
#include <iostream>
#include <fstream>
#include <mutex>
#include <memory>
#include <vector>
#include <chrono>
#include <string>
#include <stdexcept>

// RAII示例1:文件操作
void process_file(const std::string& filename) {
    std::ifstream file(filename);  // 构造时打开文件
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file");
    }

    std::string line;
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }
    // 离开作用域,析构函数自动关闭文件
    // 即使抛出异常也能正确关闭
}

// RAII示例2:锁管理
class ThreadSafeCounter {
    mutable std::mutex mtx;
    int count = 0;

public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);  // 构造时加锁
        ++count;
        // 离开作用域自动解锁,即使抛异常
    }

    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return count;
    }
};

// RAII示例3:自定义RAII类
class ScopedTimer {
    std::string name;
    std::chrono::time_point<std::chrono::high_resolution_clock> start;

public:
    ScopedTimer(std::string n) : name(std::move(n)),
        start(std::chrono::high_resolution_clock::now()) {}

    ~ScopedTimer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        std::cout << name << ": " << duration.count() << " us" << std::endl;
    }
};

void heavy_computation() {
    ScopedTimer timer("heavy_computation");  // 自动计时
    // ... 复杂计算 ...
    std::vector<int> v(1000000);
    for (int i = 0; i < 1000000; ++i) v[i] = i * i;
}

8.2 unique_ptr —— 独占所有权

C++
#include <iostream>
#include <memory>
#include <vector>

class Resource {
    std::string name;
public:
    Resource(std::string n) : name(std::move(n)) {
        std::cout << "Construct " << name << std::endl;
    }
    ~Resource() {
        std::cout << "Destroy " << name << std::endl;
    }
    void use() { std::cout << "Using " << name << std::endl; }
};

int main() {
    // 创建unique_ptr(推荐make_unique)
    auto p1 = std::make_unique<Resource>("A");
    p1->use();

    // 转移所有权(不能拷贝)
    auto p2 = std::move(p1);
    // p1现在为nullptr
    if (!p1) std::cout << "p1 is null" << std::endl;
    p2->use();

    // 自定义删除器
    auto file_deleter = [](FILE* f) {
        std::cout << "Closing file" << std::endl;
        if (f) fclose(f);
    };
    std::unique_ptr<FILE, decltype(file_deleter)> file(
        fopen("test.txt", "w"), file_deleter
    );

    // unique_ptr在容器中
    std::vector<std::unique_ptr<Resource>> resources;
    resources.push_back(std::make_unique<Resource>("R1"));
    resources.push_back(std::make_unique<Resource>("R2"));

    // 工厂模式返回unique_ptr
    // auto obj = create_object();  // 返回unique_ptr,所有权转移给调用者

    return 0;
}
// 离开作用域:自动析构所有资源

8.3 shared_ptr —— 共享所有权

C++
#include <iostream>
#include <memory>

class Node {
public:
    int value;
    std::shared_ptr<Node> next;  // shared_ptr 共享式智能指针,引用计数管理生命周期

    Node(int v) : value(v) { std::cout << "Node(" << v << ") created" << std::endl; }
    ~Node() { std::cout << "Node(" << value << ") destroyed" << std::endl; }
};

int main() {
    auto p1 = std::make_shared<Node>(1);
    std::cout << "use_count: " << p1.use_count() << std::endl;  // 1

    {
        auto p2 = p1;  // 共享所有权
        std::cout << "use_count: " << p1.use_count() << std::endl;  // 2

        auto p3 = p1;
        std::cout << "use_count: " << p1.use_count() << std::endl;  // 3
    }
    // p2, p3离开作用域,引用计数减少
    std::cout << "use_count: " << p1.use_count() << std::endl;  // 1

    return 0;
}
// p1离开作用域,引用计数为0,自动删除

8.4 weak_ptr —— 解决循环引用

C++
#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    // ❌ 如果用shared_ptr,A和B互相引用,永远不会释放
    // std::shared_ptr<A> a_ptr;

    // ✅ 用weak_ptr打破循环
    std::weak_ptr<A> a_ptr;
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a;  // weak_ptr不增加引用计数

    std::cout << "a use_count: " << a.use_count() << std::endl;  // 1(不是2!)
    std::cout << "b use_count: " << b.use_count() << std::endl;  // 2

    // 使用weak_ptr前要检查对象是否还存在
    if (auto locked = b->a_ptr.lock()) {
        std::cout << "A is still alive" << std::endl;
    }

    return 0;
}
// 正常析构:A destroyed, B destroyed

智能指针选型指南

场景 选择 理由
独占资源(默认选择) unique_ptr 零开销、语义清晰
共享资源 shared_ptr 引用计数自动管理
观察者/缓存 weak_ptr 不影响生命周期
工厂函数返回 unique_ptr 调用者获得所有权
树/图节点的子节点 unique_ptr 父拥有子
树/图节点的父指针 原始指针或weak_ptr 避免循环引用
跨线程共享 shared_ptr + atomic 引用计数线程安全

📋 面试要点:make_shared vs new——make_shared一次分配(控制块+对象),new两次分配;shared_ptr引用计数是原子操作,有一定开销;unique_ptr大小与原始指针相同(零开销抽象)。


九、完整CMake项目模板

9.1 目录结构

Text Only
my_project/
├── CMakeLists.txt              # 顶层CMake
├── CMakePresets.json            # CMake预设
├── cmake/
│   ├── CompilerWarnings.cmake   # 编译警告配置
│   └── Sanitizers.cmake         # Sanitizer配置
├── include/
│   └── my_project/
│       ├── math_utils.h
│       └── string_utils.h
├── src/
│   ├── CMakeLists.txt
│   ├── main.cpp
│   ├── math_utils.cpp
│   └── string_utils.cpp
├── test/
│   ├── CMakeLists.txt
│   ├── test_math.cpp
│   └── test_string.cpp
├── .clang-format
├── .clang-tidy
├── .gitignore
└── README.md

9.2 顶层 CMakeLists.txt

CMake
cmake_minimum_required(VERSION 3.20)

project(my_project
    VERSION 1.0.0
    DESCRIPTION "A modern C++ project template"
    LANGUAGES CXX
)

# 设置C++标准
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 生成compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 选项
option(ENABLE_TESTING "Enable tests" ON)
option(ENABLE_SANITIZERS "Enable sanitizers" OFF)

# 自定义CMake模块
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(CompilerWarnings)
include(Sanitizers)

# 主库
add_library(my_project_lib
    src/math_utils.cpp
    src/string_utils.cpp
)
target_include_directories(my_project_lib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
set_project_warnings(my_project_lib)

# 可执行文件
add_executable(my_project_app src/main.cpp)
target_link_libraries(my_project_app PRIVATE my_project_lib)

# 测试
if(ENABLE_TESTING)
    enable_testing()
    add_subdirectory(test)
endif()

9.3 test/CMakeLists.txt

CMake
include(FetchContent)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

add_executable(my_project_tests
    test_math.cpp
    test_string.cpp
)

target_link_libraries(my_project_tests PRIVATE
    GTest::gtest_main
    my_project_lib
)

include(GoogleTest)
gtest_discover_tests(my_project_tests)

9.4 CMakePresets.json

JSON
{
    "version": 6,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 20,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "base",
            "hidden": true,
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/${presetName}",
            "cacheVariables": {
                "CMAKE_CXX_STANDARD": "20",
                "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
            }
        },
        {
            "name": "debug",
            "inherits": "base",
            "displayName": "Debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug",
                "ENABLE_SANITIZERS": "ON"
            }
        },
        {
            "name": "release",
            "inherits": "base",
            "displayName": "Release",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "debug",
            "configurePreset": "debug"
        },
        {
            "name": "release",
            "configurePreset": "release"
        }
    ],
    "testPresets": [
        {
            "name": "debug",
            "configurePreset": "debug",
            "output": {
                "outputOnFailure": true
            }
        }
    ]
}

9.5 .clang-format

YAML
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
BreakBeforeBraces: Attach
PointerAlignment: Left
SortIncludes: true
IncludeBlocks: Regroup

9.6 .clang-tidy

YAML
Checks: >
    -*,
    bugprone-*,
    cert-*,
    clang-analyzer-*,
    cppcoreguidelines-*,
    modernize-*,
    performance-*,
    readability-*,
    -modernize-use-trailing-return-type,
    -readability-identifier-length

WarningsAsErrors: ''
HeaderFilterRegex: '.*'

十、C++工程最佳实践Checklist

代码质量

  • 使用C++20或更高标准
  • 启用严格编译警告(-Wall -Wextra -Wpedantic -Werror
  • 配置clang-tidy静态分析
  • 配置clang-format统一代码风格
  • CI中集成Address Sanitizer和UB Sanitizer

内存管理

  • 遵循RAII原则管理所有资源
  • 默认使用unique_ptr,需要共享时用shared_ptr
  • 不使用裸new/delete
  • 注意shared_ptr循环引用,用weak_ptr打破
  • 移动语义:为拥有资源的类实现移动构造/移动赋值

构建系统

  • 使用现代CMake(target-based approach)
  • 使用CMakePresets.json管理构建配置
  • 使用FetchContent或包管理器管理依赖
  • 生成compile_commands.json供IDE使用
  • 支持多平台构建(Linux/macOS/Windows)

测试

  • 集成Google Test或Catch2
  • 使用CTest管理测试
  • 单元测试覆盖核心逻辑
  • CI中自动运行测试

CI/CD

  • GitHub Actions/GitLab CI配置完整
  • 多编译器交叉验证(GCC/Clang/MSVC)
  • Debug + Release双配置测试
  • Sanitizer测试job
  • 代码质量检查(clang-tidy、clang-format check)

项目组织

  • 清晰的目录结构(src/include/test/cmake)
  • 头文件放在include/project_name/
  • 使用命名空间避免名称冲突
  • README包含构建说明和项目简介
  • .gitignore排除构建产物

✏️ 练习

练习1:从零搭建CMake项目

按照本章的项目模板,创建一个完整的C++20项目,包含一个矩阵运算库(Matrix类,支持加法、乘法、转置),配上Google Test单元测试,确保能编译通过并且所有测试通过。

练习2:FetchContent实践

使用FetchContent引入spdlog日志库和nlohmann/json库,编写一个读取JSON配置文件并输出格式化日志的小程序。

练习3:CI/CD配置

为练习1的项目编写GitHub Actions配置,要求:① 在Ubuntu/macOS/Windows三平台测试 ② Debug和Release双配置 ③ 集成Address Sanitizer。

练习4:智能指针实践

实现一个简单的对象池(ObjectPool),内部用unique_ptr管理对象,get()返回带自定义删除器的unique_ptr使对象在释放时自动回到池中而非被销毁。

练习5:RAII设计

设计一个DatabaseConnection类,使用RAII管理数据库连接的生命周期。要求:构造时连接,析构时断开,支持移动语义但禁止拷贝,配合unique_ptr使用。


📋 面试要点

高频问题

  1. unique_ptr vs shared_ptr vs weak_ptr? unique_ptr独占零开销,shared_ptr引用计数共享,weak_ptr观察不增加引用计数。
  2. make_shared为什么优于new? 一次内存分配(对象+控制块一起),减少碎片和分配开销;异常安全。
  3. RAII是什么?为什么重要? 资源获取即初始化,利用析构函数保证即使异常发生也能正确释放资源。
  4. CMake中PUBLIC/PRIVATE/INTERFACE的区别? PUBLIC:自己和依赖方都要;PRIVATE:只有自己;INTERFACE:只有依赖方。
  5. 如何管理C++项目的第三方依赖? FetchContent(源码级)、vcpkg/Conan(二进制包管理)、find_package(系统安装)。
  6. shared_ptr的线程安全性? 引用计数操作是原子的(线程安全),但被管理对象的访问不是线程安全的。
  7. 循环引用如何解决? 用weak_ptr打破循环,或重新设计所有权关系。
  8. Address Sanitizer能检测什么? 堆缓冲区溢出、栈缓冲区溢出、use-after-free、use-after-return、内存泄漏等。

💡 本章小结

本章覆盖了C++工程实践的核心内容:CMake构建系统、依赖管理、测试框架、CI/CD、以及智能指针与RAII。这些知识是从"写得出代码"到"做得出工程"的关键跨越。在AI相关岗位面试中,展示出良好的工程素养(规范的项目结构、完善的测试、CI/CD经验)往往比单纯的算法能力更能体现你的综合实力。


🔗 下一章

并发编程 - 学习C++多线程与并发编程。