jiangbinliu的专栏

首发地址:

C++的亲爸爸写了一系列博客:

在这些博客里, 他讲了关于C++的5个众所周知的 误解, 看了之后, 我受益良多. 我深深反省, 以往我不喜欢C++, 是因为我不懂 C++, 又自以为很懂C, 而且盲目迷信 Linus. 现在用 C++ 做 leetcode, 很顺手.

选译如下

迷思一 要懂C++, 先得懂C

不是这样的. 基本的C++编程可比C简单多了.

C是C++子集. 但并不是最易学的子集. C缺少 运算符重载 和 类型安全, 标准库比较原始. 而 C++ 的标准库可以让简单的工作保持简单. 考虑一个小函数 构造一个电子邮件地址:

string compose(const string& name, const string& domain){ return name+’@’+domain;}

这个函数的使用方式是这样的:

string addr = compose(“gre”,”research.att.com”);

C版本的需要显式操作字符串和内存.

char* compose(const char* name, const char* domain){ char* res = malloc(strlen(name)+strlen(domain)+2); // space for strings, ‘@’, and 0 char* p = strcpy(res,name); p += strlen(name); *p = ‘@’; strcpy(p+1,domain); return res;}

用起来是这样的:

char* addr = compose(“gre”,”research.att.com”);// …free(addr); // release memory when done

如果你是一个老师, 你愿意教学生哪种版本? 如果你是个学生, 你愿意使用哪个? 上面的C版本真的正确吗? 为什么?

最后, 你猜那个版本性能好? 当然, 是C++版的. 因为不需要计算字符串的长度, 也不需要在堆上分配空间.

一,1 学习 C++

上面的例子不是孤例, 而是很典型的例子. 那为什么很多老师还在先教C呢?

然而, C并不是C++易学易用的部分. 在学通了 C++ 之后, C子集也就好学了. 在 C++ 之前学C就意味着要再次忍受C的缺陷, 而这些都是C++中已经避免了的.

有了 C++11 之后, C++ 变得更易入门. 举个例子, 这是标准库 vector 的初始化方式.

vector<int> v = {1,2,3,5,8,13};

C++98 时代, 我们只能用列表初始化数组, 而 C++11 时代, 我们可以定义一个constructor, 接受 {} initializer.

我们也可以用 range-for 遍历 vector.

for(int x : v) test(x);

这行代码将对 v 里的每个元素都调用 test().

range-for 可以遍历任何序列, 所以我们可以直接遍历 initializer list:

for (int x : {1,2,3,5,8,13}) test(x);

C++11 的目标之一, 就是让简单的归于简单, 同时不引起性能过载.

迷思二 C++是面向对象的

不. C++支持面向对象, 和其他一些编程范式, 不仅仅局限于某一个范式.

翻译略.

迷思三 可靠的软件都用GC

垃圾收集机制运行的不错, 但远远谈不上完美. 内存可能有残留, 并且资源也不全是内存. 考虑如下情况:

class Filter { // take input from file iname and produce output on file onamepublic: Filter(const string& iname, const string& oname); // constructor ~Filter();// destructor // …private: ifstream is; ofstream os; // …};

Filter 的构造器打开了两个文件. 然后, Filter 读入文件内容, 做一些过滤, 然后输出文件. 过滤机制可能是硬编码在 Filter 里, 也可能是通过回调函数提供, 这不重要, 我们要讨论的是资源管理. 我们可能这么创建 Filter:

void user(){ Filter flt {“books”,”authors”}; Filter* p = new Filter{“novels”,”favorites”}; // use flt and *p delete p;}

在有垃圾收集机制的语言中, 没 delete 关键字, 也没有解构函数. 对内存的回收, 在这个例子中, GC 可以做到完美, 但是文件就不能自动回收了. 需要用户写代码, 手工管理资源容易产生bug.

传统的C++代码使用解构函数来确保资源被正确回收. 一般说来, 这种资源在构造函数中申请, 这种方式被称为 RAII. 在 user() 中, flt 的解构函数会隐式调用 is 和 os 的解构函数, 这些解构函数依序关闭文件, 释放相关资源. delete 操作符也释放 *p 的相关资源.

C++老手会发现 user() 还是罗嗦易错的, 下面的会更好一些:

void user2(){ Filter flt {“books”,”authors”}; unique_ptr<Filter> p {new Filter{“novels”,”favorites”}}; // use flt and *p}

现在 *p 会在 user2() 结束的时候自动释放资源. unique_ptr 是标准库的一个类, 被设计为确保资源不泄露, 同时不会引起时间和空间的过载, 所以不要再使用裸指针了.

然而, 我们还是看得到 new, 这个方案还是有点繁复(Filter类型声明重复了), 而且智能指针阻止了性能优化. 使用C++14的仆人函数 make_unique, 这个函数构造一个对象, 并且返回这个对象的 unique_ptr.

void user3(){ Filter flt {“books”,”authors”}; auto p = make_unique<Filter>(“novels”,”favorites”); // use flt and *p}

除非我们真的需要第二个Filter的指针形式(最好不需要), 我们可以这样写:

void user3(){ Filter flt {“books”,”authors”}; Filter flt2 {“novels”,”favorites”}; // use flt and flt2}

最后一个版本更短, 更简单, 更清晰, 更快.

那么, 还有最后一个问题: Filter的解构器做了什么? 它释放Filter占用的所有资源, 也就是说, 关闭文件(通过调用他们的解构器). 实际上, 这些都是(隐式)应当的, 所以除非Filter还有资源要释放, 我们可以移除解构器的声明, 编译器会替我们做好. 所以, 刚才我写下来的只是:

class Filter { // take input from file iname and produce output on file onamepublic: Filter(const string& iname, const string& oname); // …private: ifstream is; ofstream os; // …};void user3(){ Filter flt {“books”,”authors”}; Filter flt2 {“novels”,”favorites”}; // use flt and flt2}

真巧, 比大部分有垃圾收集机制的语言(Java, C#)都要简洁, 而且关上了所以资源泄露的大门, 妈妈再也不用担心程序员忘记关文件了.

这是我理想中的资源管理方式. 这里的资源不仅仅是内存, 还包括其他的资源, 比如文件, 线程 和 锁. 但是这种方式真的普适吗? 如果资源作为参数传进函数中去会如何? 如何一个资源的所有者不止一个会如何?

三,一 转移所有权: move顺境的美德是节制,逆境的美德是坚韧,这后一种是较为伟大的德性。

jiangbinliu的专栏

相关文章:

你感兴趣的文章:

标签云: