性能与语言毋庸置疑,C++ 是一门注重性能的语言。如果你不需要性能,尤其当程序的运行时间远远小于写代码花的时间时,像 Python 这样的脚本语言往往是最佳选择。但是,反过来,如果你的应用程序属于计算密集或者内存密集型,特别是,当你的代码需要部署在多台服务器或者移动设备上的场合,使用 C++ 常常就完全值得了。在很接近底层的场合,如果内存和存储资源比较匮乏,C 也常常会是一个很好的选择;但如果你在资源方面不那么捉襟见肘的话,C++ 提供的零开销抽象,会让你的生产力有一个大幅度的提升。我们回顾一下 C++ 之父 Bjarne Stroustrup 老爷子对“零开销抽象”的解释:
你不用的东西,你就不需要付出代价。你使用的东西,你手工写代码也不会更好。
换句话说,我们是既要性能,也要抽象。当然,抽象从来不是没有任何代价的。对于 C++ 而言,至少语言的复杂性,会是这种抽象的代价。C++ 里“既要……又要……”的地方并不止前面一处。我们还想要初学者友好。我们还想要向后兼容性——几十年前的代码,仍然应该能够正确编译。显然,这些目标是有矛盾的,不可兼得——你不可能又支持很多抽象功能,又性能高,又对初学者友好,同时还一直保持向后兼容性……那我们该怎么办呢?洋葱原则老爷子对此问题的回答是使用洋葱原则。抽象层次就像一个洋葱,是层层嵌套的。在解决问题时,只要可能,你应该使用尽可能高级的抽象机制,利用比较简单的方式来解决问题。只有在因为性能之类的原因需要进一步优化时,我们才应该使用 C++ 提供的高级功能,在使用抽象机制的同时,进行项目相关的特殊定制。当然,人对抽象和性能的理解通常都是有限的,两者都要的话,复杂度通常会很高——因此,这种深度定制的后果往往就会像切洋葱一样,把自己的眼泪熏出来。拿老爷子的原话:“如果要完成的任务是简单的,那就用简单的方法做;当要完成的任务不是那么简单时,就需要更详细、更复杂的技巧或写法。这就好比你剥下了一层洋葱。剥得越深,流泪就越多。”根据洋葱原则,在学习 C++ 时,我们不应该从那些琐碎易错的细节学起,自底向上。相反,学习应当自顶向下,先学习高层的抽象,再层层剥茧、丝丝入扣地一步步进入下层。如果一次走太深的话,挫折可能就难免了。系统知识不过,C++ 是一门系统编程语言,写 C++ 我们几乎肯定会和系统底层打交道(否则可能就没有必要使用 C++ 了)。我们只能说,应当从高层开始学起;而不是说,我们不需要了解系统底层的细节。一般而言,系统的下面几个方面我们需要较早就接触到,否则很难对性能有很好的理解:
栈,以及栈内存和堆内存的区别
多级缓存架构
多线程和锁
构建过程
以“栈”为例,这是理解 C++ 里对象生存期的一个关键点。函数的调用信息在栈上,本地变量在栈上,函数返回时所有的本地变量都会被销毁,内存被回收。构造和析构以后进先出的“栈”顺序进行,高效而确定。C++ 里最重要的惯用法,RAII(resource acquisition is initialization),也就顺理成章地出现了。同时,理解了这些之后,为什么返回本地变量的引用或指针是未定义行为,也会非常容易理解。测试与优化一般而言,指令执行少的代码更快,我们分析算法使用的大 O 表示法也是从这个角度考虑性能的。但实际的项目里,使用这种方式来分析性能可能存在困难。比如: