我的 C++ 编码规范
一、为什么我开始在意编码规范
刚开始做算法题时,我对代码的要求其实很简单:能 AC 就行。
为了尽快通过测试点,变量名可以是 a、b、tmp,几行逻辑能挤在一起就挤在一起,遇到稍复杂的题目再补几句只有自己看得懂的注释。当时觉得这样写得快,也没有什么问题。
但随着学习的深入,我逐渐发现:代码写完的那一刻,并不是它生命的终点。
一道题隔几天再回来看,曾经觉得“一眼就懂”的代码,可能已经需要重新推一遍;程序出现边界错误时,过于随意的变量命名和排版又会增加调试成本。如果以后进入真正的软件项目,代码还需要被其他人阅读、修改和维护,那么“能运行”显然只是最低要求。
因此,这学期我开始尝试给自己的代码增加第二个评价标准:
不仅要让程序通过测试,也要让未来的自己能够快速读懂它。
二、命名:让变量自己“说话”
我以前很喜欢使用这样的变量:
int a, b, c;
int tmp;
bool flag;
代码刚写完时当然知道它们是什么意思,但问题是,这种信息只存在于写代码那一刻的记忆里,并没有真正存在于代码中。
因此,我给自己的第一条要求是:
能通过名字表达的信息,就不要依赖注释和记忆。
1. 类型和结构体
类型名称使用大驼峰命名,使其和普通变量明显区分。
struct TreeNode;
class DisjointSetUnion;
看到 TreeNode 时,不需要再额外判断它究竟是一个变量还是一种数据结构。
2. 普通变量
局部变量和函数参数使用小写加下划线的形式:
int max_capacity;
int current_size;
int target_value;
相比:
int m;
int s;
int x;
前一种写法虽然多敲几个字符,但在调试复杂算法时,这几个字符往往能省下更多时间。
当然,我不会为了“规范”而走向另一个极端。像循环变量 i、j,或者数学公式中含义非常明确的变量,没有必要强行写成长名称。
规范的目的应该是减少理解成本,而不是增加形式成本。
3. 常量
对于会影响算法逻辑的固定值,我尽量避免直接散落在代码中。
例如:
const int NOT_FOUND = -1;
const int INF = 0x3f3f3f3f;
这样做的意义并不仅仅是“看起来规范”。
假如某个特殊值以后发生变化,我只需要修改一个地方;同时,NOT_FOUND 显然也比孤零零的 -1 更容易理解。
4. 函数
函数名尽量体现“它正在做什么”。
例如:
BinarySearch();
QuickSort();
CheckCondition();
我希望看到一个函数名时,大致就可以判断它承担的职责,而不必立刻进入函数内部阅读实现。
三、排版:让代码的结构能被“看见”
代码的逻辑本身是抽象的,而排版实际上是在帮助我们把这种逻辑结构可视化。
因此,我给自己的第二条原则是:
排版不是装饰,而是程序结构的一部分。
1. 统一缩进
本学期自己的课程代码统一使用 4 个空格缩进,不混用 Tab 和空格。
例如:
if (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
}
即使不仔细阅读语句,也能通过缩进快速判断代码的层次关系。
2. 大括号保持统一
我统一采用下面这种形式:
if (condition) {
// ...
} else {
// ...
}
我认为具体选择哪一种括号风格并不是最关键的,真正重要的是:
一份代码里不要同时出现三四种写法。
一致性本身就是一种可读性。
3. 给代码“留一点空气”
把不同功能区域适当分开:
// 输入数据
...
// 核心算法
...
// 输出结果
...
一个空行看起来微不足道,但当代码达到几十甚至上百行时,这种视觉分区会明显降低阅读压力。
四、注释:解释“为什么”,而不是翻译代码
因为代码已经清楚地告诉读者 i 增加了 1。
真正值得记录的,往往是代码背后的原因。
例如二分查找中:
int mid = left + (right - left) / 2;
如果要写注释,更有意义的是:
// 避免 left + right 在极端情况下发生整数溢出
int mid = left + (right - left) / 2;
再比如动态规划,与其写:
// 更新 dp[i]
不如说明:
// 当前状态只可能由前一个合法状态转移而来
代码负责说明“做了什么”,注释主要解释“为什么这样做”。
五、比“代码好看”更重要的是:少制造 Bug
规范最终还是要服务于程序正确性。
尤其是 C++,很多问题并不会在编译阶段直接暴露,而可能表现为数组越界、整数溢出、空指针访问甚至 Segmentation Fault。
因此,我给自己增加了几条和安全有关的习惯。
1. 二分中点避免溢出
不写:
int mid = (left + right) / 2;
而写:
int mid = left + (right - left) / 2;
两者在普通数据下结果相同,但后一种方式对极端边界更加安全。
2. 注意数组边界
使用静态数组时,预留合理的安全空间,同时明确当前程序采用的是 0-based 还是 1-based 下标。
因为在算法题中,很多错误不是算法思想错了,而只是:
arr[n]
多访问了一次。
3. 指针必须明确初始化
例如:
TreeNode* root = nullptr;
而不是:
TreeNode* root;
当涉及动态内存时,也要注意对象生命周期,避免形成悬空指针。
4. 谨慎使用全局命名空间
在短小的算法练习中,使用:
using namespace std;
确实能减少一些输入量。
但在头文件、类封装或者规模更大的程序中,我更倾向于:
std::vector<int>
std::cout
std::cin
因为代码规模越大,名字冲突的风险也越值得注意。
六、我的理解:规范不是把所有代码写成一个样子
学习编码规范后,我反而觉得,规范最容易被误解的一点,就是把它当成一套必须机械执行的格式。
例如:
到底应该 2 个空格还是 4 个空格?
函数名究竟应该采用哪一种大小写风格?
一行最多应该写 80 个还是 100 个字符?
这些当然需要约定,但它们并不是编码规范最核心的部分。
我目前更看重的是三个问题:
第一,别人能不能快速理解我的代码?
第二,我一个月之后还能不能快速理解自己的代码?
第三,这种写法是否能够减少低级错误出现的机会?
只要始终围绕这三个问题,很多具体规范其实都可以根据项目和团队要求进行调整。
所以,我不会把自己现在制定的规范看成一份永久不变的“标准答案”。
它更像是我目前学习阶段的一份代码习惯清单。
八、从 AC 开始,但不止于 AC
算法课首先考查的当然是算法本身。
一道题连正确结果都得不到,再漂亮的格式也没有意义。
但我希望自己在追求 AC 之外,再多做一步。
写完代码以后,我会再问自己几个问题:
- 变量名是否能够表达真实含义?
- 有没有不必要的“魔法数字”?
- 边界条件是否清楚?
- 注释是在解释逻辑,还是只是在重复代码?
- 一周之后,我还能不能快速读懂它?
如果这些问题也能得到肯定答案,那么这道题带来的收获,就不再只是掌握了某一个算法。
算法训练解决的是“如何让程序做对一件事”,而编码规范训练的,是“如何把这件事清楚、可靠地表达出来”。
这学期,我准备把这套规范真正落实到 PTA 练习和课程实验中。
毕竟,AC 是一次提交的终点。
而写出别人能够继续读下去的代码,才是程序真正进入工程世界的起点。
浙公网安备 33010602011771号