「译」 C++ 五大谣言(及辟谣),第1部分 - 哆啦比猫的技术瞎扯 - Arch Linux · ドラえもん · 实时绘制

「译」 C++ 五大谣言(及辟谣),第1部分

哆啦比猫 posted @ 2014年12月12日 21:55 in C/C++ with tags c++ 谣言 myth , 4918 阅读

译者注:本文于 2014年12月9日 由 C++ 之父 Bjarne Stroustrup 发表于 C++ 官方网站上 https://isocpp.org/blog/2014/12/myths-1

「为了您在冬季能享受阅读的快乐,我们很高兴地推出了这个由 Bjarne Stroustrup 写的三部分系列文章。本文是第一部分,第二和第三部分将会在接下来的两个星期一发表,届时正好是圣诞节。Enjoy. ——小编」

简介

在这个由三个部分构成的系列文章中,我将会探索并破解以下五大 C++ 谣言:

  1. “要理解 C++,你必须先学会 C”
  2. “C++ 是面向对象语言”
  3. “要写可靠的软件,你需要垃圾回收机制”
  4. “为了软件运行的效率(efficiency),你必须写底层代码”
  5. “C++ 只适合大型、复杂的程序”

如果你或你的同事相信其中的任何一个谣言,那请你们阅读这篇短文。对于某个特定的人、某件特定事、某个特定的时刻,这些谣言中的多数都曾是正确的。但是,在今天的 C++ 大背景下,在使用那些 烂大街的(widely available)、新的(up-to date)、兼容 ISO C++ 2011 标准的 编译器和工具的前提下,它们都成为了谣言。

我觉得这些谣言很流行,因为我经常听到它们。人们偶尔会去找些理由来支持这些谣言,但更多时候,人们就把它们当作是显然的事实,无需理由。有时,人们以这些谣言为由,把 C++ 从“考虑使用的语言”中去除。

彻底破除其中的任何一个谣言,都需要长篇论文亦或是一本书的篇幅,但在此我只是想简单的提出问题并简要地说明我的理由。

谣言1:“要理解 C++,你必须先学会 C”

不。学习编程基础时,用 C++ 要比用 C 简单得多。

C 大体上是 C++ 的一个子集,但它并不是最适合初学的那个子集,因为 C 缺乏 记号支持(notational support)、类型安全性,以及 C++ 提供的可以简化简单任务的 更易使用的 标准库。考虑下面这个非常简单的函数,它可以用来合成(compose)电子邮箱地址:

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); // 分配空间给 两个字符串、字符'@'和字符串结束符 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); // 用完后释放内存

你更愿意教哪种?哪种用起来更容易?我真的把 C 语言版的写对了吗?你确定?理由?

最后,哪个版本可能会更高效(efficient)?没错,是 C++ 版的,因为它不需要数 参数字符串 中有多少个字符,对较短的 参数字符串 也不需要使用堆内存(动态内存)。

学习 C++

上面的例子并不是莫名的个案,我倒是觉得它很典型。那么,为什么有那么多老师坚持“先学C语言”的教育方法呢?

  • 因为他们年年都这样做
  • 因为课表要求这样
  • 因为那些老师年轻时就是这样学的
  • 由于 C 语言比 C++ 内容要少,就觉得 C 语言用起来更简单
  • 因为反正以后学生们都得学 C (或是 C++ 中的 C 子集)

但是,C 并不是 C++ 里初学起来 最容易、最有用 的子集。更何况,你一旦学会了一定数量的 C++,就可以很容易的学会 C 子集。在学 C++ 之前学习 C 语言意味着,你得忍受那些在 C++ 中可以轻松避开的错误,也意味着你得学习减少错误的技巧。(没有理解这句:Learning C before C++ implies suffering errors that are easily avoided in C++ and learning techniques for mitigating them.)

如果你想知道教授 C++ 的现代方法,请看我的《C++程序设计原理与实践(Programming: Principles and Practice Using C++)》。书的末尾甚至有一章 专门展示如何使用 C 语言。这本书相当成功地被 好几所大学 成千上万的初学者 使用。这本书的第二版使用了 C++11 和 C++14 提供的设施,使得 C++ 学起来更容易。

自从有了 C++11,C++ 变得更加容易易上手了。比如,这里使用了标准库里的向量 vector,并使用一系列元素来初始化它:

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

在 C++98 中,只有数组能用列表来初始化。而在 C++11 中,只要你愿意,就可以定义一个构造函数,来接收任意类型的 {} 初始化列表(In C++11, we can define a constructor to accept a {} initializer list for any type for which we want one)

我们可以用基于范围的 for 循环(range-for loop)来遍历那个向量:

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

这会以向量 v 中的每个元素为参数调用 test(),一个元素调用一次。

基于范围的 for 循环可以用来遍历任何序列,所以上面的例子可以简化一下,直接使用初始化列表:

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

C++11 的一个目标是要使简单之事保持简单(make simple things simple)。当然,这是用不损失性能的方法实现的。

谣言2:“C++ 是面向对象语言”

不。C++ 支持面向对象(OOP)和其它编程风格,但故意不局限于“面向对象”的狭隘思想当中。C++ 允许你综合使用各种编程技巧,如面向对象,亦或是泛型(generic)编程。很多时候,解决某个问题的最佳方案要涉及不止一种编程风格(也叫“范式(paradigm)”)。我说的“最佳”,指的是 最短、最好理解、最高效、最易维护 等等。

“C++ 是面向对象语言”这个谣言 导致人们认为(和 C 语言相比)C++ 不是必须的,除非你要一个大型的类层次结构,其中包含了大量的虚(运行时多态)函数——但是对于许多人、许多问题而言,这种用法是不合适的。相信这个谣言导致人们谴责 C++ 不是一个纯粹的面向对象语言,毕竟,他们会将“好”与“面向对象”等同起来,而 C++ 显然包含了大量不是面向对象的东西,所以 C++ 必须是个“不好”的语言。在二者中的任何一种情况下,这个谣言都为那些不学 C++ 的人提供了不错的借口。

看下这个例子:

void rotate_and_draw(vector<Shape*>& vs, int r)
{
  for_each(vs.begin(),vs.end(), [](Shape* p) { p->rotate(r); });  // 旋转(rotate) vs 中的所有东西
  for (Shape* p : vs) p->draw();                                  // 画出(draw) vs 中的所有东西
}

这是面向对象吗?当然是,它严重依赖于带有虚函数的类层次结构。这是泛型吗?当然是,它严重依赖于 参数化类型 的容器(vector)和 泛型函数 for_each。这是函数式吗?可以算是,它用了匿名函数(lambda,那个 [] 构造)。那它到底是什么?它是 现代 C++:C++11。

我同时使用了 基于范围的 for 循环 和 标准库里的 for_each 算法 来实现两个循环,这仅仅是为了展示特性。在真实的环境下,我只写一个循环,至于是用 for_each 还是用 基于范围的 for 循环 来实现,那都有可能。

泛型编程

你想要让这段代码更加通用(generic)?毕竟,它现在只能作用于 储存 Shape 指针 的 vector。让它支持下 链表 以及 内置数组 如何?要不,支持下“智能指针”(自主管理资源的指针,如 shared_ptr 和 unique_ptr)?那些不是 Shape 类型但是可以调用 draw() 和 rotate() 的对象呢?看下这个函数:

template<typename Iter>
void rotate_and_draw(Iter first, Iter last, int r)
{
  for_each(first,last,[](auto p) { p->rotate(r); });  // 旋转 闭开区间[first, last) 内的所有对象
  for (auto p = first; p!=last; ++p) p->draw();       // 画出 闭开区间[first, last) 内的所有对象
}

这段代码可以应付任何能从 first 迭代至 last 的序列。这正是 C++ 标准库提供的算法的风格。我用了 auto,这样可以避免写出“Shape相似对象”接口的类型。auto 是 C++11 的功能,意为“使用初始化表达式的类型”。这样一来,for 循环中 p 的类型就会推导为 first 的类型。在匿名函数的参数中用 auto 作类型 是 C++14 的功能,不过我们已经在用了。

看下这段代码:

void user(list<unique_ptr<Shape>>& lst, Container<Blob>& vb)
{
  rotate_and_draw(lst.begin(),lst.end());
  rotate_and_draw(begin(vb),end(vb));
}

