如何写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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <memory>
#include <iostream>


class A {
public:
A() {
std::cout << "constructing A\n";
}
~A() {
std::cout << "deconstructing A\n";
}
};


int main(){
std::shared_ptr<A> p = std::shared_ptr<A>(new A());
return 0;
}

这种感觉真是太美好了,我甚至有一种我在写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++ 博大精深,修行的道路还有很长呀。