浅论 C++ 的复杂性

C++ 拥有 30 多年的历史,作为一门影响广泛的编程语言,它所受到的关注和争论恐怕是任何一门其他的语言所不能比拟的。十几年前,Java 等新生语言的出现曾导致“C++信任危机”,但最终 C++ 以自身非凡的品质屹立于主流编程语言的行列。在有着众多编程语言可以选择的今天,到底有没有必要学习、怎样学习、怎样使用 C++?对于广大程序员,特别是刚刚接触编程的初者,这些问题至关重要。

C++ 遭受批评最多的是它的复杂性,对于这个话题,已经有很多文献讨论过。在这里不想提出什么新的观点,或者根本就提不出什么新的观点,只是想把自己的感触简要地和大家谈谈。

1.C++ 复杂吗

这个问题的答案是肯定的,C++ 真的很复杂。从 C++ 的发展和组成来看,C++ 并不是一种单一、纯粹的编程语言,它有着复杂的内部结构。

最初,C++ 仅仅是在 C 的基础上附加了一些 object-oriented(面向对象)的特性。C++ 最初的名字是“C with Class”。以后 C++ 不断创新和发展,融入了 procedural(过程化),object-oriented(面向对象),functional(函数化),generic(泛型)以及 meta-programming(元编程)特性。这些能力和弹性使 C++ 成为强大而又复杂的工具。

面对如此复杂强悍的编程语言,我们该如何理解它和学习它呢?最简单的方法就是将C++视为一个由相关子语言组成的联合体。在每一个特定的子语言中,它的特性趋向于直截了当,简单易记。但当你从一个子语言跳转到另一个子语言的时候,它的规则就会发生变化。C++ 的子语言有 4 个。

  • C

归根结底 C++ 基于 C 发展而来,blocks(模块)、statements(语句)、preprocessor(预处理器)、built-in data types(内建数据类型)、arrays(数组)、pointers(指针)等,全都来自于 C。在很多方面,C++ 提出了比相应 C 版本更高级的解决问题的方法,例如内联函数、引用、函数和操作符重载等。这些特性能够和传统的 C 很好地结合在一起,可以视为对 C 的扩充,体现了C++ 的 “A better C” 的初衷。

  • Object-Oriented C++

面向对象就是 C with Classes 涉及到的全部:classes(类)、encapsulation(封装)、inheritance(继承)、polymorphism(多态)、virtual functions(虚函数)等。C++ 这一部分直接用于 object-oriented design(面向对象设计)的经典规则。

  • Template C++

这是 C++ generic programming(泛型编程)部分,大多数程序员对此缺乏经验。Template(模板)的考虑已遍及C++,而且好的编程规则中包含特殊的template-only(模板专用)条款已经不再不同寻常。实际上,tempalate(功能)极为强大,它提供了一种全新的programming paradigm(编程范式)—— template metaprogramming(TMP,模板元编程)。

  • STL(Standard Template Library,标准模板库)

STL 是标准模板库,看名称就知道,它是非常特殊的一个。它对容器(container)、迭代器(iterator)、算法(algorithm)以及函数对象(function objects)的规约有极佳的紧密配合与协调。但是 templates 即程序库也可以以其他的方式建立起来。STL 有很多独特的处理方法,使用 STL 编程时,需要遵循它的规则。

C++ 的四种子语言(sublanguages)紧密地结合在一起,但它们的确又有各自鲜明的风格。当从一种子语言转到另一种时,为了搞笑编程时需要改变编程的策略,这是C++程序员可能遇到的情形,对此必须有心里准备。例如,使用 built-in(内建)类型时,pass-by-value(传值)通常比pass-by-reference(传引用)更高效。但当从 C++的 C 部分转移到 Object-Oriented C++(面向对象 C++),由于传值调用会导致建立参数的副本,调用用户自定义的构造函数和析构函数会降低效率,所以更好的做法是传 const 引用。在 Template C++ 中工作时,这一点更加重要。因为在这种情况下,你甚至不知道你的操作涉及到的对象的类型。然而,当你进入 STL,由于iterator(迭代器)和 function objects(函数对象)以 C 的pointers(指针)为原型塑造出来的,所以对 STL 的迭代器和函数对象而言,旧式 C 中的 pass-by-value(传值)规则又重新生效。

因此,C++ 不是使用一套规则的单一语言,而是由上面四个子语言组成的联邦语言。每一种都有自己的规则。有了这样的理解,就能更清楚地了解 C++ 的内部结构,并能根据不同的应用需求使用不同的子语言,充分发挥 C++ 语言的长处。

C++ 的的复杂性可以体现在以下三个方面:
(1)学习周期长。C++ 由于其内在的复杂机制,要想成为一名合格的 C++ 程序员,业界的规律是 3 到 5 年。

(2)开发效率低。主要是历史原因,C++ 诸多库都停留在很低的层次上,使用的便利性无法与RAD工具(Rapid Application Develop,快速应用开发)相提并论。有人提议将 C++ 库的层次提高,但是这是一件非常艰难的工作,因为这与 C++ 的设计理念冲突,C++ 希望最大限度地保持通用性和底层性。

