跳至内容

C++ 核心准则与专业模式

学习 C++ 核心准则、命名规范、const 正确性、API 设计原则、文档实践、性能分析和生产环境调试技术。

编写专业的 C++ 代码

编写正确、高效的 C++ 是必要的,但对于专业软件开发来说还不够。专业的 C++ 还意味着代码是可读的(他人和未来的你都能理解)、可维护的(容易修改而不引入 bug)和健壮的(优雅地处理错误并可预测地失败)。由 Bjarne Stroustrup 和 Herb Sutter 维护的 C++ 核心准则将数十年来艰难获得的智慧编纂为可操作的规则。本课涵盖最具影响力的准则和专业实践。

C++ 核心准则概述

核心准则涵盖资源管理、接口、错误处理和命名。以下是实践中最重要的一些规则:

core_guidelines.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <span>
#include <stdexcept>

// R.1: Manage resources automatically using RAII
class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connection_string) {
        // Acquire resource in constructor
        std::cout << "Connected to: " << connection_string << "\n";
    }
    ~DatabaseConnection() {
        // Release resource in destructor — always runs
        std::cout << "Disconnected\n";
    }

    // Rule of 5: if you define a destructor, define or delete all 5
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    DatabaseConnection(DatabaseConnection&&) noexcept = default;
    DatabaseConnection& operator=(DatabaseConnection&&) noexcept = default;

    void query(std::string_view sql) const {
        std::cout << "Executing: " << sql << "\n";
    }
};

// I.4: Make interfaces precisely and strongly typed
// BAD: void process(int type, void* data, int size);
// GOOD:
enum class DataType { json, xml, csv };

void process(DataType type, std::span<const std::byte> data) {
    // type-safe, no raw pointers, size is carried with the data
}

// F.3: Keep functions short and simple
[[nodiscard]] bool is_valid_email(std::string_view email) {
    auto at_pos = email.find('@');
    if (at_pos == std::string_view::npos) return false;
    auto dot_pos = email.find('.', at_pos);
    return dot_pos != std::string_view::npos && dot_pos > at_pos + 1;
}

int main() {
    // R.5: Prefer scoped objects — no unnecessary heap allocation
    DatabaseConnection db("host=localhost dbname=mydb");
    db.query("SELECT * FROM users");
    // db is automatically closed when it goes out of scope

    // R.11: Avoid raw new/delete — use smart pointers
    auto widget = std::make_unique<std::vector<int>>(100);

    std::cout << is_valid_email("user@example.com") << "\n";   // 1
    std::cout << is_valid_email("invalid-email") << "\n";       // 0

    return 0;
}

const 正确性作为一种准则

const 正确性不仅仅是防止意外修改 ── 它记录意图、启用编译器优化,并使代码默认线程安全。除非需要可变,否则让一切都是 const 的:

const_correctness.cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

class StudentRegistry {
public:
    // const method — promises not to modify the object
    [[nodiscard]] size_t count() const { return students_.size(); }

    // const reference return — caller can read but not modify
    [[nodiscard]] const std::string& get_name(size_t index) const {
        if (index >= students_.size())
            throw std::out_of_range("Invalid student index");
        return students_[index];
    }

    // Non-const method — modifies the object
    void add_student(std::string name) {
        students_.push_back(std::move(name));
    }

    // const-correct algorithm usage
    [[nodiscard]] bool has_student(std::string_view name) const {
        return std::any_of(students_.cbegin(), students_.cend(),
            [name](const std::string& s) { return s == name; });
    }

private:
    std::vector<std::string> students_;
};

void print_registry(const StudentRegistry& reg) {
    // Can only call const methods on a const reference
    for (size_t i = 0; i < reg.count(); ++i) {
        std::cout << reg.get_name(i) << "\n";
    }
    // reg.add_student("Eve");  // ERROR: cannot call non-const method
}

int main() {
    StudentRegistry reg;
    reg.add_student("Alice");
    reg.add_student("Bob");

    print_registry(reg);

    // const local variables
    const auto count = reg.count();  // immutable
    const auto& name = reg.get_name(0);  // immutable reference

    std::cout << "Count: " << count << ", First: " << name << "\n";

    return 0;
}

API 设计原则

C++ API 设计的黄金法则,归功于 Scott Meyers:使接口容易被正确使用,难以被错误使用。

使用强类型:不要用 void set_timeout(int ms),而用 void set_timeout(std::chrono::milliseconds timeout)。调用者不可能在你期望毫秒时意外传入秒。

使用 [[nodiscard]]:任何返回错误码、资源或计算值的函数都应该标记 [[nodiscard]]。忽略返回值几乎总是一个 bug。

优先使用值语义:小类型按值传递,较大类型按 const& 传递。按值返回(编译器会应用拷贝省略)。避免输出参数。

最小化公共接口面积:每个公共成员都是你必须维护的承诺。默认使用 private,只暴露必要的内容。

