C++ 核心准则与专业模式
学习 C++ 核心准则、命名规范、const 正确性、API 设计原则、文档实践、性能分析和生产环境调试技术。
编写专业的 C++ 代码
编写正确、高效的 C++ 是必要的,但对于专业软件开发来说还不够。专业的 C++ 还意味着代码是可读的(他人和未来的你都能理解)、可维护的(容易修改而不引入 bug)和健壮的(优雅地处理错误并可预测地失败)。由 Bjarne Stroustrup 和 Herb Sutter 维护的 C++ 核心准则将数十年来艰难获得的智慧编纂为可操作的规则。本课涵盖最具影响力的准则和专业实践。
C++ 核心准则概述
核心准则涵盖资源管理、接口、错误处理和命名。以下是实践中最重要的一些规则:
#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 的:
#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);性能分析
永远不要在没有性能分析的情况下优化。这些工具告诉你时间实际花在哪里,用数据取代猜测:
# 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. 为什么应该在优化前做性能分析?