(3)容易犯错,维护难度大。C++ 是一种功能强大且自由度极大的语言,使用C++的过程中一不小心就犯下错误,留下代码漏洞,特别对于初学者,要能够自如高效的使用C++语言需要很长时间的磨练。

2.C++ 复杂的原因

C++ 复杂的真正原因是什么?对此,仁者见仁智者见智。因为是学院派的东西吗?不,学院派的出来的东西就一定复杂吗?这个理由站不住脚。

经历三十多年的发展,C++ 的触角已经遍及了当今世界学术、工业界的方方面面,体积虽然庞大,但结构却很清晰,C++ 并不因此而复杂。

C++ 是因为支持的编程范式太多了吗?也不是,新生的语言几乎都在走 C++ 的成功范式,Python 和 Ruby 等新型动态语言的范式甚至更多,然而它们却以简单和开发效率高而著称、其实C++ 正真复杂的原因是坚守三大原则决不妥协。一是对 C 的完全兼容,而是静态类型检查,三是最高性能。其中高性能又是这三大原则中的重点,既要发展新特性,同时又要保持高性能,这是C++ 语言复杂性的根本原因。

C++ 没有采用一些可能会降低程序性能的做法,如采用垃圾回收机制等。而这些做法有可能降低 C++ 的复杂性。C++ 之父 Bjarne Stroustrup 教授在多种场合下表示,对 C++ 的设计没有大的后悔之处,原因在于对三大原则的坚持首先是正确的,若坚守三大原则,即使重新设计 C++,结果也与今日相差不远。

3.需要学习和使用 C++ 吗

既然 C++ 如此复杂,还有必要学习和使用 C++ 吗?

对于这个问题,无法给出强制性的回答。在这个世界上,一定存在从来不用 C++ 编程能够出色完成特定编码工作的程序员,也许他们所使用的语言就是 Java、Python、C#、Go 或者其他编程语言。但是我的建议是需要就用,不需要就不用学。但是 C++ 是一门优秀且值得学习的语言,原因是 C++ 具有如下特性。

  • C++ 是一门贯通低级到高级的语言

C++语言向下兼容C语言,能够直接通计算机的硬件和底层打交道,甚至能够直接使用内联汇编。向上,C++语言是4中子语言的而结合体,它所能支持的特性的丰富程度也是其他语言所难以企及的。对于一个能够静下心来,能够持续持续不断努力提升自己对计算机系统理解程度(计算机体系结构、硬件、操作系统、应用开发、软件项目和过程管理)的程序员来说,C++语言是一个绝佳的选择。

  • C++ 是一门高效的语言

C++ 程序的执行效率与 C 语言相当,同时又提供了诸多的高级特性。这样,C++ 语言为程序员创造了这样一种可能:在利用各种高级特性(面向对象方法、泛型编程等)充分表达设计思想、解决各种复杂问题的同时,保持应用程序的高效运行。这也是其他编程语言难以做到的。

  • C++ 是一门复杂的语言

这个观点听起来有些怪异。C++语言的复杂性往往是造成人们放弃 C++ 的原因,但同时,C++语言的复杂性也可能成为人们选择 C++ 的原因。C++ 的先驱大师 Andy Koenig 在他的《C++沉思录》里回击了对 C++ 复杂的攻击。他认为,选择什么样的编程语言,取决于要解决的问题。世上没有万灵药,要解决复杂的问题,必然要依赖于复杂的工具。C++ 程序员是实用主义者,他们首先保证问题能被解决,其次才能谈得上其他。实际上,要解决的问题是复杂的,计算机系统使不完美的,人类的自然语言体系和表达习惯就更是不完美的。而一门成熟的通用编程语言,要在这三极之间保持平衡,谈何容易。Java语言通过削减矛盾(用虚拟机代替真实机器),削减表达能力来获得简单性,这也同时限制了它在实时性高计算密集的领域里得到应用。无论是调度仿真、实时控制还是媒体编辑,一旦触及重量型的关键应用,除了 C++ 你别无选择。C++ 的复杂性源于对其高效解决问题的承诺。这就好比现实生活中,思想简单的人不能委以重任。

  • C++ 是一门成熟的编程语言

这并不是说其他的编程语言不成熟。成熟是一种相对的概念。C++ 在其 30 多年的发展和使用过程中,开发了无数成功的软件系统,积累了丰富的成功案例和可重用资源。其数量之大,应用之广,影响之深,也是首屈一指。有兴趣的读者可以光临 Bjarne Stroustrup 教授的主页,了解一下C++ 语言在业界创造的辉煌成绩。

4.如何应对 C++ 的复杂性

尽管 C++ 的复杂性有其产生的深刻背景,但复杂性确实是个问题。在实践上最突出的表现就是开发效率的降低,毕竟简单易用的工具能带来生产率的提高。但是 C++ 的复杂性导致了开发效率的降低只是一种表象,它是没有对复杂性进行有效控制而产生的后果。换句话说,问题不在于 C++ 的复杂性,而在于使用 C++ 的人有没有有效控制这种复杂性。

那么,如何应对 C++ 的复杂性,下面给出几点建议。

(1)用沉稳的心态去学习 C++
学习编程语言,掌握语法,能上手实践,不过是万里长征迈出了第一步。更何况想 C++ 这样的语言,要做到掌握各个子语言的基本内容,都不是一件容易的事情。所以心态一定要平稳,着急不来,更不可轻狂。要真正掌握语言,非得集中精力学习实践一两年,将该语言所擅长领域的问题过一遍,才有可能。若论精通,那是一个没有止境的过程,Henry Spencer 用了 30 年C,仍乐此不疲。《The Pragmatic Programmer》 一书中评价 Ruby 说到,学上四个小时就可以用它解决实际问题,但是 10 年之后还为它层出不穷的特性感到惊讶。真正掌握 C++ 语言之后,再熟悉一两门层次不同、思维不同的语言,那就是更高层次的追求了。

要注意的是,这也是一个心理学的问题。C++ 语言中总是存在着一些新奇的特性,它会引起你强烈的兴趣,将你的注意力从真正有用的事情中分离出来。这些被称之为“奇技淫巧”的东西即使能短暂给你带来自豪感,但是不应该成为你学习C++的主流。要注意,不要为了使用每一种特性而去使用,要根据实际问题和项目的需求去应用C++的特性。

(2)采用科学的学习方法
全面掌握 C++ 固然重要,但不等于说只有掌握了 C++ 全部特性才能用来它解决问题。你可以把你对 C++ 的理解限制于一个相对简单的程度,只要你需要解决问题的复杂度不超过你所掌握的工具的复杂度。初学者要把 C++ 分为逻辑层次上、难度比较独立的部分,根据自己的需要循序渐进地的学习,利用每一部分所学解决能够解决的问题,只有这样,才能学得扎实。不要怕碰到问题,从某种角度来说,遇到问题是好事,因为这是弥补自己知识盲点的机会。自己不懂得东西太多了,只是还未暴露出来,解决了问题,你就学到了东西。

(3)正确地使用 C++
C++ 被错误地使用是一种很普遍的现象,这也是 C++ 遭受“过于复杂”的抱怨的真正原因。C++ 由 4 个子语言组成,C++ 提供了如此丰富的特性和自由度。如何选这些特性体现了 C++ 程序员的真正功力和成熟度。

首先,要小心选择你所使用的子语言。例如 C++ 向下兼容 C,那么是不是任何场合下,都要使用 C++ 面向对象的特性呢?或者无论什么情况下,都选择 C ,因为 C 更简单?这是不可取的一刀切的思维。显然 C 有自己擅长的领域,比如设备驱动开发、操作系统的大部分工作都不需要 OOP/GP( Object Oriented Programming/Generic Programming)。然而,在更多领域,抽象与效率是并重的,这些正是 C++ 面向对象的特性适用的场合。

其次,充分利用现有的、经过实践检验的资源。代码重用是现代软件工程提倡的一种做法,不仅因为它可以提高开发效率,还因为它可以降低程序的复杂程度。如果一个高效的容器(或智能指针)能把你从无聊的手动内存管理中解放出来,为啥还要用那原始的malloc/free呢?如果一个好的string类或正则表达式类能把你从繁琐的字符串处理中解脱出来,那么为啥要手动去做这些是呢?如果一个transform(或for-each)能够用一行代码把事情漂亮搞定,为啥还要手写一个for循环呢?

再次,控制你代码的复杂程度。C++ 不是为了复杂而复杂,而是因为要解决复杂的问题而引入了复杂的机制。问题的关键在于,程序有时是自己把问题搞复杂了。例如在 C++ 中,一个普通程序员很可能会写出一堆高度耦合的类,很快情况就变得一团糟。但这不是 C++ 的问题,这种情况很可能发生在任何一门面向对象语言中,因为总有程序员在还没有弄懂什么是 has-a 和 is-a 之前,就敢于在类上再写类,就这样一层一层的堆砌上去。它们学会了在一门特性语言中如何定义类,如何继承类的语法,然后就认为自己已经掌握了 OOP 的精髓了。

由于 C++ 如此灵活,很多问题在 C++ 中都有好几种解决办法,于是在这些选择中进行权衡本身就成了一个困难这也是得程序员犯错误的可能性增加了。所以掌握一门优秀的设计思想(比如说优先使用组合而不是继承),或者遵循C++社群这些年积攒下来的只会,或者说干脆只使用 C++ 语言中 C with Class 部分以规避复杂性的风险,都是程序员需要不断学习和不断实践的。

总之,正确使用 C++ 所应遵循的原则是:了解 C++ 的高级特性,用简单的方法解决复杂的问题,即将复杂的解决方案包装在简单的形式之下,重用前人的劳动成果,遵循最佳的实践。


参考文献

[1] 陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.P419-423
[2] Scott Meyers(著),侯捷(译).Effective C++[M].北京:电子工业出版社,2011:11-13

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页