遵循最小惊奇原则:函数应该做其名称暗示的事情,不多不少。副作用应从名称中显而易见(save_to_file,而非 process)。

命名规范与文档

一致的命名使代码在团队中可读。常见的 C++ 规范:

类型:PascalCase ── class HttpClient、struct Point3D、enum class Color
函数/方法:snake_case 或 camelCase ── calculate_total()、getSize()
变量:snake_case ── int item_count、double total_price
成员变量:尾部下划线 ── int count_、std::string name_
常量:k 前缀或 UPPER_SNAKE ── constexpr int kMaxRetries = 3
命名空间:lowercase ── namespace network、namespace detail

选择一种规范并在项目中强制执行(clang-format + clang-tidy 可以自动化这一点)。

文档:对公共 API 使用 Doxygen 风格注释:

/// @brief Connects to the database.
/// @param connection_string The database connection URI.
/// @return true if connection succeeded.
/// @throws DatabaseError if the connection fails.
[[nodiscard]] bool connect(std::string_view connection_string);

性能分析

永远不要在没有性能分析的情况下优化。这些工具告诉你时间实际花在哪里,用数据取代猜测:

benchmark.cpp
# Linux: perf — hardware performance counters
perf record -g ./my_program         # record call graph
perf report                           # interactive flamegraph viewer

# Linux: perf stat — high-level statistics
perf stat ./my_program
# Output: cycles, instructions, cache misses, branch misses, IPC

# macOS: Instruments (via Xcode)
xcrun xctrace record --template 'Time Profiler' --launch ./my_program

# Cross-platform: Google Benchmark (micro-benchmarking)
# In your code:
#include <benchmark/benchmark.h>

static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorPushBack)->Range(8, 1 << 16);

static void BM_VectorReserved(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::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorReserved)->Range(8, 1 << 16);

BENCHMARK_MAIN();
最佳实践

审查 C++ 代码时,检查以下常见问题:

1. 资源管理:每个资源(内存、文件、锁)都由 RAII 管理吗?有原始的 new/delete 吗?
2. const 正确性:参数、成员函数和局部变量在可能的情况下都是 const 的吗?
3. 错误处理:返回值被检查了吗?异常在适当的层次被捕获了吗?std::expected 或错误码的使用是否一致?
4. 线程安全:共享资源被保护了吗?数据竞争是否可能?消毒器能捕获问题吗?
5. 移动语义:对象在适当的地方被移动而非拷贝了吗?是否在 const 对象上使用了 std::move(这会静默地拷贝)?
6. API 清晰度:函数名称是否具有描述性?参数是否使用强类型?重要返回值是否使用了 [[nodiscard]]?
7. 构建警告:代码能在 -Wall -Wextra -Wpedantic 下干净编译吗?所有警告都被处理了而非被抑制了吗?
8. 测试:边界情况被测试了吗?新的代码路径被测试覆盖了吗?消毒器通过了吗?

常见陷阱

在生产环境中调试 C++ 比在开发中更困难。为此做好准备:

始终附带调试符号发布:对生产二进制文件使用 RelWithDebInfo 构建。单独的 .debug 文件中的调试符号不会增加运行时开销,但使崩溃分析成为可能。

记录结构化数据:使用带有日志级别的结构化日志(JSON)。包含时间戳、线程 ID 和请求 ID。使用 spdlog 或 std::format 进行快速、类型安全的日志记录。

核心转储:启用核心转储(ulimit -c unlimited)并知道如何用 gdb 分析它们。符号服务器存储已部署二进制文件的调试信息。

常见生产 bug:
- 内存泄漏 ── 在预生产环境中使用 ASan,或使用 Valgrind / Heaptrack 等工具
- 死锁 ── 在测试中使用 TSan,记录锁获取顺序,设置锁超时
- 性能退化 ── CI 中的持续基准测试在部署前检测退化
- ABI 兼容性 ── 更改结构体布局或虚函数表会破坏 ABI;使用 Pimpl 惯用法实现稳定的 ABI

关键要点
  • 遵循 C++ 核心准则 ── 资源使用 RAII、五法则、强类型、最小公共 API
  • 默认让一切都是 const 的 ── 它记录意图、防止 bug 并启用优化
  • 设计容易被正确使用、难以被错误使用的 API ── 强类型、[[nodiscard]]、值语义
  • 优化前先做性能分析 ── 使用 perf、Instruments 或 Google Benchmark 找到真正的瓶颈
  • 审查代码的资源管理、const 正确性、线程安全和正确的错误处理
  • 为生产调试做准备:附带调试符号发布、使用结构化日志、启用核心转储

测验 — 检验你的知识

(15 经验值)

1. C++ 核心准则中的「五法则」指什么?

2. C++ API 设计的黄金法则是什么?

3. 为什么应该在优化前做性能分析?