CMake与工程实践¶
🎯 学习目标¶
完成本章学习后,你将能够: - 掌握CMake的核心语法与现代CMake的目标导向(target-based)实践 - 理解并使用PUBLIC/PRIVATE/INTERFACE控制依赖传播 - 熟练使用find_package、FetchContent管理第三方依赖 - 集成Google Test/Catch2进行单元测试 - 配置CI/CD流水线实现自动化构建与测试 - 回顾智能指针与RAII模式,掌握现代C++内存管理最佳实践 - 能够从零搭建一个规范的C++工程项目
上图展示了 configure/build/test/install 到 CI/CD 的完整链路,对应本章工程实践主线。
一、CMake基础¶
1.1 CMakeLists.txt 基本结构¶
# 指定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 构建流程¶
# 标准的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(不推荐)
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使用 | 纯头文件库 |
# 示例: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¶
生成器表达式在构建时求值(非配置时),用于条件配置:
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¶
# 查找系统安装的库
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+,推荐)¶
自动拉取并编译第三方源码,无需手动安装。
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 集成¶
# 安装包
vcpkg install fmt spdlog boost-asio
# 使用vcpkg工具链文件配置CMake
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[vcpkg_root]/scripts/buildsystems/vcpkg.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 集成¶
# conanfile.txt
[requires]
fmt/10.2.0
spdlog/1.13.0
boost/1.84.0
[generators]
CMakeDeps
CMakeToolchain
conan install . --output-folder=build --build=missing
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
四、编译选项管理¶
4.1 调试/发布配置¶
# 方式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 编译器警告¶
# 封装为函数,方便复用
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集成¶
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¶
# 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)
// 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¶
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)
// 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 运行¶
cd build
ctest # 运行所有测试
ctest -V # 详细输出
ctest -R "MathUtils" # 按名称过滤
ctest -j$(nproc) # 并行运行
ctest --output-on-failure # 失败时显示输出
六、交叉编译配置¶
# 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)
# 使用工具链文件构建
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¶
# .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¶
# .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++最重要的编程范式之一。核心思想:将资源的生命周期与对象的生命周期绑定。
#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 —— 独占所有权¶
#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 —— 共享所有权¶
#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 —— 解决循环引用¶
#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 目录结构¶
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_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¶
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¶
{
"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¶
BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
BreakBeforeBraces: Attach
PointerAlignment: Left
SortIncludes: true
IncludeBlocks: Regroup
9.6 .clang-tidy¶
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使用。
📋 面试要点¶
高频问题¶
- unique_ptr vs shared_ptr vs weak_ptr? unique_ptr独占零开销,shared_ptr引用计数共享,weak_ptr观察不增加引用计数。
- make_shared为什么优于new? 一次内存分配(对象+控制块一起),减少碎片和分配开销;异常安全。
- RAII是什么?为什么重要? 资源获取即初始化,利用析构函数保证即使异常发生也能正确释放资源。
- CMake中PUBLIC/PRIVATE/INTERFACE的区别? PUBLIC:自己和依赖方都要;PRIVATE:只有自己;INTERFACE:只有依赖方。
- 如何管理C++项目的第三方依赖? FetchContent(源码级)、vcpkg/Conan(二进制包管理)、find_package(系统安装)。
- shared_ptr的线程安全性? 引用计数操作是原子的(线程安全),但被管理对象的访问不是线程安全的。
- 循环引用如何解决? 用weak_ptr打破循环,或重新设计所有权关系。
- Address Sanitizer能检测什么? 堆缓冲区溢出、栈缓冲区溢出、use-after-free、use-after-return、内存泄漏等。
💡 本章小结¶
本章覆盖了C++工程实践的核心内容:CMake构建系统、依赖管理、测试框架、CI/CD、以及智能指针与RAII。这些知识是从"写得出代码"到"做得出工程"的关键跨越。在AI相关岗位面试中,展示出良好的工程素养(规范的项目结构、完善的测试、CI/CD经验)往往比单纯的算法能力更能体现你的综合实力。
🔗 下一章¶
并发编程 - 学习C++多线程与并发编程。