每个项目都有自己的风格指南:一套关于代码应如何编写的约定。有些管理者选择基本的编码规则,有些则偏好更高级的规则。然而,在许多项目中根本没有指定任何编码规则,每位开发者都各行其是。
当代码库中的所有代码都以一致的风格编写时,理解一个大型代码库要容易得多。
关于编码最佳实践的资源有很多。我们可以通过以下方式学习良好的编码规则:
- 阅读书籍或杂志。
- 利用在线资源。
- 向同事学习。
- 参加培训课程。
我们也可以与一位专家共事几个月来提升团队的编码技能。然而,找到合适的人选并不容易,而且可能会让公司花费不菲。但既然我们可以向 Linus Torvalds 这样杰出的开发者学习,又何必另寻专家呢?只需研究他开发或维护过的源代码,就能获得关于如何编写高效 C 代码的宝贵洞见。
Linus Torvalds 因创建 Linux 内核并多年担任其首席开发者而广为人知,他还创建了极受欢迎的分布式版本控制系统Git。单凭这一点,他的代码就非常值得研究 :)
走进 Git 源代码
让我们来看一段 Git 的代码片段:

关于这段代码,有以下几点观察:
- 函数声明为 static。
- 函数返回错误码。
- 函数参数很少。
- 函数尽早退出。
- 变量声明为 static。
- 变量名易于理解。
- 函数非常短小。
- 代码缩进良好。
- 函数体内没有多余的注释:代码自己会说话。
- 函数体缩进良好。
- 头文件保护宏(define guard)清晰明了。
浏览 Git 源代码时,我们可以看到它的实现是多么一致:同样的最佳实践贯穿始终。为了验证这一点,让我们搜索 static 函数:
from m in Methods where m.IsStatic select m
树状图(treemap)对于概览 CQLinq 查询匹配到的代码元素非常有用;蓝色矩形代表查询结果。

几乎所有函数都声明为 static,因此它们只在声明它们的翻译单元中可见。
走进 Linux 内核源代码
让我们转到 Linux 源代码,看看下面这个函数的实现:

代码看起来非常整洁。具体来说,这个函数
- 只有寥寥几行代码。
- 它的签名定义得很清晰。
- 注释写得很到位。
- 代码缩进良好。
- 变量名非常清晰。
- 遵守了 const 正确性。
- 它会检查输入参数,并在参数不满足某些条件时发出警告。
另一位开发者可能会用这样的方式实现同样的函数:

编码风格对源代码的可读性影响巨大。投入几个小时进行开发者培训,并定期开展代码评审,可以让代码更易于维护和演进。
让我们使用CppDepend来探索 Linux 内核源代码,发掘其开发者所采用的一些基本编码规则。
模块化
模块化是一种软件设计技术,它能提高软件由独立部分组成的程度;模块化代码更易于管理和维护。
对于 C 这样没有命名空间、组件或类等逻辑构造的过程式语言来说,模块化可以通过目录和文件来实现。
以下是几种可能的做法:
- 把所有源文件放在一个目录里。
- 将与某个模块或子模块相关的文件隔离到特定目录中。
在 Linux 内核中,开发者使用目录和子目录来对内核源代码进行模块化。
封装封装是指隐藏实现内部的函数和数据。在 C 语言中,封装通过关键字 static 实现。这些实体称为文件作用域的函数和变量。
让我们执行以下 CQLinq 查询,搜索所有 static 函数:

我们可以使用度量视图(Metric view)来直观地了解有多少函数符合。在度量视图中,代码库以树状图表示。树状图是一种用嵌套矩形展示树形结构数据的方法。CppDepend 树状图使用的树结构就是常见的代码层次结构:
- 项目包含目录。
- 目录包含文件。
- 文件包含结构体、函数和变量。
树状图视图为展示 CQLinq 查询结果提供了一种实用的方式,让我们可以直观地看到查询涉及的类型。

可以看到,许多函数都声明为 static。
现在让我们搜索 static 字段:

与函数一样,许多变量也声明为 static。
在 Linux 内核源代码中,只要函数和变量需要在文件作用域内保持私有,就会使用封装。
用结构体存储数据模型
在 C 语言编程中,函数使用变量来完成处理;这些变量可以是:
- 静态变量。
- 全局变量。
- 局部变量。
- 结构体中的变量。
每个项目都有自己的数据模型,可能被许多源文件使用。使用全局变量是一种选择,但通常更好的做法是把相关的数据归入结构体。
让我们搜索基本类型的全局变量:

符合条件的变量只有寥寥几个,而其中一些也许可以归入结构体,比如(elfcorehdr_addr 和 elfcorehdr_size)或(pm_freezing 和 pm_nosig_freezing)。
让函数短小精悍
下面是来自Linux 编码风格网页关于缩进的说明:
Functions should be short and sweet, and do just one thing. They should
fit on one or two screenfuls of text (the ISO/ANSI screen size is 80x24,
as we all know), and do one thing and do that well.
The maximum length of a function is inversely proportional to the
complexity and indentation level of that function. So, if you have a
conceptually simple function that is just one long (but simple)
case-statement, where you have to do lots of small things for a lot of
different cases, it's OK to have a longer function.让我们搜索代码超过 30 行的函数。

只有少数方法的代码超过 30 行。
函数参数数量
NbParameters > 8 的函数可能调用起来很痛苦,还可能降低性能。另一种替代方案是提供一个专门用于传递参数的结构体。

只有两个函数的参数超过八个。
局部变量数量
NbVariables 大于 8 的方法可能难以理解和维护。NbVariables 大于 15 的方法则极其复杂,应当拆分成更小的方法,除非它们是由工具自动生成的。

只有五个函数的局部变量超过 15 个。
避免定义复杂函数
有许多度量可用于识别复杂函数;代码行数(NBLinesOfCode)、参数数量和局部变量数量是其中最基本的几个。
还有其他一些有用的度量可以识别复杂函数:
- 圈复杂度是一种流行的过程式软件度量,等于一个过程中可能发生的决策数量。
- 嵌套深度是定义在方法上的度量,表示方法体内嵌套最深的作用域的深度。
- 最大嵌套循环数等于函数中循环嵌套的最大层数。
这些度量的最大可接受值在很大程度上取决于团队的偏好;没有放之四海皆准的阈值。
让我们搜索可以作为重构候选的函数:

只有极少数函数可以算作复杂函数。
命名规范
不存在通用的命名规范;每个项目都可以采用最适合自身需求的规范。最重要的是始终如一地应用所选规范。
在 Linux 中,结构体必须以小写字母开头。我们可以检查整个内核源代码是否都遵守了这一约定——执行以下查询:

只有四个结构体以“_”而非小写字母开头。
缩进
缩进对于让代码易于阅读非常有用;以下是来自Linux 编码风格网页关于缩进的说明:
Rationale: The whole idea behind indentation is to clearly define where
a block of control starts and ends. Especially when you've been looking
at your screen for 20 straight hours, you'll find it a lot easier to see
how the indentation works if you have large indentations.
Now, some people will claim that having 8-character indentations makes
the code move too far to the right, and makes it hard to read on a
80-character terminal screen. The answer to that is that if you need
more than 3 levels of indentation, you're screwed anyway, and should fix
your program.结语探索知名的开源项目是提升编程技能的绝佳方式,尤其当这些项目由专家开发和维护时更是如此。您甚至不需要下载和构建项目——直接在 GitHub 上浏览代码即可。
