入行这么多年,我见过太多因为代码风格问题吵起来的场景。有人坚持 Allman 风格,有人死守 K&R,有人变量名全小写下划线,有人非要首字母大写。C++ 这门语言本身就带着多范式基因,从 C 风格的过程式写法到模板元编程,从裸指针到智能指针,同一份代码里可以同时出现完全不同的气质。这种自由度在写小工具时是爽快,一旦项目规模上来,就成了灾难。lil_tea 这份 C++ 风格指南,最初就是为解决这类问题而整理的。
这个标题乍看像个个人项目,但它背后折射出的需求是普遍的:每个 C++ 开发者在某个阶段都需要一份属于自己的、或者属于团队的编码规范。不管是刚从 C 转过来的老手,还是刚刚啃完语法书的新人,都会在"到底该怎么写才像样"这个问题上卡住。这份指南要解决的,就是这个"像样"的问题——让代码不仅跑得对,还读得懂、改得动、review 得顺。下面我会从为什么需要风格指南、核心决策点、实操落地、以及踩坑记录四个维度,把这份指南拆开揉碎讲清楚。
1. 为什么 C++ 比其他语言更需要一份风格指南
很多语言有官方风格,比如 Go 有 gofmt、Python 有 PEP 8、Rust 有 rustfmt,语言层面就给你定死了,就算你不想遵守,工具也会强制你执行。但 C++ 没有。标准委员会不关心你变量名用驼峰还是下划线,编译器也照单全收,不报一个 warning。这就导致每个 C++ 项目都可以是一套全新的风格,而且每套风格的支持者都觉得自己那套才是正统。
1.1 C++ 多范式特性带来的风格分裂
C++ 支持过程式、面向对象、泛型、函数式四种主要范式,这四种范式天然会诱导出不同风格的代码。写惯 C 的人倾向于把变量声明在函数开头,用裸指针传参,习惯用 struct 聚合数据;写惯 Java 的人一上来就是 class 套 interface,getter/setter 铺满整个文件;玩模板的人则满屏都是 typename 和 constexpr。这些写法单独看都没问题,但如果混在同一个项目里,阅读体验会非常割裂。
我见过一个真实的例子:同一个函数里,前半段用 int* p = new int(5);,后半段突然冒出一个 std::unique_ptr<int> q = std::make_unique<int>(5);,然后两个指针都被当参数传给一个函数,函数内部不知道是该 delete 还是不该 delete。这种代码跑起来可能没问题,但它把心智负担全甩给了后来维护的人。风格指南在这里要做的第一件事,就是统一资源管理策略和指针使用方式,把这类隐含的歧义从源头上消灭掉。
1.2 风格指南不是限制,是降低认知负担的手段
很多初学者抵触风格指南,觉得"我代码能跑就行了,管那么多干嘛"。这个想法在写作业时成立,在企业项目里不成立。企业代码的平均寿命是 5 到 10 年,写代码的人一两年就换一茬。如果每份代码风格都不同,后来者每看一个新文件就要重新适应一种风格,这会严重拖慢开发效率。风格指南的本质不是限制你的表达自由,而是把风格问题变成无需思考的默认选项,让你把有限的脑力放在真正的逻辑难点上。
所以 lil_tea 这份指南的第一条原则就是:风格一致性优先于个人偏好。哪怕你个人更喜欢另一种写法,只要团队定了规则,就按规则来。这跟交通规则一个道理——靠右行驶未必比靠左行驶更科学,但所有车都靠右行驶一定比一半靠右一半靠左安全得多。
1.3 一套好的风格指南应该覆盖哪些范围
要明确一点:风格指南不是 C++ 语法教程,它不需要教人怎么写循环、怎么用 vector。它应该聚焦在那些有争议、有选择空间的地方。我整理了几类必覆盖的内容:
- 命名规范:类型、函数、变量、常量、宏、文件名的命名规则。
- 格式排版:缩进、括号、行宽、空行、头文件顺序。
- 注释规范:什么时候该写注释、什么时候不该写、注释怎么写。
- 现代 C++ 实践:智能指针 vs 裸指针、auto 的使用策略、const 正确性、异常处理。
- 工程实践:头文件组织、include 顺序、命名空间使用、API 设计约定。
下文会逐一展开讲。需要提醒的是,风格指南应该给"默认答案",而不是给"唯一答案"。比如"行宽不超过 80 列"可以作为默认值,但必要时允许例外。指南最好包含例外条款,否则执行时会很痛苦。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写好一份风格指南的核心决策点
这一节是整份指南的灵魂。我在整理 lil_tea 风格指南时,反复权衡过很多细节,有些看起来微不足道,实际上影响深远。下面挑几个最关键的说。
2.1 命名规范:驼峰还是下划线
命名是最容易引发争论的话题。C++ 标准库用 snake_case,STL 用 snake_case;Google C++ Style Guide 用 snake_case 函数 + PascalCase 类型;很多游戏公司用 camelCase 函数。到底选哪套?我的建议很直接:跟标准库保持一致。
原因有三:第一,标准库是所有 C++ 开发者共同的语言基础,用 snake_case 能最大程度降低学习成本;第二,你可以把 STL 源码当范文看,风格统一方便吸收经验;第三,现代 C++ 比较流行 snake_case 全小写,代码紧凑,不易与其他语言混淆。
具体规则我建议这样定:
| 类别 | 规则 | 示例 |
|---|---|---|
| 类型名(class/struct/enum) | PascalCase | HttpClient、CompressionMode |
| 函数名 | snake_case | parse_request()、get_user_info() |
| 变量名 | snake_case | buffer_size、name_list |
| 成员变量 | snake_case 加尾下划线 | buffer_size_、name_list_ |
| 常量/constexpr | snake_case | kMaxRetryCount 或 max_retry_count |
| 宏 | UPPER_SNAKE | LOG_INFO、LIMIT_MAX |
| 文件名 | snake_case | http_client.cpp、log_writer.h |
成员变量加尾下划线这个习惯可能有点争议。Google 风格用普通下划线收尾,LLVM 风格不加后缀直接裸写,也有一派人通过 m_ 前缀区分。我选尾下划线是因为它兼顾了可读性和 IDE 补全:在类内写 buffer_size 时 IDE 会优先匹配成员变量,用起来很顺手。只要全项目统一,哪种都能用,但千万别混着来。
2.2 格式排版:自动化和默认值
格式这件事,我最大的经验教训是:不要手写,必须交给工具。人工对齐参数、手动换行、自己调缩进,这些行为除了浪费时间,还会带来大量无意义的 diff。风格指南里只要给出 clang-format 的配置项,然后让 CI 强制所有代码先过一遍格式化就行。
基础默认值可以参考这样一套:
- 缩进:2 空格,不用 Tab。
- 行宽:100 列。80 太窄,写个嵌套的函数签名就得换行;120 太宽,并排两个窗口时会被截断。
- 大括号风格:Allman 还是 K&R?我推荐 K&R(开括号不独占一行),因为它在行数紧凑性上更优,也更接近标准库源码的排版习惯。
- 指针和引用:靠左还是靠右?
int* p还是int *p?C++ 的语法决定了int* p这种写法在声明多个变量时会误导人(int* p, q;里q是int),但 clang-format 配PointerAlignment: Left就能规避这个问题。我的建议是靠左,理由是与类型语义保持一致。
clang-format 配置示例:
yaml复制BasedOnStyle: Google
IndentWidth: 2
TabWidth: 2
UseTab: Never
ColumnLimit: 100
BreakBeforeBraces: Attach
PointerAlignment: Left
DerivePointerAlignment: false
AccessModifierOffset: -2
NamespaceIndentation: None
这套配置基本够用。注意 NamespaceIndentation: None,我强烈建议命名空间内容不缩进,否则每个 namespace 多缩进一层,五层嵌套后代码就跑到屏幕外面去了。
2.3 注释规范:注释是解释为什么,不是翻译是什么
我统计过团队里最差的注释类型,排名第一的是"朗读者式注释",比如 int count = 0; // 计数器。这种注释完全没有信息量,纯粹是噪音。真正有价值的注释只有两种:解释为什么这么写的,以及解释"非显然"的行为约束。
风格指南里我会建议这么几点:
- 注释写在代码上方,用
//,不要用/* */。 - 不要逐行解释代码逻辑,要解释设计意图和约束条件。
- 函数注释写清楚:参数含义、返回值、调用前提、需要注意的副作用。
- 代码和注释之间留空行,避免看起来像贴上去的。
一段好的注释示例:
cpp复制// Buffer full because the last write was partial.
// Keep the remaining bytes in head_buffer_ for the next flush.
// This is intentional: flush is called after every read loop,
// and pushing back to the queue here would cause a double-writing bug.
if (head_buffer_used_ < static_cast<ssize_t>(buf_size)) {
std::copy(head_buffer_ + head_buffer_used_,
head_buffer_ + head_buffer_used_ + remaining,
buf);
head_buffer_used_ += remaining;
}
这段注释解释了"为什么缓冲区不满时不直接丢弃剩余字节",这是别人看代码时最容易困惑的地方。至于 std::copy 怎么工作,不需要在注释里讲。
2.4 现代 C++ 实践条款
这部分是风格指南里最容易过时的内容,也是最能体现团队水平的部分。我建议在指南里明确以下规则:
- 禁止裸 new/delete:所有动态内存一律用
std::unique_ptr或std::shared_ptr。这条规则在 C++14 之后基本是共识。 - 用
std::make_unique和std::make_shared而不是new传给智能指针构造:避免内存泄漏(参数求值顺序导致的泄漏)并减少一次分配。 - 默认使用
const引用传参:除非函数需要保留参数的副本,否则不要用值传参。只有像int、double这样的内置类型可以值传。 - 优先
auto声明局部变量:但在 API 边界避免用auto模糊类型语义,比如 public 函数的返回值要显式写清楚。 - 使用
enum class代替裸enum:后者会污染外层作用域。 - 不要用
std::bind,用 lambda:std::bind的可读性差,而且调试信息不友好。 - 用
nullptr表示空指针,不要用NULL或0。
这些条款既适合规范新代码,也为老代码迁移提供了方向。需要特别说明的是,现代 C++ 实践条款不是耍酷,而是为了减少错误。enum class 的一个典型好处是:比较不同类型枚举时编译器会直接报错,而裸枚举之间会隐式转换为 int,很容易在 switch 的 default 分支里漏掉 case。
3. 实操:从零起草一份团队 C++ 风格指南
前面讲的是决策点,这一节讲落地。我知道很多人看了一堆风格指南文章后最大的困惑是:道理我都懂,但告诉我第一步干嘛?别急,我按自己走过的路径给你梳理一套可执行的流程。
3.1 第一步:选一个基础模板,别从零开始
不要自己从白纸写风格指南。网上现成的成熟模板很多,选一个最接近你项目气质的,然后裁剪修改。我的推荐优先级是:
- Google C++ Style Guide:最全面、社区认可度最高,适合大型项目和企业团队。但内容很多(中文翻译版也上万字),适合有精力去读的团队。
- Modern C++ Coding Guidelines(Herb Sutter 与 C++ Core Guidelines): 侧重现代 C++ 最佳实践,适合新项目,但它偏"指导"而非"命令",有些条款比较抽象,落地时还需要转成具体规则。
- LLVM Coding Standards:简洁、直接、易执行。如果你是做工具链或性能敏感的项目,这套非常合适。
- Qt Coding Style:如果你用 Qt 框架,直接沿用 Qt 风格可以减少在框架和业务代码之间来回切换的割裂感。
lil_tea 这份指南一开始就是参照 Google Style 和 LLVM 的风格整合的。我没有全部照搬,比如 Google 禁止 exceptions,但我们项目跑在常规 Linux 服务器上,异常开销不是瓶颈,所以就允许使用异常。这种"按需裁剪"非常关键,风格指南是给团队用的,不是给 Google 的代码库用的。
3.2 第二步:规定头文件结构和 include 顺序
头文件是 C++ 工程里最容易踩坑的地方,风格指南必须明确 include 顺序。乱序 include 会导致隐蔽的编译错误:一个头文件里依赖了另一个头文件的间接 include,而你在自己文件里先 include 了其他头文件,导致 include 顺序改变时出现"漏包含"问题。
我建议的顺序是:
- 本文件对应的 .h 头文件(保证 .h 自包含,能编译通过)。
- C 标准库头文件(如
<cstdio>、<cstring>)。 - C++ 标准库头文件(如
<vector>、<string>)。 - 第三方库头文件(按字母序)。
- 本项目内部头文件(按字母序或按模块顺序)。
对应到一个实际文件里是这样:
cpp复制// http_client.cpp
#include "http_client.h" // ① 本文件对应头文件,优先保证自包含
#include <cstddef> // ② C 标准库
#include <cstring> // ② C 标准库
#include <map> // ③ C++ 标准库
#include <string> // ③ C++ 标准库
#include <fmt/format.h> // ④ 第三方库
#include "log/log_writer.h" // ⑤ 项目内部
#include "net/connection.h" // ⑤ 项目内部
这样做的好处是,第一行 #include "http_client.h" 会把"该头文件是否自带所需依赖"的问题第一时间暴露给编译器。如果 http_client.h 里用了 std::string 但没 include <string>,编译会直接报错,你立刻就能发现,而不是等到某个 .cpp 文件碰巧先 include 了 <string> 才侥幸通过。
3.3 第三步:配置 clang-format 与 clang-tidy 并接入 CI
手写规范只是第一步,真正让规范"跑起来"的是工具链。clang-format 负责格式,clang-tidy 负责静态检查。我建议在项目根目录放两个文件:
.clang-format:格式规则,前面已经给过配置。.clang-tidy:检查规则,用来发现命名问题、现代 C++ 使用问题等。
一个实用的 .clang-tidy 示例:
yaml复制Checks: >
clang-analyzer-*,
cppcoreguidelines-*,
modernize-*,
readability-*,
performance-*,
bugprone-*
WarningsAsErrors: false
HeaderFilterRegex: '.*'
单独跑 clang-format 的命令:
bash复制clang-format -i src/*.cpp include/*.h
CI 里建议这样强制检查:
bash复制# 检查代码是否已经格式化
find src include -name '*.cpp' -o -name '*.h' | xargs clang-format --dry-run --Werror
# 跑 clang-tidy
clang-tidy src/*.cpp -p build
注意 --dry-run --Werror 的组合——它不会实际修改文件,只检查是否有格式问题,有问题就报错。这样开发者本地可以先用 -i 自动格式化,CI 再做最终校验,双保险。
3.4 第四步:建立代码评审检查表
风格指南光有文档是不够的,要通过评审会议把它内化成团队的肌肉记忆。我建议把规范浓缩成一页 check list,贴在每个 PR 的描述区模板里:
- [ ] 文件名、命名空间、类型、函数、变量命名符合规范
- [ ] include 顺序正确,且 .h 文件自包含
- [ ] 没有裸 new/delete,动态内存全部由 RAII 管理
- [ ] const 正确性:只读参数用 const 引用,成员函数能加 const 就加
- [ ] 没有
std::bind、enum等废弃特性 - [ ] 注释只解释"为什么",不翻译"是什么"
- [ ] 代码已经过 clang-format 格式化
这个 check list 不用长,但要锚定核心痛点。评审的时候逐项打勾,比在 comment 里逐条写"这里应该加 const"高效得多。风格类的问题不该靠人的眼睛去抓,应该靠工具。评审人的精力应该花在逻辑正确性和架构合理性上。
3.5 第五步:处理历史代码,先立规矩再翻新
如果你已经有一个老项目,最现实的问题是怎么过渡。我见过不少团队因为想一步到位把上百万行老代码全部格式化,结果格式化后 diff 巨大,reviewer 根本没法看,最后回滚放弃。这种做法非常打击士气。
我建议分三步走:
- 冻结新代码:从今天起,所有新提交的代码和修改过的文件必须符合新风格。
- 顺路改老代码:当你因为功能迭代需要修改某个文件时,顺手把该文件整体格式化,同时补上缺失的 include、修正命名。这样改动和格式化放在同一个 commit 里,函数逻辑变更和格式变更混在一起虽然评审会辛苦一点,但至少能保证改动文件的新增部分是干净的。
- 特定文件专项优化:对于核心模块、多人同时修改的高频文件,专门排期做一次风格迁移。迁移时建议用
git blame历史记录辅助,尽量选在业务迭代较周的空窗期执行。
整个迁移周期拉长到半年也不丢人。风格迁移的目标是"未来的每行新代码都是干净的",不是"过去的每行旧代码都重写"。
4. 落地过程中常见的问题与排查实录
风格指南落地不是一蹴而就的。我自己走过不少弯路,也帮团队排查过不少起看起来跟风格无关、其实根源就是风格不一致引发的 bug。这些经验写成速查表,希望能帮你少踩几个坑。
4.1 格式化后大量 diff 冲成一片,怎么回退和复查
这是最典型的坑。好不容易写了 clang-format 配置,一跑 -i,整个项目几百个文件全变了,reviewer 界面一片红。
我的处理方案是:格式化前先建一个单独的 commit 保存当前状态,然后再跑格式化,格式化后的结果单独提交。这样至少你能用 git diff --stat 和 git diff -w 来区分"真正代码改动"和"纯格式改动"。-w 参数会忽略空白字符变化,如果 git diff -w 的输出为空,说明这次改动纯粹是格式调整,revert 也就有了依据。
更精细一点的做法是配置 .git-blame-ignore-revs,把大范围的格式化 commit 写进去,让 git blame 跳过这些提交,避免后续排查历史时每次都定位到格式化 commit 上。
bash复制# .git-blame-ignore-revs
# Apply clang-format to all source files
2a3b1c4d...commit-hash...
4.2 clang-format 和 clang-tidy 的规则冲突
有些时候,clang-format 会把代码排成一种样子,但 clang-tidy 又有一条规则建议另一种写法,两者打架。最典型的是指针靠左和 readability-identifier-naming 的命名检查冲突,或者格式化后行宽超限但 clang-tidy 又触发 readability-function-size 警告。
解决的思路是:先统一规则的优先级。我建议以 clang-format 的输出为准,格式问题全部交给它;clang-tidy 只负责逻辑和现代 C++ 相关的检查。如果在 .clang-tidy 里发现某些规则与团队风格冲突,直接禁用或调 warn 级别,不要让它卡 CI。
实际排查时可以这样:
bash复制# 单独查看某个文件被 clang-tidy 报了什么
clang-tidy --list-checks -checks='-*, readability-*' src/foo.cpp
# 通过 -fix 自动修复能修的项
clang-tidy src/foo.cpp -p build -fix
4.3 团队抗拒风格指南怎么办
风格指南推行最大的阻力不是技术,是人心。一个团队里总会有人觉得"我写了十年 C++,不需要谁来教我怎么取名"。我的经验是以数据说话,而不是强压。
你可以做一个小实验:挑一个几百行的老文件,让它通过 clang-format 和 clang-tidy,然后用 git diff --stat 统计改动行数,再请那位工程师评审改动。如果改动行数很高,直观展示了老代码离规范有多远,他就自然明白为什么需要规则了。另外一个策略是——让最资深的人先签字认领风格指南初稿,每个人有意见可以提,但一旦定稿就必须执行。这个过程本身也是在培养对规则的 ownership。
4.4 高频争议:8 个反复出现的 C++ 风格问题
下面这些问题,几乎每次 style review 都会遇到。提前在指南里写明答案,能省下大量讨论时间。
1. ++i 还是 i++?
统一用前缀 ++i。对于迭代器,后缀形式会产生旧值的拷贝,虽然现代编译器大多能优化,但养成写前缀的习惯可以避免踩中某些非优化构建的性能坑。
2. using namespace std; 用不用?
头文件里绝对禁止,.cpp 文件里也建议不要用。写 std:: 是显式的自我标注,能让你和读者都清楚每个名字从哪里来。
3. 函数参数用引用还是指针?
可空参数用指针(T*),不可空参数用引用(T&)。这是 C++ 社区最主流的约定:看到指针就要考虑"它可能是空",看到引用默认它是有效的。
4. size_t 还是 int 表示大小?
优先 size_t。STL 容器的大小类型就是 size_t,编译器会有符号比较警告(-Wsign-compare),虽然可以强转,但默认用 size_t 能少很多麻烦。
5. 类内枚举放 public 还是 private?
能在类内 private 就 private,通过公有静态方法向外暴露。枚举值也属于实现细节,不该成为类接口的一部分。
6. 头文件里要不要 #pragma once?
要。#pragma once 不是标准 C++,但所有主流编译器都支持,而且比 include guard 少写六个宏名字,杜绝 guard 宏冲突问题。如果哪天遇到编译器不支持(概率极低),再切换回 include guard 也不迟。
7. const int& 和 std::string_view 怎么选?
字符串参数用 std::string_view,接受临时字符串时避免分配;但要注意 string_view 不拥有内存,生命周期必须比函数调用长。
8. 类成员变量初始化用初始化列表还是默认成员初始化器?
两者都行,但不要混用。建议在类内定义处写默认值(int count_ = 0;),然后在构造函数初始化列表里只初始化有参依赖的成员。这样能减少重复,且避免漏初始化。
4.5 风格指南需要定期升级
C++ 标准每三年出一个小版本,风格指南也该定期更新。我的做法是每季度安排一次 1 小时的专题讨论,把 C++ 标准的新特性和团队实践对照一下,看有没有值得纳入规范的新条款。比如 C++20 出了 concepts 和 ranges,C++23 出了 std::expected,这些新特性用起来更安全、更简洁,如果能引入,就该更新进风格指南。
但更新要克制,不能每出一个新特性就写进规范。判断标准只有一个:这个新特性是否能让代码的错误率显著下降,或者大幅提升可读性。如果只是"看起来更酷",先观察一两个项目确认效果再决定。
5. 风格指南的扩展玩法:从代码规范到工程文化
风格指南写到后面,往往就不只是代码风格了,它会自然延伸出一套工程文化。你会发现团队开始讨论依赖管理、目录结构、模块划分这些"看起来跟风格无关"的话题,然后所有这些都被沉淀进一份活的文档里。
5.1 与代码评审流程融合
我的经验是,风格指南不能独立于评审流程存在。如果评审不看风格条款,指南就是空中楼阁。所以我在团队里把风格检查和功能评审分成了两道工序:第一道由自动化工具跑,阻塞所有格式问题;第二道由人工 reviewer 看,只关注逻辑、架构、边界情况。
这样一来,人工评审的负担降低了至少三成。原本 15 分钟的 style review 时间可以全部省下来去做更深入的逻辑review。团队成员对这种"先机器后人类"的流程普遍反馈良好。
5.2 让新成员快速上手的 onboarding 文档
风格指南还有一个隐藏价值:它是新成员入职学习的最佳切入点。新人通过读风格指南,一方面能快速了解团队对代码的期望,另一方面也能借此熟悉项目的模块划分和常用库。
我在团队的 wiki 里把风格指南设成了新人必读文档的 Top 3 之一,并配了一个小型练习题:让新人对一个不规范的代码文件做一次完整的风格修正,commit 到 PR 里。这个练习看似简单,实际上能帮助新人掌握 clang-format 的用法、熟悉代码提交流程、还能在第一次 PR 里就跟 reviewer 建立沟通渠道,一举三得。
5.3 从代码风格到 API 设计风格
等团队对基本风格形成肌肉记忆后,可以尝试把规范扩展成 API 设计风格指南。比如约定:
- 所有返回错误信息的函数统一返回
std::expected<T, Error>,替代裸返回码或异常。 - 所有配置参数的构造函数统一接收一个
Config结构体,而不是十来个散参数。 - 类只暴露最小接口,私有成员尽可能多。
这些约定已经超出了"排版和命名"的范畴,但它们同样是在降低整个项目的认知复杂度。从风格指南走向 API 设计指南,是团队的代码规范从"表面整洁"走向"架构整洁"的必经之路。
在整理 lil_tea 这份 C++ 风格指南时,我反复提醒自己一件事:规范是给人服务的,不是人去伺候规范。过度细节的风格强制、为了统一而统一的教条,反而会降低生产力。真正高价值的风格指南是那种拿起来就能用、用了能少挨骂、遇到特殊情况又允许你说"这次我破例"的文档。保持轻量、保持聚焦、保持工具的自动化程度够高,这套东西才能在日复一日的提交里真正活下去。
