第40章 软件工程实践

第40章 软件工程实践

欢迎来到软件工程的江湖!如果说前面的章节教你的是"如何用C++说话",那这一章就是教你"如何在团队中优雅地合作、如何让代码长期健康活下去"的艺术。

软件工程不是"写代码"那么简单。它是需求分析、架构设计、编码实现、测试验证、持续集成、部署运维的全流程。一行代码写出来不难,难的是三年后还有人会改、还能改、还敢改。

这一章我们就来聊聊那些让C++项目从"能跑就行"走向"专业可维护"的关键实践。系好安全带,幽默与干货齐飞!


40.1 测试实践与测试框架

为什么你的代码需要测试

先讲个真实的故事:某公司有个核心算法,被一个工程师优化后性能提升了40%。代码review通过了,测试也跑了——单测全过。然后上线。第一周用户发现计算结果偶尔飘了10%。第三周回滚了。

问题出在哪?单测全过,但没人测过那个"偶尔飘10%“的边界条件。

测试不是为了证明代码是对的,而是为了发现代码什么时候会错。 这个心态不转变,你的测试就是在给自己写"安慰剂”。

C++的测试框架江湖上主要有几位大佬:

框架特点适合场景
Google Test (gtest/gMock)功能最全,跨平台,业界标配中大型项目
Catch2单头文件,写法超简洁中小型项目、快速原型
Doctest性能最好,编译飞快对编译时间敏感的项目
Boost.TestBoost家族成员,功能丰富使用Boost的项目

Google Test快速上手

Google Test是C++测试领域的老大哥,我们重点讲:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// calc.cpp - 一个简单的计算器
#include "calc.h"

int add(int a, int b) {
    return a + b; // 正常人理解的加法
}

int safeDivide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero");
    }
    return a / b;
}
1
2
3
4
5
// calc.h
#pragma once

int add(int a, int b);
int safeDivide(int a, int b);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// calc_test.cpp
#include <gtest/gtest.h>
#include "calc.h"

// 测试加法——最基础的正确性验证
TEST(AddTest, PositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);        // 期望相等,不相等就报错
    EXPECT_EQ(add(0, 0), 0);
    EXPECT_EQ(add(-1, 1), 0);
}

TEST(AddTest, NegativeNumbers) {
    EXPECT_EQ(add(-5, -3), -8);
    EXPECT_EQ(add(-100, 200), 100);
}

// 测试除法——关注异常路径
TEST(SafeDivideTest, NormalCases) {
    EXPECT_EQ(safeDivide(10, 2), 5);
    EXPECT_EQ(safeDivide(7, 2), 3);     // 整数除法,向下取整
    EXPECT_EQ(safeDivide(-6, 2), -3);
}

TEST(SafeDivideTest, DivisionByZero) {
    // EXPECT_THROW: 期望这段代码抛出指定异常
    EXPECT_THROW(safeDivide(1, 0), std::runtime_error);
}

// 测试套件:把相关测试归为一组
class CalculatorTest : public ::testing::Test {
protected:
    void SetUp() override {
        // 每个测试前都会执行这里
        // 比如初始化计算器对象
    }
    void TearDown() override {
        // 每个测试后都会执行
    }
};

TEST_F(CalculatorTest, CombinedOperations) {
    // 这个测试用上了SetUp/TearDown
    int result = add(3, 4);
    EXPECT_GT(result, 0); // 更大
    EXPECT_LT(result, 100);
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

编译运行:

1
2
g++ -std=c++17 -o calc_test calc.cpp calc_test.cpp -lgtest -lgtest_main -pthread
./calc_test

输出类似:

[==========] Running 5 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 5 tests from AddTest, SafeDivideTest
[ RUN      ] AddTest.PositiveNumbers
[       OK ] AddTest.PositiveNumbers (0 ms)
[ RUN      ] AddTest.NegativeNumbers
[       OK ] AddTest.NegativeNumbers (0 ms)
[ RUN      ] SafeDivideTest.NormalCases
[       OK ] SafeDivideTest.NormalCases (0 ms)
[ RUN      ] SafeDivideTest.DivisionByZero
[       OK ] SafeDivideTest.DivisionByZero (0 ms)
[ RUN      ] CalculatorTest.CombinedOperations
[       OK ] CalculatorTest.CombinedOperations (0 ms)
[----------] 5 tests from 2 test suites (5 total)
[==========] 5 tests from 2 test suites (5 total, 0 disabled).
[  PASSED  ] 5 tests.

单元测试的最佳实践

测试也有"好吃懒做"的写法,和"精明强干"的写法。区别在哪?

❌ 糟糕的测试(自掘坟墓):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
TEST(BadTest, TestSomething) {
    // 测试私有实现细节——一旦重构就全部失败
    EXPECT_EQ(obj.getInternalState().flags, 0xABCD);

    // 只测试成功路径,不测边界
    EXPECT_EQ(calculate(5, 2), 2.5); // 正常情况

    // 硬编码预期值,没有可读性
    EXPECT_EQ(complexFunc(12345), 99987);
}

✅ 优秀的测试(长期主义):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 清晰的命名:类名+测试场景+预期行为
TEST(ValidatorTest, ValidEmail_PassesValidation) {
    Validator v;
    EXPECT_TRUE(v.isValid("user@example.com"));
}

TEST(ValidatorTest, InvalidEmail_MissingAtSymbol_ReturnsFalse) {
    Validator v;
    EXPECT_FALSE(v.isValid("userexample.com")); // 没有@符号
}

TEST(ValidatorTest, InvalidEmail_MissingDomain_ReturnsFalse) {
    Validator v;
    EXPECT_FALSE(v.isValid("user@")); // 没有域名
}

// 参数化测试:同一套逻辑测多个输入
class IsPrimeParamTest : public testing::TestWithParam<int> {};

TEST_P(IsPrimeParamTest, PrimesAndComposites) {
    int n = GetParam();
    bool result = isPrime(n);
    // 逻辑正确性由测试参数保证
}

INSTANTIATE_TEST_SUITE_P(
    PrimeNumbers,
    IsPrimeParamTest,
    testing::Values(2, 3, 5, 7, 11, 13, 17, 19, 23)
);

INSTANTIATE_TEST_SUITE_P(
    CompositeNumbers,
    IsPrimeParamTest,
    testing::Values(4, 6, 8, 9, 10, 12)
);

黄金法则:

  • Arrange-Act-Assert(准备-执行-断言) 三段式结构,每个测试只测一件事
  • 测试之间互相独立,不依赖顺序
  • 测试的是行为,不是实现细节
  • 失败的测试要能本地复现,不能是随机失败

40.2 持续集成与持续部署(CI/CD)

什么是CI/CD

想象一下:你写代码提交了,然后"手动"编译、手动跑测试、手动部署到服务器。结果某天你忘了跑测试就上线了,用户发现了bug,你背锅。

CI/CD就是让你从这种"人肉流水线"中解放出来。

  • CI(Continuous Integration,持续集成):每次代码提交(push/PR)自动触发构建和测试,团队成员能快速看到"我的代码有没有搞坏什么"
  • CD(Continuous Delivery/Deployment,持续交付/部署):通过自动化把通过测试的代码自动部署到各环境

C++项目的CI通常在Linux/macOS/Windows多平台上并行跑,因为编译器版本差异可能带来问题。

GitHub Actions实现C++ CI

GitHub Actions是GitHub内置的CI工具,免费额度足够个人项目用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# .github/workflows/cpp-ci.yml
name: C++ CI

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

jobs:
  build-and-test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        compiler: [gcc, clang, msvc]
        exclude:
          - os: windows-latest
            compiler: clang  # Windows上不单独装clang(已有)
          - os: macos-latest
            compiler: msvc   # macOS上没有MSVC

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install dependencies (Ubuntu)
        if: matrix.os == 'ubuntu-latest'
        run: |
          sudo apt-get update
          sudo apt-get install -y cmake build-essential

      - name: Configure CMake
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release

      - name: Build
        run: cmake --build build --parallel

      - name: Run tests
        run: |
          cd build
          ctest --output-on-failure

      - name: Static Analysis (仅Ubuntu + GCC)
        if: matrix.os == 'ubuntu-latest' && matrix.compiler == 'gcc'
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Debug
          cmake --build build
          # 顺便跑一下静态分析工具
          find . -name "*.cpp" -exec echo "=== {} ===" \; -exec cppcheck --enable=all --std=c++17 {} \;

自动化测试金字塔

你的测试也不是越多越好,要有个合理的测试金字塔

           /\
          /  \       <- E2E测试 (少而精,只测核心流程)
         /----\        模拟真实用户操作,成本最高
        /      \
       /--------\    <- 集成测试 (测模块间协作)
      /          \     比如:两个类一起工作正常吗?
     /------------\
    /              \ <- 单元测试 (多,覆盖广)
   /________________\   测每个函数/类的行为

在C++中推荐实践:

  • 单元测试:使用Google Test/Catch2,覆盖率目标60-80%
  • 集成测试:用CMake的CTest框架
  • 静态分析:每次PR跑clang-tidycppcheck
  • 性能测试:集成到CI中,每次构建有基准测试报告

40.3 代码质量与度量

代码质量不是玄学

“代码质量"听起来很虚,但它是可以通过具体指标量化的。量化不是为了内卷,而是让问题可见

常见指标:

指标含义推荐范围
圈复杂度(Cyclomatic Complexity)代码分支数量的度量,越高越复杂≤10 最好,10-20 可接受,>20 危险
代码覆盖率(Code Coverage)多少行代码被测试跑了核心业务>80%
代码重复率(Duplication Rate)重复代码在总代码中的比例<5% 最好,越低越好
Halstead指标程序长度、工作量的估算相对比较用
依赖深度模块间依赖的层次深度越浅越好

用clang-tidy做静态分析

clang-tidy是LLVM项目自带的C++静态分析工具,能发现常见的代码味道(code smell):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 安装
# Ubuntu: sudo apt install clang-tidy
# macOS: brew install llvm

# 分析单个文件
clang-tidy source.cpp -- -std=c++17

# 分析整个CMake项目并生成报告
mkdir build && cd build
cmake ..
scan-build -o scan_report cmake --build .  # scan-build会注入clang static analyzer到构建过程

常见的clang-tidy检查项:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# .clang-tidy 配置文件
Checks: >
  -*,
  readability-*,
  performance-*,
  modernize-*,
  bugprone-*,
  clang-analyzer-*,
  cppcoreguidelines-*,

CheckOptions:
  - key: readabilityIdentifierLength.MinLength
    value: 3
  - key: cppcoreguidelines-specialMemberFunctions.AllowMissingMoveOperations
    value: '1'

在CMake中启用clang-tidy:

1
2
3
4
5
6
7
8
9
# CMakeLists.txt
include(GoogleTest)
enable_testing()

# 如果有clang-tidy就启用
find_program(CLANG_TIDY clang-tidy)
if(CLANG_TIDY)
    set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY} -checks=-*,readability-*,performance-*,modernize-*,-modernize-use-trailing-return-type)
endif()

代码度量工具

lcov + gcov(覆盖率):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 编译时加 coverage 标志
g++ -std=c++17 -fprofile-arcs -ftest-coverage -o myapp main.cpp

# 运行程序生成 .gcda 文件
./myapp

# 生成覆盖率报告
gcov main.cpp          # 生成 .gcov 文件
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_html

# 浏览器打开 coverage_html/index.html 看可视化报告

cloc(代码行数统计):

1
2
3
4
5
6
7
# 安装: sudo apt install cloc
cloc src/ --by-file --xml --out=report.xml

# 输出示例:
# Language          files        blank      comment         code
# C++                   12         843         1204         4821
# C/C++ Header           8         312          456         1024

40.4 配置管理与环境管理

为什么要管理配置

一个C++项目可能有多个环境:开发环境、测试环境、预生产环境、生产环境。每个环境的数据库地址、API密钥、日志级别可能都不一样。

如果这些配置都硬编码在代码里,那换环境就要改代码——灾难的开始。

配置管理的目标:代码与配置分离。

使用INI/JSON/YAML配置文件

YAML格式(最易读):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# config.yaml - 应用配置
database:
  host: localhost
  port: 5432
  name: myapp_dev
  username: dev_user
  password: ${DB_PASSWORD}  # 从环境变量读取

app:
  log_level: debug        # 开发环境开debug
  max_connections: 10
  feature_flags:
    new_ui: true
    experimental: false

server:
  host: 0.0.0.0
  port: 8080

读取YAML配置(使用yaml-cpp):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// config_manager.cpp
#include <iostream>
#include <fstream>
#include <yaml-cpp/yaml.h>
#include <map>
#include <optional>

class ConfigManager {
public:
    explicit ConfigManager(const std::string& config_path) {
        load(config_path);
    }

    // 获取字符串配置
    std::string getString(const std::string& path, const std::string& default_val = "") {
        try {
            return node_[path].as<std::string>();
        } catch (...) {
            return default_val;
        }
    }

    // 获取整数配置
    int getInt(const std::string& path, int default_val = 0) {
        try {
            return node_[path].as<int>();
        } catch (...) {
            return default_val;
        }
    }

    // 获取布尔配置
    bool getBool(const std::string& path, bool default_val = false) {
        try {
            return node_[path].as<bool>();
        } catch (...) {
            return default_val;
        }
    }

    // 检查key是否存在
    bool hasKey(const std::string& path) {
        try {
            node_[path];
            return true;
        } catch (...) {
            return false;
        }
    }

private:
    YAML::Node node_;

    void load(const std::string& path) {
        try {
            node_ = YAML::LoadFile(path);
        } catch (const std::exception& e) {
            std::cerr << "Config load failed: " << e.what() << std::endl;
            throw;
        }
    }
};

int main() {
    ConfigManager config("config.yaml");

    std::cout << "Database: " << config.getString("database.host") << ":"
              << config.getInt("database.port") << std::endl;

    std::cout << "Log level: " << config.getString("app.log_level") << std::endl;

    // 也可以获取嵌套结构
    int max_conn = config.getInt("app.max_connections", 100);
    std::cout << "Max connections: " << max_conn << std::endl;

    return 0;
}

环境变量模式

敏感信息(密码、API Key)绝不能写在配置文件里——要用环境变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 从环境变量读取敏感配置
#include <cstdlib>
#include <iostream>
#include <stdexcept>

class EnvConfig {
public:
    static std::string require(const char* key) {
        const char* val = std::getenv(key);
        if (!val) {
            throw std::runtime_error(
                std::string("Required environment variable not set: ") + key
            );
        }
        return val;
    }

    static std::string optional(const char* key, const std::string& default_val = "") {
        const char* val = std::getenv(key);
        return val ? val : default_val;
    }
};

// 使用
int main() {
    std::string db_password = EnvConfig::require("DB_PASSWORD");
    std::string log_dir = EnvConfig::optional("LOG_DIR", "/var/log/myapp");

    std::cout << "Using DB password: " << (db_password.empty() ? "[empty]" : "***") << std::endl;
    std::cout << "Log directory: " << log_dir << std::endl;
}

vcpkg管理依赖

C++的依赖管理一直是痛点。vcpkg是微软出品的跨平台C++包管理器,帮你把第三方库的安装、配置、链接一条龙搞定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 安装vcpkg
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh  # Linux/macOS
.\bootstrap-vcpkg.bat # Windows

# 搜索包
./vcpkg search yaml-cpp

# 安装包(安装到当前目录的vcpkg_installed/)
./vcpkg install yaml-cpp:x64-linux
./vcpkg install gtest fmt spdlog

# 集成到CMake(CMake 3.19+)
# 在CMakeLists.txt里:
#   find_package(yaml-cpp CONFIG REQUIRED)
#   target_link_libraries(myapp PRIVATE yaml-cpp::yaml-cpp)

CMake中使用vcpkg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(MyApp)

# vcpkg集成
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
    CACHE STRING "Vcpkg toolchain")

find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(yaml-cpp CONFIG REQUIRED)

add_executable(myapp main.cpp)

target_link_libraries(myapp PRIVATE
    fmt::fmt
    spdlog::spdlog
    yaml-cpp::yaml-cpp
)

40.5 版本控制与分支策略

Git工作流:C++团队的版本控制艺术

Git是C++项目的标配版本控制系统。但"会用git add和git commit"和"用好git"之间,差了一整个银河系。

常用分支策略对比

1. GitFlow(重型,适合发布周期长的项目):

main  ───●────────────────●────────────● (生产版本)
           \            /            /
develop ────●───●──●───●───●──●──●────●──● (日常开发)
               /  /  /              /
feature/x ───●──●──●               (新功能)
               /  /
hotfix/y ──────────●──────── (紧急修复)

优点:清晰,发布管理规范
缺点:分支太多,小团队太重

2. GitHub Flow(轻量,适合持续部署的互联网项目):

main ─────────────────────●────●────● (永远可部署)
         ↖PR     ↖PR    ↖PR
    feature/x ●─●─●    feature/y ●─●

优点:简单,持续部署友好
缺点:不适合多版本并行维护

3. Trunk-Based Development(极简,每个人的代码尽可能快地合并到trunk):

1
2
3
4
5
6
7
# 每个人每天至少向main/master合并一次
git checkout -b feature/tiny-feature
# 写代码
git commit -m "fix: handle null pointer in parser"
git push
# 如果功能太大,用feature flag隐藏半成品
# 简言之:feature flag就是代码里的"电源开关",功能做完了再合闸送电

Git钩子:自动化守卫

Git钩子是在特定Git事件前后自动运行的脚本,可以帮你做代码质量把控:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 安装预提交钩子(每次commit前自动运行)
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash
set -e

echo "Running pre-commit checks..."

# 1. 代码格式化检查(如果使用了clang-format)
if command -v clang-format &> /dev/null; then
    FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cpp$\|\.h$' | grep -v build/)
    if [ -n "$FILES" ]; then
        # 检查是否有未格式化的代码
        for file in $FILES; do
            if ! clang-format --style=file "$file" | diff -q "$file" - > /dev/null 2>&1; then
                echo "ERROR: $file is not formatted. Run 'clang-format -i $file' to fix."
                exit 1
            fi
        done
        echo "✓ Code format check passed"
    fi
fi

# 2. 运行快速测试(不要跑完整的测试套件,太慢了)
if [ -f build/run_quick_tests ]; then
    echo "Running quick tests..."
    ./build/run_quick_tests
    echo "✓ Quick tests passed"
fi

echo "✓ Pre-commit checks passed"
EOF
chmod +x .git/hooks/pre-commit

.gitignore模板(C++项目)

C++项目的.gitignore如果不写好,会把编译产物、二进制文件、vcpkg安装目录全部提交进去——噩梦的开始:

# C++项目.gitignore模板

# 编译产物
*.o
*.obj
*.exe
*.dll
*.so
*.dylib
*.a
*.lib
*.out

# 构建目录
build/
cmake-build-*/
cmake_build/
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json

# vcpkg
vcpkg_installed/
vcpkg/

# 依赖目录
third_party/
extern/
deps/

# IDE
CMakeLists.txt.user
*.suo
*.user
*.userosscache
*.sln.docstates
.vs/
.idea/
*.swp
*.swo
*~
.DS_Store

# 测试覆盖率报告
*.gcda
*.gcno
*.gcov
coverage/
*.info

# 日志文件
*.log

40.6 文档化与注释规范

为什么文档比代码更重要(有时候)

代码是给机器看的,文档是给未来的你和你的队友看的。三个月后再看自己的代码,你不写文档就会问自己:“这谁写的?写的是人话吗?”

C++文档工具链:

工具用途特点
DoxygenAPI文档生成工业标准,支持C++
Sphinx + Breathe项目文档网站可以整合Doxygen输出
Markdown + Doxygen轻量级文档简单项目首选

Doxygen注释规范

Doxygen支持多种注释风格,推荐Qt风格(在C++中非常流行):

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
 * @file parser.h
 * @brief JSON解析器核心类
 * @author Your Name
 * @version 1.0
 *
 * 本文件提供JSON解析器的完整实现,支持标准JSON格式,
 * 包括嵌套对象、数组、字符串转义等。
 */

#ifndef PARSER_H
#define PARSER_H

#include <string>
#include <map>
#include <variant>
#include <vector>
#include <stdexcept>
#include <optional>

namespace myapp {

/**
 * @brief JSON值类型
 *
 * 使用std::variant实现类型安全的JSON值表示
 */
using JsonValue = std::variant<
    std::nullptr_t,
    bool,
    int64_t,
    double,
    std::string,
    std::vector<JsonValue>,               // JSON数组
    std::map<std::string, JsonValue>      // JSON对象
>;

/**
 * @brief JSON解析错误异常
 */
class JsonParseException : public std::runtime_error {
public:
    explicit JsonParseException(const std::string& msg, size_t line, size_t col)
        : std::runtime_error(msg + " at line " + std::to_string(line)
                             + ", col " + std::to_string(col)),
          line_(line), col_(col) {}

    size_t line() const { return line_; }
    size_t col() const { return col_; }

private:
    size_t line_;
    size_t col_;
};

/**
 * @brief JSON解析器
 *
 * 负责将JSON字符串解析为内存中的JsonValue对象树。
 * 支持标准的JSON语法,包括UTF-8编码。
 *
 * @code
 * JsonParser parser;
 * auto value = parser.parse(R"({"name": "Alice", "age": 30})");
 * std::cout << std::get<std::string>(value["name"]) << std::endl; // Alice
 * @endcode
 *
 * ### 使用示例
 *
 * @code
 * // 示例1: 解析简单JSON
 * JsonParser p;
 * auto v = p.parse("{\"key\": \"value\"}");
 *
 * // 示例2: 处理解析错误
 * try {
 *     p.parse("{invalid json}");
 * } catch (const JsonParseException& e) {
 *     std::cerr << "Parse error: " << e.what() << std::endl;
 * }
 * @endcode
 */
class JsonParser {
public:
    /**
     * @brief 构造函数
     * @param allow_comments 是否允许JSON中包含C++风格注释(默认false)
     */
    explicit JsonParser(bool allow_comments = false);

    /**
     * @brief 解析JSON字符串
     * @param json_str 要解析的JSON字符串
     * @return 解析后的JsonValue对象树
     * @throws JsonParseException 解析失败时抛出
     */
    JsonValue parse(const std::string& json_str);

    /**
     * @brief 解析JSON文件
     * @param filename 文件路径
     * @return 解析后的JsonValue对象树
     * @throws JsonParseException 解析失败时抛出
     * @throws std::ifstream::failure 文件读取失败时抛出
     */
    JsonValue parseFile(const std::string& filename);

    /**
     * @brief 将JsonValue转换为格式化的JSON字符串
     * @param value 要转换的值
     * @param indent 缩进空格数(默认2,0表示不缩进)
     * @return 格式化后的JSON字符串
     */
    static std::string stringify(const JsonValue& value, int indent = 2);

    // 常用查询接口
    /**
     * @brief 获取对象中的字段
     * @param obj JSON对象(必须是对象类型)
     * @param key 字段名
     * @return 字段值,如果不存在返回std::nullopt
     */
    static std::optional<JsonValue> get(
        const JsonValue& obj,
        const std::string& key
    );

private:
    bool allow_comments_;
    std::string input_;
    size_t pos_ = 0;

    JsonValue parseValue();
    JsonValue parseObject();
    JsonValue parseArray();
    std::string parseString();
    JsonValue parseNumber();
    void skipWhitespace();
    char peek();
    char consume();

    [[nodiscard]] size_t line() const;
    [[nodiscard]] size_t column() const;
};

} // namespace myapp

#endif // PARSER_H

生成HTML文档:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 安装doxygen
# Ubuntu: sudo apt install doxygen
# macOS: brew install doxygen

# 生成Doxygen配置文件
doxygen -g Doxyfile

# 编辑Doxyfile关键配置
# PROJECT_NAME = "My JSON Library"
# INPUT = src/
# GENERATE_HTML = YES
# HTML_OUTPUT = docs
# RECURSIVE = YES

# 生成文档
doxygen Doxyfile

# 浏览器打开
# open docs/html/index.html  # macOS
# xdg-open docs/html/index.html  # Linux

注释的"度”

注释多了是噪音,注释少了是谜语。这个"度"怎么把握?

场景推荐做法
函数/类接口必须写清楚参数、返回值、异常、用法示例
复杂算法写清算法思路,不要写"代码翻译版"注释
业务逻辑用注释解释"为什么"而不是"是什么"
简单代码不要加注释——i++不需要说"这是自增"
临时方案加上// TODO: 临时方案,上线后要重构
修复Bug// BUG-FIX: Issue #123 - 防止空指针

❌ 过度注释(废话连篇):

1
2
3
4
5
6
7
8
// 将count加1
count++;

// 如果name不为空
if (name != "") {
    // 打印name
    std::cout << name << std::endl;
}

✅ 适度注释(直击要点):

1
2
3
4
5
6
7
// 归零:避免浮点数累加误差,100次累加后重新基准化
count++;

// 发送验证码:URL编码在网关层处理,这里直接透传原始字符串
if (!pendingVerification.empty()) {
    sendCode(pendingVerification);
}

40.7 性能基准测试

为什么你需要基准测试

你的代码"快不快"不能靠感觉,要靠数据。优化前跑一次基准测试,优化后再跑一次,数据对比才是硬道理。

在C++中做基准测试的主流工具:

工具特点适用场景
Google Benchmark (benchmark.h)Google Test同门,精度高标准基准测试
Catch2 + Benchmark一个框架搞定测试和基准小项目
std::chrono(手写)零依赖,简单场景快速验证

Google Benchmark实战

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// benchmark_example.cpp
#include <benchmark/benchmark.h>
#include <vector>
#include <algorithm>
#include <random>

// 辅助函数:生成随机数据
std::vector<int> generateRandomData(size_t n) {
    std::vector<int> v(n);
    std::iota(v.begin(), v.end(), 0);
    std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});
    return v;
}

// 基准测试:std::sort vs std::stable_sort
static void BM_SortStd(benchmark::State& state) {
    auto data = generateRandomData(state.range(0));

    for (auto _ : state) {
        auto v = data; // 每次都拷贝一份,不污染原始数据
        std::sort(v.begin(), v.end());
        // 用ClobberMemory阻止编译器优化掉整个循环
        benchmark::ClobberMemory();
    }

    // 设置复杂度参数,框架会自动分析并报告O(N log N)
    state.SetComplexityN(state.range(0));
}

BENCHMARK(BM_SortStd)
    ->RangeMultiplier(2)         // 参数倍增
    ->Range(1<<10, 1<<20)         // 1K到1M
    ->Complexity(benchmark::oNLogN)
    ->Unit(benchmark::kMillisecond);

static void BM_SortStable(benchmark::State& state) {
    auto data = generateRandomData(state.range(0));

    for (auto _ : state) {
        auto v = data;
        std::stable_sort(v.begin(), v.end());
        benchmark::ClobberMemory();
    }
    state.SetComplexityN(state.range(0));
}

BENCHMARK(BM_SortStable)
    ->RangeMultiplier(2)
    ->Range(1<<10, 1<<20)
    ->Complexity(benchmark::oNLogN)
    ->Unit(benchmark::kMillisecond);

// 参数化测试:不同数据量
static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        v.reserve(state.range(0)); // 预分配
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::ClobberMemory();
    }
}

// 不预分配 vs 预分配对比
BENCHMARK(BM_VectorPushBack)->Range(1<<8, 1<<16);

BENCHMARK_MAIN();

编译运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 安装google benchmark
# vcpkg install benchmark:x64-linux

g++ -std=c++17 -O2 -o benchmark_test benchmark_example.cpp \
    -lbenchmark -lpthread

# 运行基准测试(默认会多次运行取平均)
./benchmark_test

# 只跑排序测试
./benchmark_test --benchmark_filter="Sort.*"

# 输出CSV格式,方便后续分析
./benchmark_test --benchmark_format=csv --benchmark_out=result.csv

# 输出JSON格式
./benchmark_test --benchmark_format=json --benchmark_out=result.json

输出示例:

2026-03-29 16:00:00
Run on (8 X 3000 MHz CPU s)
CPU Cache: L1d 32K, L1i 32K, L2 256K, L3 8M
-------------------------------------------------------------------
Benchmark                              Time             CPU   Iterations
-------------------------------------------------------------------
BM_SortStd/1024/1                 0.0003 ms        0.0003 ms      1234567
BM_SortStd/2048/1                 0.0006 ms        0.0006 ms       617283
BM_SortStd/4096/1                 0.0012 ms        0.0012 ms       308642
...
BM_SortStd/1048576/1              1.2345 ms        1.2345 ms         1000
----------------------------------------------------------------
Complexity
-------------------------------------------------------------------
                         O(N log N)  Mean           1.000x
                        *testing against baseline

本章小结

本章我们从测试、CI/CD、代码质量、配置管理、版本控制、文档化和性能基准测试七个维度,全面探索了C++软件工程的核心实践。

关键要点回顾:

  1. 测试实践:测试不是事后的补丁,而是发现代码何时会错的守卫。Google Test/Catch2等框架提供了丰富的断言和mock能力,用EXPECT_*而非assert可以获得更好的错误信息;遵循Arrange-Act-Assert三段式,写出真正能捕获bug的测试。

  2. 持续集成(CI):GitHub Actions配合CMake和clang-tidy,可以实现每一次push都自动跑全量构建+测试+静态分析。多平台矩阵测试(Linux/macOS/Windows)能有效捕获编译器差异带来的问题。

  3. 代码质量度量:clang-tidy做静态分析,gcov+lcov做覆盖率统计,cloc统计代码量。量化指标让代码质量可见、可追踪、可改进。

  4. 配置与环境管理:YAML/JSON配置文件 + 环境变量模式实现"代码与配置分离"。vcpkg解决了C++依赖管理的老大难问题,一条命令安装、配置、链接全部搞定。

  5. 版本控制策略:GitFlow适合发布周期长的项目,GitHub Flow适合持续部署场景。善用Git钩子(pre-commit)可以在代码进入仓库前就拦截问题。

  6. 文档化规范:Doxygen注释是C++工业标准的文档生成方案。注释要写"为什么"而非"是什么",简单代码不注释,接口文档必须写。

  7. 基准测试:Google Benchmark提供了高精度的性能测量能力。优化前先测量,优化后再测量,数据对比才是优化效果的硬指标。

实践建议:

  • 从今天开始,给每个新项目配置CI/CD流水线
  • 在每次代码提交前运行pre-commit检查(格式化+快速测试)
  • 养成写Doxygen风格注释的习惯,文档自动生成
  • 性能优化前先写基准测试,用数据指导优化方向
  • 把vcpkg作为依赖管理的事实标准,告别手动下载编译第三方库

思考题:

  1. 如果你的团队有10个人,大家都在同一个develop分支上工作,如何避免频繁的代码冲突?
  2. 在测试金字塔中,单元测试和端到端测试的比例通常是多少?为什么?(提示:业界经验是70%:20%:10%——底层单元测试最多、顶层E2E最少)
  3. 为什么说"配置硬编码"是软件工程的原罪之一?请举例说明其危害。

延伸阅读:

  • 《代码大全(第2版)》—— Steve McConnell,系统讲解软件构建全过程
  • 《修改代码的艺术》—— Michael Feathers,深入讲解如何处理遗留代码
  • Google C++ Style Guide —— C++代码规范的行业标杆
  • “The Pragmatic Programmer” —— 程序员必读的软件工程经典

本章完

最后修改 March 29, 2026: 新增 C++ 教程 (8c0ff46)