Linux、PHP 和 Git 是用 C 开发的流行项目。另一方面,OpenOffice、Firefox、Clang 和 Photoshop 则是用 C++ 开发的——这说明两门语言都很适合开发复杂的应用程序。试图证明一门语言比另一门更好,也许不是最有意义的争论。不过,我们可以讨论选择其中一门而放弃另一门背后的动机。
当我第一次读到 Linus Torvalds 对 C++ 的看法时,作为一名 C++ 开发者,我完全不同意他的观点。但这是 Linux 内核和 Git 的首席开发者的视角,值得认真考量。
重读他的观点之后,我发现自己认同这一论断:
inefficient abstracted programming models where two years down the road you notice that some abstraction wasn't very efficient, but now all your code depends on all the nice object models around it, and you cannot fix it without rewriting your app.诚然,C++ 为编写优雅、结构良好的代码提供了更多可能,但这是有代价的:变更和重构可能很困难。然而,这并不意味着我就必须选择另一门语言。每门语言或每个库都涉及取舍;在多年开发之后,我们需要知道如何限制潜在变更对 C++ 代码的影响。
让我们用Git的源代码和CppDepend进行分析,识别一些设计特点,并在两个方面比较 C 和 C++:
- 易于理解。
- 变更管理。
模块化:物理方式与逻辑方式
模块化是一种软件设计技术,它能提高软件由独立部分组成的程度;模块化代码更易于管理和维护。
我们可以用两种方式对项目进行模块化:
- 物理方式:使用目录和文件。这种模块化形式由操作系统提供,可以应用于任何语言。
- 逻辑方式:使用命名空间、组件、类和结构体。这种技术取决于语言本身的能力。
用 C 开发时,我们主要使用物理模块化来组织代码。目录被用来隔离模块。下面是 Git 部分目录之间的依赖图:

然而,使用 C++,我们可以用命名空间来对代码库进行模块化;这些机制由语言本身提供。在前面的图中,那些图形可以换成命名空间而不是目录,来模块化我们的代码。
选择这两种方式之一所带来的影响:易于理解:逻辑方式更好,因为模块化由语言结构明确定义;只要读代码,我们就能知道某个代码元素属于哪个模块。
变更管理:好的设计通常需要多次迭代;使用物理方式时,设计变更的影响可能比逻辑方式小得多。我们可能只需要把一个函数或变量从一个文件移到另一个文件,或者把一个文件从一个目录移到另一个目录。
然而在 C++ 中,这样的变更可能影响大量代码,因为逻辑模块化是通过语言结构实现的,因此需要修改代码。
封装:类与文件
在 C++ 中,封装被定义为把数据和函数组合成一个称为类的单一单元的过程。有了封装,程序员无法直接访问数据;数据只能通过类内部的函数访问。
在 C 语言中,我们也可以实现封装,但采用的是模块化一节中描述的那种物理方式:一个文件可以包含函数及它们使用的数据,我们可以用 “static” 关键字限制函数和变量的可见性。
Git 使用这种技术来隐藏函数和变量。为了验证这一点,让我们搜索 static 函数:
from m in Methods where m.IsStatic select m
树状图对于概览 CQLinq 查询匹配到的代码元素非常有用;蓝色矩形代表查询结果。

几乎所有函数都声明为 static,因此它们只在声明它们的翻译单元中可见;变量也是如此。
from f in Fields where f.IsStatic select f
易于理解:使用 C++ 的封装机制可以改善代码的可读性和组织性;C 更底层,更多依赖物理方式而非逻辑方式。
变更管理:如果我们需要改变一个变量或函数的封装位置,在 C 中可能非常容易,而在 C++ 中则可能影响大量代码。
多态与选择(selection)惯用法
多态意味着代码、操作或对象在不同上下文中表现不同。
这种技术在 C++ 项目中被广泛使用,那 C 呢?
在过程式语言中,选择通过 “switch”、“if” 甚至 “goto” 等关键字实现,但这种技术往往会增加代码的圈复杂度。
让我们在 Git 源代码中搜索复杂函数。

尽管 Git 设计得很好,但许多函数可以被认为是复杂的。这部分是由于大量使用了 “if”、“switch” 和 “goto” 等控制流语句。而在 C++ 中,我们可以使用多态来降低代码复杂度。
易于理解:使用多态可以把特定的行为隔离到一个类中,从而提高代码的可读性和内聚性。
变更管理:用多态添加另一种行为,可能需要新增一个类;而使用选择惯用法,您只需要在 switch 语句中再加一个 case。
继承与组合
Git 主要使用结构体来定义函数所操作的数据。让我们搜索它使用的所有结构体:
from t in Types where t.IsStructure select t

有趣的是,几乎所有数据都包含在结构体中。为了验证这一点,我们可以搜索所有不在结构体内的非 const 公有基本类型变量:
from f in Fields where f.IsPublic && f.IsPrimitiveType
&& !f.IsStatic && !f.IsConst
select f

只有少数变量匹配这条查询,这是 Git 设计上的一个优点。
那么如何扩展一个结构体呢?在 C 中,我们可以使用组合,就像 “remote” 结构体那样——许多结构体都引用它。

然而在 C++ 中,我们还可以使用继承来扩展结构体;例如,known_remote 结构体可以继承自 remote。
易于理解:使用继承可以让数据关系更容易理解,但必须谨慎使用;它只应用于“是一个”(is-a)关系。
变更管理:继承会引入更紧的耦合,因此一次变更可能影响大量代码。
结语:
C++ 为编写优雅、结构良好的代码提供了更多可能,但这是有代价的:变更和重构可能很困难。
重构要求在修改之前先理解现有代码。C 程序可能更难理解,但更容易修改;而 C++ 项目可能比 C 项目更有结构,但在进行变更时可能需要付出更多努力。
我们如何限制 C++ 中变更的影响?
在使用 OOP 方法时,限制变更影响的一个好办法是应用设计模式,特别是松耦合和高内聚的原则,把变更隔离在特定区域内。
另一种有效的方法是采用泛型编程和现代 C++ 实践。泛型编程可以比 OOP 更灵活,并有助于限制对 C++ 代码的变更影响。
