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)关注逻辑和设计,格式问题交给工具,评审时不再争论空格。
十、一份最小可执行的规范清单
如果团队从零开始,先把这十条立起来,投入产出比最高:
- 全项目统一的命名与格式风格(配
.clang-format)。 - 禁止裸
new/delete,资源一律 RAII。 - 单参数构造函数必须
explicit。 - 基类析构必须
virtual;重写虚函数必须override。 - 头文件不
using namespace;能前向声明就不 include。 - 函数参数:输入用 const 引用/值,结果用返回值。
nullptr取代NULL,constexpr取代魔数。- 警告全开并
-Werror,CI 跑 clang-tidy。 - 并发共享数据必须说明保护机制。
- 公开接口必须有注释,注释写"为什么"。
结语
编码规范的终极目标不是代码长得整齐,而是让读代码的人能预测代码的行为。风格可以争论,但一致性不容妥协。先抄一份成熟规范(Google C++ Style Guide 或 C++ Core Guidelines),用工具固化下来,再随团队成长逐步演进——这比从零发明一套完美的规范要实际得多。
延伸阅读:
浙公网安备 33010602011771号