简介

  • 抽象,是有选择的忽略。
  • 编程依赖于一种选择,选择什么何时忽略
  • 编程就是通过建立抽象来葫芦俄那些我们此刻并不重视的因素
  • 本书坚持以两个思想为核心:实用和抽象

抽象

  • 在处理大问题的时候,这样的工具总是能够帮助将问题分解成独立的子问题,并能确保它们相互独立,也就是说当处理问题的某个部分的时候,完全不必担心其他部分
  • 有些抽象不是语言的一部分
  • 文件的概念
    • 事实上每种操作系统都以某种方式使文件能为用户所用。在大多数情况下,文件根本不是物理存在的。
    • 文件只是组织长期存储的数据的一种方式,并且由程序和数据结构的集合提供支持来实现这个抽象。
  • 通常,我们不可能为特定的工具挑选合适的问题

面向对象编程(OOP)

  • 面向对象编程,指使用继承和动态绑定的编程方式
  • 继承,是一种抽象,它允许程序员在某些时候忽略相似对象间的差异,又在其它时候利用这些差异。
  • 采用这种编译时检查的方式,是因为C++能够为动态绑定的函数调用快速生成代码
  • 只有在程序通过指向基类对象的指针或者基类对象的引用调用虚函数时,才会发生运行时的多态现象。
  • 任何虚函数只有在继承的情况下才有用

  • 对象的创建和复制不是运行时多态的。所以容器:无论是类似于数组或者结构体的内建容器还是用户自定义容器类:只能获得编译时类型已知的元素值

  • 如果有一系列类之间存在继承关系,当需要创建,复制和存储对象,而这些对象的确切类型只有到运行时才能够知道时,这种编译时的检查会带来一些麻烦
  • 通常,解决这个问题的方法是增加一个间接层。C++采用了一种更自然的方法。就是:定义一个类来提供并且隐藏这个间接层,这种类,通常叫做句柄(handle)类
  • 句柄类采用最简单的形式,把一个单一类型的对象与一个与之有某种特定继承关系的任意类型的对象捆绑起来。
  • 句柄类的一个常见用户就是通过避免不必要的复制来优化内存管理

  • 容器通常只能包含一种类型的对象,所以很难在容器中存储对象本身
  • 存储指向对象的指针,虽然允许通过继承来处理类型不同的问题,但是也增加了内存分配的额外负担

  • 定义一个行为和Vehicle对象相似,而有潜在地表示了所有继承自Vehicle类的对象的东西。我们把这中类的对象叫做代理(surrogate)

handle classe

  • 需要一种方法,让我们在避免某些缺点(如缺乏安全性)的同时能够获取指针的某些优点,尤其是能够在保持多态性的前提下避免复制对象的代价。
  • C++的解决方法就是定义一个适当的类,由于这些类的对象通常被绑定到它们所控制的类的对象上,所以这些类常被称为handle类(handle classe)。因为这些handle的行为类似指针,所以有时也被称为智能指针smart pointer

引用计数型句柄

  • 之所以要使用句柄,原因之一就是为了避免不必要的对象复制。也就是说,得允许多个句柄绑定到单个对象上
  • 写时复制,copy on write。其有点是只有在绝对必要时才进行复制,从而避免了不必要的复制。在涉及句柄的类库中,这一技术经常用到

面向对象程序范例

  • 通常认为,面向对象编程有三个要素:数据抽象,继承以及动态绑定
  • 解决方法的实质是要对希望模拟的下层系统中的对象进行建模。当我们分析出表达式树是有节点和边所构成,便可以设计数据抽象来对树进行建模。
  • 继承让我们抓住了各种节点类型之间的相似之处,而动态绑定帮助我们为各种类型节点定义操作,让编译器来负责安排在运行时能够调用正确的函数。
  • 这样,数据抽象加上动态绑定可以让我们集中精力考虑每个类型的行为和实现,而不必关心与其他对象的交互。

设计

  • 在解决问题的时候,有一点要始终牢记,不仅要看到眼前的问题,还要看到长远的变化。
  • 在实际开发中,灵活性通常是有意义的,因为它使我们面对需求的变更不至于一切推翻重来。至于应当为这种灵活性付出多大代价,当然有一个工程上的权衡问题,只能根据对环境的理解来作出回答。
  • 我们必须清楚,在选择一个设计方案之前,必须首先把问题及其背景搞清楚

虚函数

  • 虚函数是C++的基本组成部分,也是面向对象编程所必需的。然而,虚函数并不一定总是适用的
  • 关于虚函数为什么不总是适用,大致有三个原因:
    1. 虚函数有时候会带来很大的消耗
    2. 虚函数不总是提供所需的行为
    3. 有时候我们写一个类时,可能不想考虑派生的问题
  • 另一个方面,我们还知道了一种必须使用虚函数的情况。当需要删除一个表面上指向基类对象,实际上却是指向派生类对象的指针,就是需要虚析构函数。

模板

  • 从某种意义上来说,模板只不过是语法宏的一种受限形式

  • 通常将容器称为模板,而容器内的对象的类型就是模板参数
  • 因此,对于任意类型T,可以想象容器可以是List<T>或者Set<T>

  • 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码
  • 模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
  • 每个容器都有一个单一的定义,比如向量,我们可以定义许多不同类型的向量,比如vector<int> 或者vector <string>
  • 可以使用模板来定义函数和类
  • 模板函数定义的一般形式如下:
    1
    2
    3
    
      template <typename type> ret-type func-name(parameter list){
          // 函数主体
      }
    
  • 在这里,type是函数所使用的数据类型的占位符的名称,这个名称可以在函数定义中使用

  • 类模板,正如定义函数模板一样,也可以定义类模板,泛型类声明的一般形式如下:
    1
    2
    3
    
      template <class type> class class-name{
    
      }
    
  • 在这里,type是占位符类型名称,可以在类被实例化的时候进行指定。可以使用一个逗号分隔的列表来定义多个泛型数据类型

  • C++最基本的设计原则就是用类来表示概念。指针把数组的标识和内部空间结合在一起

迭代器

  • 通常情况下,每个容器类都有一个或者多个相关的迭代器类型。迭代器能使我们在不暴露容器内部结构的情况下访问容器的元素。

模板和泛型算法

  • 1994年7月,在安大略基其纳召开的C++标准会议上,委员会投票通过了一项由Alex Stepanov提出的建议,即将他和他的同事们在Hewlett-Packard实验室开发的一系列泛型算法作为一部分收录到标准C++标准库中。这些被包含到库中的类和算法合起来称为标准模板库(Standard Template Library, STL
  • 所谓泛型算法,就是这样的算法:对于所操作的数据结构的细节信息,只加入最低限度的了解。当然,理想的情况应该是根本不需要这样的信息,但是现实却不是这样,作为一种折中,STL根据数据结构能够支持的有效操作,将这些数据结构进行分类,然后对于每个算法,它会指出该算法所需要的数据结构类别
  • 被分类的不是算法,也不是数据结构,而是用来访问数据结构的类型。这些类型的对象叫做迭代器。
  • 迭代器共有五种:输入迭代器,输出迭代器,前向迭代器,双向迭代器和随即存取迭代器。
  • 概念继承将这些种类关联起来,之所以称之为“概念的”,是因为这些种类本身都是概念,而不是类型或者对象。

函数对象

  • 除了迭代器和配接器外,STL还提供了一种称为函数对象(function object)的概念。
  • 简单地说,函数对象提供了一种方法,将要调用的函数与准备传递给这个函数的隐式参数捆绑起来,这就允许我们使用相当简单的语法来建立复杂的表达式
  • 函数对象表示了一种操作。通过组合函数对象,我们可以得到复杂的操作。

  • 抽象数据类型,ADT
  • 数据抽象的目的就是控制复杂度

  • C++中的一个更为重要的思想(尽管这个思想不是C++特有的)是:用户自定义类型可以很容易地当作内建类型使用。通过定义新类型,用户可以为了他们自己的目的来定制语言

抽象数据类型

  • 如果我们准备在接口和实现之间实现完全隔离,就会希望语言支持数据抽象。
  • C++语言中将接口与实现分隔开的最基本的方法之一就是采用构造函数和析构函数。正是这两个函数允许类设计者能够说:这个类的对象使用对象本身内容之外的信息。
    • 构造函数本身提供了生成给定类对象的方法
    • 析构函数则提供了与构造函数相反的行为

命名空间

  • 命名空间解决了一种在C中十分突出而在C++中愈加严重的问题:如何防止不同的程序库设计者为各自组建采用相同的名字
  • 本质上,命名空间允许库设计者对会被库放到全局作用域的所有名称指定一个包装器(wrapper)

技术

  • C++的一个基本思想就是:通过类定义可以明确指明当这个类的对象被构造,销毁,复制和赋值时应该发生什么事情。这意味着设计的当的类可以为理解程序的动态行为提供一个强有力的工具,这一点往往比人们所认识的更重要

  • C++程序经常要为一整组对象分配内存,随后将他们同时释放。解决这个问题的方法之一是定义一个包含这样的集合的类。事实证明,为了让一个集合包含不相关的类的对象,一个好方法是使用多重继承。

总结

  • 一种常见对C++的批评是说该语言太复杂。我认为只有在孤立地看待C++时,这种观点才成立。设计任何一门语言,也可以说是任何软件,都是有特定的背景。
  • C++面向的是特定的用户群。这个用户群要应付各种复杂的问题,写出要运行相当长时间的解决方案。这些解决方案必须满足任意的性能需求,要工作在不同的硬件和操作平台上,还要和许多已经存在的系统共存。
  • 学习和使用C++的建议:
    • 做理解的事情;理解要做的事情
    • 逐步加深扩展理解
    • 做练习时要把握分寸,过犹不及
    • 依据操作思考。从C到C++最大的观念性变化就是要停止考虑程序的结构,而开始考虑程序数据的行为。
    • 早些考虑测试
    • 思考。所有的建议都只是建议。是否对你有用是由你决定的。你可以依照我的建议做任何事情,尽量确切的遵循它,或者拼命反其道而行之,或者忽略它,或者对此嗤之以鼻。不管怎么对待,你都要清楚为什么要这样做。不管你理不理解都要对结果负责。

采访

  • 是否应该更加重视标准库教育,而不是语言细节的教育?
  • 当然是库优先于语言细节。两个原因:
    • 首先,学生们可以不必费力包装低层次的语言细节,从而更容易建立整体语言的全局观念,了解到其真实威力。不过根据我们的经验,学生们首先掌握如何使用程序库之后,就会很容易理解类的概念,学会如何构造类的技术。如果首先去学习语言细节,那么就很难理解类的概念及其功能。这种理解上的缺陷,使他们很难设计和构造自己的类。
    • 更重要的一点是,首先学习程序库,能够是学生培养起良好的习惯,就是复用库代码,而不是凡事自己动手。首先学习语言细节的学生,最后的编程风格往往是C类型的,而不是C++风格。他们不会充分地运用库,而自己的程序带有严重的C主义倾向:指针满天飞,整个程序都是低层次的。结果是,在很多情况下,你为C++的复杂性付出了高昂代价,却没有从中获得任何好处。
  • 一个问题产生良好的设计方案的途径,就是使用一种允许你进行各种设计的工具。这样一来,你就可以选择最适合该问题的设计方案。如果你选择了这样的工具,那么你就必须负责选择合适的设计方案。

  • 为什么认为“基于对象”和“基于模板”的抽象机制优先于面向对象抽象机制?
  • 所谓面向对象编程,就是使用继承和动态绑定机制编程。如果你知道有一个很好的程序使用了继承和动态绑定,你能作出怎样的推断?在我们看来,这意味着该程序中有两个或两个以上的类型,至少有一个共同的操作,也至少有一个不同的操作。否则就不需要继承机制。此外,程序中必然有一个场景,需要在运行时从这些类型中挑选出一个,否则就不需要动态绑定机制
  • 某些面向对象编程语言,如Python,其所有类型都是动态的,那么技术书籍的作者就不会面对这样的问题。例如,C++中的容器类大多数用模板写成,因其可以容纳毫无共同之处的对象,所以要求元素类型必须是某个共同基类的派生类毫无道理。然而,在Python中,容器类中本来就可以放置任何对象,所以类似模板那样的类型机制就不必要了。
  • 所以,我认为你所看到的问题,其实是因为很难找到又小又好的面向对象程序做范例,才会产生的。而且,对于其他语言必须依赖动态类型才能解决的问题,C++能够使用模板来高效地解决。

  • 如果我说我只能记住你的一句话,那一定是:用类来表示概念。假设再记住一句话,应该是什么?
  • 避免重复。如果发现自己在程序的两个不同部分里做了相同的事情,试着把这两个部分合并到一个子过程中。如果发现两个类的行为相近,试着把这两个类的相似部分统一到基类或模板中

  • 我们都希望成为更好的C++程序员,请给我们三那个你认为最重要的建议。
    • 避免使用指针
    • 提倡使用程序库
    • 使用类来表示概念