这里我们假设 Blob 是某个支持 draw() 和 rotate() 操作的图形类型,而 Container 是某个容器类型。标准库提供的链表(std::list)有两个成员函数 begin() 和 end(),以便用户遍历其元素序列。经典的面向对象。很好。但是,如果 Container 类型不支持 C++ 标准库的 那种在一个闭开区间 [b, e) 上迭代的概念 该怎么办?它要是没有 begin() 和 end() 成员函数,那又该怎么办?好吧,但是我从没见过不支持遍历的容器,这样的话,我们可以给定一个合适的语义,并依此定义两个独立的函数 begin() 和 end()。标准库就为 C 风格的数组 提供了那两个函数,所以,如果 Container 类型是 C 风格的数组,问题自然解决—— C 风格的数组仍然很常见的。

适配

考虑一个更难的情况:如果 Container 存的是一个对象指针,而且有着不同的访问与遍历模型,该怎么办?比如,我们有一个必须这样访问的 Container:

for (auto p = c.first(); p!=nullptr; p=c.next()) { /* 对 *p 做点什么 */}

这种风格并不罕见。我们可以把它映射到 [b, e) 序列上,就像这样:

template<typename T> struct Iter {
  T* current;
  Container<T>& c;
};

template<typename T> Iter<T>  begin(Container<T>& c) { return Iter<T>{{c.first(), c}}; }
template<typename T> Iter<T>  end(Container<T>& c)   { return Iter<T>{{nullptr, c}}; }
template<typename T> Iter<T>& operator++(Iter<T>& p) { p.current = p.c.next(); return p; }
template<typename T> bool     operator!=(const Iter<T>& lhs, const Iter<T>& rhs) { return (lhs.current != rhs.current); }
template<typename T> T*       operator*(Iter<T> p)   { return p.current; }

需要指出的是,这样的修改是 非侵入式(nonintrusive) 的:我不需要修改 Container 或是 其类层次结构,就能把它映射到 C++ 标准库支持的那种遍历模型之上。这是一种叫“适配”的形式,而无需“重构”。

我选用这个例子,是为了说明这些泛型编程的技术并不只是局限于标准库(标准库大量使用这些技术)。另外,按照最常见的“面向对象”的定义,这样的代码并不是面向对象的。

“C++ 代码必须面向对象(指的是 到处使用继承和虚函数)”这样的想法会导致严重的性能损失。如果你想要在运行时解析一系列类型,那面向对象是个很棒的主意,我就经常这样用。但是,这相对来说并不灵活(并非每个相关类型都适合放到同一个层次结构里),而且,虚函数调用 会抑制内联(而这将在一些简单而重要的地方让你损失 50 倍的性能(speed))。

后记

在第2部分中,我将讨论“要写可靠的软件,你需要垃圾回收机制”。

译者注:原文的代码有一些小错误,已修正。代码的命名风格和缩进风格等保持和原文一致。由于本人汉语水平有限,如有不通顺之处欢迎提出修改建议,如有其它疑议也欢迎指出。

版权声明:本文英文版作者为 C++ 之父 Bjarne Stroustrup,原文版权请参见 ISO C++ 官方网站使用条款中的版权部分。本文简体中文翻译采用 知识共享·署名 3.0 中国大陆许可协议(CC-BY) 进行许可,by Giumo Xavier Clanjor (哆啦比猫/兰威举), 2014。


凡未特殊声明(转载/翻译),所有文章均为原创。
by Giumo Xavier Clanjor (哆啦比猫/兰威举), 2010-2019.
知识共享许可协议本作品采用知识共享署名·非商业性使用·相同方式共享 3.0 中国大陆许可协议进行许可。
文中凡未特殊声明且未声明为引用的代码均以 MIT 协议授权。

blog comments powered by Disqus
© 2010-2019 Giumo Xavier Clanjor (哆啦比猫/兰威举).
© 2013, 2014, 2015-2016 and 2017 The Dark Colorscheme Designed by Giumo Xavier Clanjor (哆啦比猫/兰威举).
知识共享署名·非商业性使用·相同方式共享 3.0 中国大陆许可协议
| © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee