编码规范

我的 C++ 编码规范

一、为什么我开始在意编码规范

刚开始做算法题时,我对代码的要求其实很简单:能 AC 就行。

为了尽快通过测试点,变量名可以是 abtmp,几行逻辑能挤在一起就挤在一起,遇到稍复杂的题目再补几句只有自己看得懂的注释。当时觉得这样写得快,也没有什么问题。

但随着学习的深入,我逐渐发现:代码写完的那一刻,并不是它生命的终点。

一道题隔几天再回来看,曾经觉得“一眼就懂”的代码,可能已经需要重新推一遍;程序出现边界错误时,过于随意的变量命名和排版又会增加调试成本。如果以后进入真正的软件项目,代码还需要被其他人阅读、修改和维护,那么“能运行”显然只是最低要求。

因此,这学期我开始尝试给自己的代码增加第二个评价标准:

不仅要让程序通过测试,也要让未来的自己能够快速读懂它。


二、命名:让变量自己“说话”

我以前很喜欢使用这样的变量:

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;

前一种写法虽然多敲几个字符,但在调试复杂算法时,这几个字符往往能省下更多时间。

当然,我不会为了“规范”而走向另一个极端。像循环变量 ij,或者数学公式中含义非常明确的变量,没有必要强行写成长名称。

规范的目的应该是减少理解成本,而不是增加形式成本。

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 是一次提交的终点。

而写出别人能够继续读下去的代码,才是程序真正进入工程世界的起点。

posted @ 2026-09-23 13:16  steve0v0  阅读(0)  评论(0)    收藏  举报