如何写C++而不是C with Class
前言
如何写 C++ 而不是 C with Class
这是我当初在知乎上提出的一个问题,很感谢各位大佬的回答,也感受到了自己的不足,所以这几个月我强迫自己写了不少 C++ 代码,并且认真阅读了 Effective C++ ,个人感觉真是重新学习了 C++ 这门语言。
但是前几天跟别人说我最近在学 C++ ,他一脸诧异的说“那不是大一就学过了吗?”,转念一想,的确是这样,那我这段时间在学什么呢?我沉默了半分钟,给出的回答是“我觉得我想写更优美的 C++ ”。
事实上, C++ 并没有我想象的那么优美,但是 C++ 的确是一门很有魔力的语言,它即让你能看到底层的各种操作,同时也有能力对接一些高层应用,算是一门承上启下的语言。写C++,我能完全知道我的代码在做什么,这大概有一种安心感。
说了这么多,本文就是想在回答这个问题的基础上写一写我认为 C++ 中比较重要的概念和关于 C++ 的一点自己的思考。
C++ 是多范式语言
首先要说的是这个问题就是错的,而且错的很离谱。
因为C++是一门多范式语言, C with Class 本身就是范式的一种,它就是 C++ ,本来就不应该被否定。这里我们可以参考 Effective C++ 的条款1:视 C++ 为一个语言联邦。
最简单的办法是将 C++ 视为一个由相关语言组成的联邦而非单一语言。在其某个次语言(sublanguage)中,各种守则与通则都倾向简单、直观易懂、并且容易记住。然而当你从一个次语言移往另一个次语言,守则可能改变。为了理解 C++ ,你必须认识其主要的次语言。幸运的是总共只有四个:
- C ...
- Object-Oriented C++ ...
- Template C++ ...
- STL ...
这里省去了具体的介绍,其中第二条 Object-Oriented C++ 就是 C with Class。我想我当时比较憧憬的 C++ 可能是 Template Meta Programming,那么原问题改为
如何写Template C++而不是C with Class
就对了吗?我认为不是。
正如条款中提到的, C++ 是一个语言联邦,这个问题从一开始就错了,不同的次语言是面对不同需求而言的,如果 C with Class 能解决问题那么就没必要强行塞上模版。举一个非常极端的例子,对于 ACM 代码 C 明显太过笨重,而比赛中一般也没必要 OOP ,模版编程更是开玩笑了,显然 STL 风格大多时候是比较合适的,编写速度快而且鲁棒性又高,何乐而不为?
而且还有一个问题是过度设计,这个问题我相信如果写过Java可能会比较有感受。很多时候能解决问题的设计往往就是 Best Practice ,过分强调可扩展性和灵活性反而会给设计带来过多负担,也浪费了性能和脑力(笑)。当然也不是说不设计,只是设计应该契合需求。
所以 C++ 是多范式语言,我觉得无论何时都应该牢记这一点,这也是 C++ 灵活性的体现。
RAII
说实在话,在提出这个问题之前我对 RAII 闻所未闻,在我眼里 C++ 中的 new 和 delete 跟 C 的 malloc 和 free 除了是运算符可以重载以外毫无区别。可以说 C/C++ 对内存管理的零容错性是我之前对 C/C++ 反感的一个重要原因,不过接触到 RAII 后问题迎刃而解。
首先 RAII 的初衷其实已经提到了,就是 C++ 中繁琐的内存管理问题。在 C++11 以后,标准库从 boost 抄来了智能指针,颇有一种“十月革命一声炮响,送来了马克思主义”的感觉,命中了书写 C++ 的一大痛点,因为之前只有 auto_ptr 实在是不太方便。
智能指针为 C++ 提供了方便的内存管理机制
1 |
|
这种感觉真是太美好了,我甚至有一种我在写Java的错觉(笑)。当然智能指针更重要的地方在于异常安全性,也就是说保证资源一定不会泄露。
OOP
面向对象三大特性:封装,继承,多态。
我一度以为面向对象也不过如此,但是 Effective C++ 让我重新认识了这三点。
封装
Effective C++ 关于封装的阐述有一个小地方是让我最震惊的,那就是
protected 并不比 public 更有封装性
这可以说非常反直觉,但是一旦一个 protected 修饰的变量发生变化,那么不仅当前类要重写,所有的派生类都要修改逻辑,想想就很恐怖。也就是说从代码维护性的角度, protected 本身是跟“高内聚,低耦合”这个原则背道而驰的,它提供的封装性可能比 public 还要差。
于是 Effective C++ 提出了结论,封装只有两种形式:不封装(public)或者封装(private),这是符合一个良好的设计的要求的,但是摆脱 protected 的确是一个痛苦的过程。
继承
在继承上我发现了 C++ 也是一门非常依赖设计的语言,甚至不亚于 Java,不过 C++ 要明显更灵活一些。
但是我觉得比较遗憾的是 C++ 没有接口,因此还要费尽心机的去区分接口继承和实现继承的问题,这时候我就开始怀念 Java 的 interface 了。
多态
首先能用RTTI解决的问题都是可以用多态解决的,这是个设计问题。
但是考虑另一个问题,面对同样的需求,除了多态我们还有什么选择? Effective C++ 给出了答案
- non-virtual interface(NVI) : 核心思想是用 public non-virtual 函数包裹权限较低的 virtual 函数,我觉得从封装上来说这是一个非常好的设计模式,但是如果 virtual 函数的权限不得不变成 protected 或者 public 的时候就要重新考虑了。
- Strategy : 核心思想是用函数指针来完成 virtual 的功能,我个人比较反感这种方法,因为给人一种在写 C 的错觉。
- 古典 Strategy : 核心思想是把 virtual 函数转到另一个专门的体系中,然后保存这个体系的指针来实现多态,这种设计模式优点在于可以代码复用,因为多态体系的代码是模块化的。
函数式编程
函数式编程我最近尝试在 Python 中使用了不少,但是我的确不太习惯用递归的思维来思考迭代,总之还是需要多练习。
但是在 C++ 中, lambda 和 function 满天飞虽然不是函数式编程,但是真的很爽呀(逃
STL
STL 用多了之后我觉得真是爱不释手,原因我觉得可以用 Python 之禅中的三句概括
- Simple is better than Complex.
- Explicit is better than implicit.
- Flat is better than nested.
STL 中几乎每个函数都是精心打磨过的,而且它们的功能都非常明确,不会有任何模糊。另外最让我喜欢的地方在于,STL几乎完全做到了“高内聚,低耦合”,简直像一个艺术品一样。
Boost
因为接手了CTBX所以被迫接触了Boost,所幸现在有 vcpkg 所以安装过程非常顺利。
但是使用 Boost 的直观感受就是,相比 STL 差了太多,不够简洁不够优美,另外官方文档质量也比较感人,我基本都是一边参考文档一边看源代码用 Boost 的,体验太差。
不过不得不承认的是 Boost 大大提高了 C++ 的扩展性和表达能力,难怪 C++ 喜欢从 Boost 抄(逃。
模版元编程
虽然一开始憧憬的是模版元编程,但是到现在为止我写的含有模版的 C++ 代码非常少,对于模版元编程也只有个概念,希望下一本《STL源码剖析》看完可以学习一点 STL 的模版技巧。
小结
看完 Effective C++ 并且自己动手写了不少后,我发现我的确是爱上了 C++ 这门语言,但是 C++ 博大精深,修行的道路还有很长呀。