C++编码规范实践指南

C++ 编码规范:写出可维护、可协作的现代 C++ 代码

前言

C++ 是一门强大而复杂的语言,它给予开发者极大的自由,但自由往往意味着风险:同一个功能可以有一万种写法,其中九千种会给后来者(包括三个月后的自己)挖坑。编码规范的意义不在于束缚,而在于降低沟通成本、减少低级错误、让团队协作成为可能。

Google、LLVM、Qt、Epic 等公司都公开了自己的 C++ 风格指南,Bjarne Stroustrup 与 Herb Sutter 主导的 C++ Core Guidelines 更是社区公认的权威参考。本文不重复罗列那些文档,而是从实战角度提炼出一套可以直接落地的规范要点,并解释背后的"为什么"。

一、命名:代码是给机器执行的,但是给人看的

命名是规范中最基础、也最容易被忽视的部分。

1.1 推荐的命名约定

实体 风格 示例
类型(类、结构体、枚举、typedef) 大驼峰 OrderProcessor、HttpClient
函数 大驼峰或小驼峰(团队统一即可) ComputeTotal()、computeTotal()
变量、函数参数 小驼峰或蛇形 totalAmount、total_amount
类成员变量 加后缀或前缀区分 count_、m_count
宏、枚举常量、constexpr 常量 全大写蛇形 MAX_BUFFER_SIZE
命名空间 全小写 namespace net_util

关键原则不是选哪种风格,而是全项目保持一致。 接手别人的项目时,遵循项目原有风格,而不是把喜欢的风格混进去。

1.2 命名的几个铁律

// 差:含义模糊
int d;  // 经过的天数?
int process(int a, int b);

// 好:自解释
int elapsedDays;
int MergeIntervals(int left, int right);
  • 不用无意义的缩写:cnt 可以,rctd 不行。缩写必须是人人都懂的。
  • 布尔变量用"是/否"语义命名:is_valid、has_error、can_retry,读 if (is_valid) 就像读自然语言。
  • 不用单字母变量(循环里的 i、j 除外)。
  • 不要用拼音,英文不行宁可写长一点。

二、头文件与包含:编译速度和依赖管理的源头

2.1 每个头文件都要有保护

#pragma once        // 现代编译器都支持,简洁
// 或传统的
#ifndef PROJECT_MODULE_FILE_H_
#define PROJECT_MODULE_FILE_H_
...
#endif

2.2 包含顺序与路径

推荐顺序(Google 风格):相关头文件 → C 系统头 → C++ 系统头 → 第三方库 → 项目内头文件。

#include "order_processor.h"   // 本文件对应的头文件

#include <sys/types.h>          // C 系统头
#include <vector>               // C++ 标准库
#include <absl/strings/str_cat.h>  // 第三方
#include "base/logging.h"       // 项目内
  • 能前向声明就不要 include:头文件里只用指针或引用的类型,用 class Foo; 前向声明,能显著减少编译时间和循环依赖。
  • 不要在头文件里 using namespace,这会污染所有包含者的命名空间,且无法撤销。

三、类与接口设计

3.1 构造函数与初始化

  • 成员初始化列表优先于构造函数体内赋值:
class Buffer {
public:
    explicit Buffer(size_t size) : data_(size) {}  // 好:直接构造
private:
    std::vector<char> data_;
};

体内赋值是先默认构造再赋值,对非 POD 类型是双重开销。

  • 单参数构造函数加 explicit,防止隐式类型转换带来的意外:
class Size {
public:
    explicit Size(int value);  // 没有 explicit,Size s = 5; 会静默通过
};

3.2 资源管理:RAII 是 C++ 的灵魂

任何资源的获取都要绑定到对象的生命周期上。 能用智能指针就不用裸 new/delete:

// 差:异常发生时泄漏
void Process() {
    FILE* fp = fopen("data.txt", "r");
    // ... 中间 return 或抛异常,fp 泄漏
    fclose(fp);
}

// 好:RAII
void Process() {
    auto file = std::ifstream("data.txt");   // 析构自动关闭
    auto buffer = std::make_unique<char[]>(1024);
}

规则很简单:unique_ptr 是默认选择,确有共享语义才用 shared_ptr,观察关系用 weak_ptr 或裸指针(裸指针表示"不拥有")。

3.3 继承的纪律

  • 基类析构函数必须是 virtual,否则通过基类指针删除子类对象是未定义行为。
  • 优先组合而非继承;继承只表达"is-a"。
  • 重写的虚函数一律标注 override(编译器会帮你发现签名不匹配),终结类/函数标注 final。

四、函数与参数

  • 函数要短:一个函数只做一件事,超过 50 行就该考虑拆分。嵌套不超过 3 层。
  • 输入参数用值或 const 引用,输出参数用指针或返回值——现代 C++ 更推荐直接返回值(RVO 让这几乎没有开销):
// 好:返回值即结果,配合 optional 表达可能失败
std::optional<User> FindUser(int id);

// 差:输出参数让调用点可读性变差
bool FindUser(int id, User* out);
  • 不用默认参数隐藏重载语义,重载让意图更清晰。
  • 参数过多(超过 5 个)时,定义一个配置结构体传参。

五、现代 C++ 特性:该用就用

编码规范不是守旧,C++11 之后的很多特性就是为了消灭老式写法的 bug:

旧习惯 现代做法
NULL nullptr(类型安全)
裸 new/delete make_unique / make_shared
typedef using(支持模板别名,更直观)
手写迭代器循环 范围 for:for (const auto& item : container)
魔数常量 constexpr 常量
throw() 异常规格 noexcept

两个高频细节:

// auto 用于类型冗长或显而易见处,不要滥用
auto it = cache.find(key);              // 好:类型显然
auto value = ComputeResult();           // 差:读者不知道返回什么

// 常量引用避免拷贝
void PrintNames(const std::vector<std::string>& names);  // 好
void PrintNames(std::vector<std::string> names);         // 差:全量拷贝

六、错误处理与防御性编程

  • 错误处理策略要全局统一:要么异常,要么错误码/std::expected,不要混用。性能敏感的底层库常用错误码,业务逻辑层异常更清晰。
  • 断言(assert)用于"绝不应该发生"的内部不变量,不用于校验外部输入——外部输入必须用正常错误处理。
  • 未使用的参数显式标注 (void)param; 或 C++17 的 [[maybe_unused]]。
  • switch 的 default 分支不要省略,enum 全枚举时 default 里加断言防新增枚举漏改。

七、并发与线程安全

  • 共享数据必须有明确的保护策略:std::mutex 配 std::lock_guard / std::scoped_lock,永远不要裸调 lock()/unlock()。
  • 锁的粒度宁小勿大,持锁期间不调用不可控的外部代码(回调、虚函数)。
  • 只读的初始化数据用 std::call_once 或函数内静态变量(C++11 起线程安全)。
  • 能用 std::atomic 的简单标志位就别上锁。

八、注释:解释"为什么",而不是复述"是什么"

// 差:复述代码
i++;  // i 加 1

// 好:说明意图和背景
// 这里需要轮询而非回调,因为设备固件 v2.3 之前不支持中断通知
while (!device->Ready()) { ... }
  • 公开接口(头文件中的函数)必须有文档注释:功能、参数、返回值、线程安全性、可能抛出的异常。
  • 临时的折中方案写 TODO(负责人): 描述 + 期望完成时间。
  • 注释要和代码同步更新——过期的注释比没有注释更害人。

九、格式化与工具链:让规范自动化

规范靠人自觉一定会崩,要靠工具兜底:

  • clang-format:统一排版。团队共享一份 .clang-format 文件进版本库,保存时自动格式化。
  • clang-tidy:静态检查,能抓出 modernize-*、bugprone-*、performance-* 等大量问题,接入 CI。
  • 编译警告全开:-Wall -Wextra -Werror(GCC/Clang),警告当错误处理。
  • 编译器版本统一:在 CI 和文档中钉死编译器版本与标准版本(如 -std=c++17)。
  • 代码评审(Code Review)关注逻辑和设计,格式问题交给工具,评审时不再争论空格。

十、一份最小可执行的规范清单

如果团队从零开始,先把这十条立起来,投入产出比最高:

  1. 全项目统一的命名与格式风格(配 .clang-format)。
  2. 禁止裸 new/delete,资源一律 RAII。
  3. 单参数构造函数必须 explicit。
  4. 基类析构必须 virtual;重写虚函数必须 override。
  5. 头文件不 using namespace;能前向声明就不 include。
  6. 函数参数:输入用 const 引用/值,结果用返回值。
  7. nullptr 取代 NULL,constexpr 取代魔数。
  8. 警告全开并 -Werror,CI 跑 clang-tidy。
  9. 并发共享数据必须说明保护机制。
  10. 公开接口必须有注释,注释写"为什么"。

结语

编码规范的终极目标不是代码长得整齐,而是让读代码的人能预测代码的行为。风格可以争论,但一致性不容妥协。先抄一份成熟规范(Google C++ Style Guide 或 C++ Core Guidelines),用工具固化下来,再随团队成长逐步演进——这比从零发明一套完美的规范要实际得多。

延伸阅读:

posted @ 2026-10-13 20:45  Heassw  阅读(4)  评论(0)    收藏  举报