《Essential C++》读书笔记

本文最后更新于:4 个月前

前言

Lippman在前言部分明确的阐明了本书的写作目的:“C++ Primer第三版其实无法扮演人们在初学C++时的导师角色。它太庞大了”。

结构与组织

结构与组织部分可能很好的说明了此书的组织架构,但我现在还看不懂,就先不重复了。之后会讲每一部分的内容附加到相应章节之前。

关于源代码

本书所有的程序,以及习题解答中的完整代码,都可以从网上获得。可以在Addison Wesley的网站博文视点网站取得。

第1章 C++编程基础

第1章借着撰写一个具有互动性质的小程序,描绘C++语言预先定义的部分。这一章涵盖了内置的数据类型、语言预定义的运算符(operator)、标准库中的vectorstring、条件语句和循环语句、输入和输出用的iostream库。

1.1 如何撰写C++程序

  • 每个C++程序都是从一个名为main的函数开始执行,其通用形式如下:

    1
    2
    3
    4
    int main()
    {

    }

  • 习惯上,程序执行无误时我们令main()返回零。若返回一个非零值,表示程序在执行过程中发生了错误。

  • C++标准的“输入/输出库”名为iostream,用以支持对终端和文件的输入与输出。

  • 如果没有在main()的末尾写下return语句,return 0将会被自动加上。

  • 第一个完整的C++程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <string>
    using namespace std;

    int main()
    {
    string user_name;
    cout << "Please enter your first name:";
    cin >> user_name;
    cout << '\n' << "Hello, " << user_name << " ... and goodbye!\n";
    return 0;
    }

1.2 对象的定义和初始化

  • 对象的名称可以是任何字母、数字、下划线(underscore)的组合。
  • 对象的名称不能以数字开头。
  • 任何命名不能和程序语言本身的关键字一致。
  • const关键字用来表示常量的对象。

1.3 撰写表达式

  • 四种运算符:算术运算符、关系运算符、逻辑运算符、复合赋值运算符
  • 条件运算符的一般形式:
    1
    2
    3
    expr
    ? 如果exprtrue,就执行这里
    : 如果exprfalse,则执行这里
  • OR逻辑运算符(||)可以同时检验多个表达式的结果。左侧表达式会被先求值,如果其值为true,剩下的另一个表达式就不需要再被求值(此所谓短路求值法)。 ### 运算符优先级(优先级从上到下)
  1. 逻辑运算符NOT
  2. 算术运算符(*, /, %)
  3. 算术运算符(+, -)
  4. 关系运算符(<, >, <=, >=)
  5. 关系运算符(==, !=)
  6. 逻辑运算符AND
  7. 逻辑运算符OR
  8. 赋值运算符
  • 如果想要改变内置的运算符优先级,可利用小括号。
  • 运算优先级是C++变成之所以复杂的原因之一。

1.4 条件语句和循环语句

条件语句

  • switch语句中break的用途:当某个标签和switch表达式值吻合时,该case标签之后的所有case标签也都会被执行,除非我们明确使用break来结束执行。 ### 循环语句

1.5 如何运用Array和Vector

  • 一般建议使用vector甚于array。
  • array和vector之间存在一点差异,那就是vector知道自己的大小是多少。(vector.size())

1.6 指针带来弹性

  • 指针主要做两件事,它可以增加程序本身的弹性,但同时增加了直接操作对象时所没有的复杂度。
  • 下述写法可以将pi的初值设置为ival所在的内存地址
    1
    int *pi = &ival
  • 提领(dereference)操作——就是取得“位于该指针所指内存地址上”的对象。在指针之前使用*号,便可以达到这个目的。
  • 指针的复杂度源于它的双重性质:既可以操作指针包含的内存地址,又可以操作指针所指的对象值。
  • 指针第二个可能令人感到复杂的地方是:在使用指针时,必须在提领之前确定它的确指向某对象。
  • 一个未指向任何对象的指针,其地址值为0。有时候我们称之为null指针。
  • 如果要通过指针来选择操作,必须用arrow(->)而非dot(.)成员选择运算符。

1.7 文件的读写

  • 要对文件进行读写操作,首先得包含fstream文件。

第2章 面向过程的编程风格

第2章解释函数的设计与使用,并逐一查看C++函数的多种不同风貌,包括inline函数、重载(overloaded)函数、function template,以及函数指针(pointers to functions)。

2.1 如何编写函数

  • 每一个函数必须定义以下四个部分:
    1. 返回类型
    2. 函数名
    3. 参数列表
    4. 函数体
  • 函数必须先被声明,然后才能被调用(被使用)。函数声明不必提供函数体,但必须明确返回类型、函数名,以及参数列表。

2.2 调用函数

  • 冒泡排序:由两个嵌套的for循环实现。外层的for循环一次以ix为索引遍历vector内的元素,其值由0递增至size-1;内层的for循环以jx为索引从ix+1依次递增至size-1;并且同时比较vector中ix和jx处两元素的值,如果jx处的元素小则将其互换。
  • 传值(pass by value):当我们将vec[ix]这样的对象传入函数,默认情形下其值会被复制一份,成为参数的局部性定义(local definition)。
  • 传址(pass by reference):令函数的参数和传入的实际对象产生关联。要达成这个目的,最简单的做法就是将参数声明为一个reference。

Pass by Reference语义

  • 当我们以by reference的方式将对象作为函数参数传入时,对象本身并不会复制出另一份——复制的是对象的地址。
  • 将参数声明为reference有两个理由:①希望的一直接对所传入的对象进行修改。②降低复制大型对象的负担。
  • 一般来说,除非希望在函数内修改参数值,否则建议在传递内置类型时,不要使用传址方式。传址机制主要用于传递class object。

作用域

  • 除了一个必要的例外(static),函数内定义的对象,只存在于函数执行期间。
  • 为对象分配的内存,其存活时间成为储存器(storage duration)或范围(extent)。
  • 对象在程序内的存活区域成为该对象的作用域(scope)
  • 对象如果在函数之外声明,则具有所谓的file scope。对象如果拥有file scope,从其声明点至文件末尾都是可见的。
  • 内置类型的对象,如果定义在file scope内,必定被初始化为0。但如果它们被定义于local scope之内,那么除非程序员指定其初值,否则不会被初始化。

动态内存管理

  • 除了local scope和file scope,还有一种储存其形式成为dynamic extent(动态范围)。其内存有程序的空闲空间(free store)分配而来,有时也称为heap memory(堆内存)。这种内存必须由程序员自行管理,其分配通过new来完成,释放通过delete表达式完成。
  • 由于某种原因,程序员不想使用delete表达式,由heap分配而来的对象就永远不会被释放,这称为内存泄漏(memory leak)。

2.3提供默认参数

  • 默认参数值的提供有两个不直观的规则:
    1. 默认值的解析(resolve)操作由最右边开始进行。如果我们为某个参数提供了默认值,那么这一参数右侧的所有参数都必须也具有默认参数才行。下面这样实属非法:
      1
      2
      //错误:没有为vec提供默认值
      void display( ostream &os = cout, const vector<int> &vec);
    2. 默认值只能指定一次,可以在声明处,亦可在函数定义处,但不能够在两个地方都指定。(头文件可为函数带来更高的可见性(visibility)为了更高的可见性,我们决定将默认值放在函数声明处而非定义处。)

2.4 使用局部静态对象

  • 为了节省函数见的通信问题而将对象定义于file scope中,永远都是一种冒险。通常,file scope对象会打乱不同函数间的独立性,使它们难以理解。
  • 局部静态对象所处的内存空间,即使在不同的函数调用过程中,依然持续存在,这也就是我们可以安全地返回其地址的原因。

2.5 声明inline函数

  • 将函数声明为inline,表示要求编译器在每个函数调用点上,将函数内容展开。面对一个inline函数,编译器可将该函数的调用操作改为以一份函数代码副本代替。这将是我们获得性能改善,其结果是把多个函数写入一个函数中,但依然维持三个独立的运算单元。
  • 一般而言,最适合声明为inline1的函数具有如下特征:体积小、常被调用、所从事的计算并不复杂。
  • inline函数的定义常被放在头文件中。

2.6 提供重载函数

  • 函数函数:参数列表(parameter list)不相同(可能是参数类型不同,可能是参数个数不同)的两个或多个函数,可以拥有相同的函数名称。
  • 编译器无法根据函数返回类型来区分两个具有相同名称的函数。
  • 将一组实现代码不同但工作内容相似的函数加以重载,可以让用户更容易使用这些函数。如果没有重载机制,我们就得为每个函数提供不同的名称。

2.7 定义并使用模板函数

  • 函数模板(function template)的机制:将单一函数的内容与希望显示的各种类型绑定(bind)起来。
  • 一般而言,如果函数具备多种实现方式,我们可将它重载(overload),其每份实力提供的是相同的通用服务。如果我们希望让程序代码的主体不变,仅仅改变其中用到的数据类型,可以通过function template达到目的。

2.8 函数指针带来更大的弹性

  • 函数指针(pointer to function)必须指明其所指函数的返回类型及参数列表;函数的定义必须将*放在某个位置,表示这份定义所表现的是一个指针;最后还必须给予一个名称。
    1
    const vector<int>* (*seq_ptr)( int );

2.9 设定头文件

  • 函数的定义只能有一份。
  • 但上述规则有一个例外——inline函数的定义。为了能够扩展inline函数的内容,在每个调用点上,编译器都得取得其定义。这意味着我们必须将inline函数的定义放在头文件中,而不是把它放在不同的程序代码文件中。
  • 在file scope内定义的对象,如果可能被多个文件访问,就应该被声明于头文件中。
  • 下述对象的声明并不完全正确:
    1
    2
    const int seq_cnt = 6;
    const vector<int>* (*seq_array[seq_cnt])( int );
    需要在seq_array定义前加上关键字extern,便成为一个声明
    1
    2
    const int seq_cnt = 6;
    extern const vector<int>* (*seq_array[seq_cnt])( int );
  • 为什么上述代码块中的seq_cnt不需要extern修饰?
    因为const object和inline函数一样,是“一次定义”规则下的例外。const object的定义只要一处文件之外便不可见。
  • 引用头文件何时使用<>何时使用""包裹?
    如果头文件和包含此文件的程序代码文件位于同一个磁盘目录下,便用""。否则,用<>

第3章 泛型编程风格

第3章涵盖了所谓的Standard Template Library(STL):一组容器类(包括vector、list、set、map,等等)、一组作用于容器上的泛型算法(包括sort()、copy()、merge(),等等)。附录B按字典顺序列出了最广为运用的泛型算法,并逐一列上了使用实例。 * 泛型算法提供了许多可作用于容器类及数组类型上的操作。这些算法之所以被称为泛型(generic),是因为它们和它们想要操作的元素类型无关。

3.1 指针的算术运算

  • 数组最后一个元素的下一个地址,扮演者我们所说的标兵的角色,用以指示我们的迭代操作何时完成。
  • 指针的算术运算并不适用于list,因为指针的算术运算必须首先假设所有元素都储存在连续空间里,然后才能根据当前的指针,加上元素大小之后,指向下一个元素。

3.2 了解Iterator(泛型指针)

  • 如何取得iterator? 每个标准容器都提供有一个名为begin()的操作函数,可返回一个iterator,指向第一个元素。另一个名为end()的操作函数会返回一个iterator,指向最后一个元素的下一个位置。因此,不论如何定义iterator对象,一下都是对iterator进行赋值(assign)、比较(compare)、递增(increment)、提领(dereference)操作:
    1
    2
    for (iter = svec.begin(); iter != svec.end(); ++iter)
    cout << *iter << ' ';
  • iterator的定义形式:
    1
    2
    vector<string> svec;
    vector<string>::iterator iter = svec.begin();
    此处iter被定义为一个iterator,指向一个vector,后者的元素类型为string。其初值指向svec的第一个元素。双冒号表示此iterator乃是位于string vector定义内的嵌套[nested]类型。
  • 如果要通过iter调用底部的string元素所提供的操作,我们可以使用arrow(箭头)运算符: cout << "(" << iter->size() << "):" << *iter << endl;

3.3 所有容器的共通操作

下列为所有容器类(包括string类)的共通操作: * equality (==) 和inequality (!=) 运算符,返回true或false。 * assignment (=)运算符,将某个容器复制给另一个容器。 * empty()会在容器无任何元素时返回true,否则返回false。 * size()返回容器内目前持有的元素个数。 * clear()删除所有元素。

3.4 使用顺序性容器

  • 顺序型容器用来维护一组排列有序、类型相同的元素。
  • vector用一块连续内存来存放元素。随机访问颇有效率,但插入或删除非末位元素缺乏效率。
  • list系以双向链接(double-linked)而非连续内存来存储内容。任意位置插入或删除颇有效率,随机访问缺乏效率。
  • deque(读作deck)和vector一样用连续内存存储元素。不同的是其两端插入和删除的效率都很高。
  • vector允许末端插入(push_back())和删除(pop_back())操作。list和deque在此基础上允许前端插入(push_front())和删除(pop_front())。
  • list不支持iterator偏移运算:
    1
    2
    //错误:list并不支持iterator偏移运算
    list.erase(it1, it1+num_tries);

3.5 使用泛型算法

  • 以下为四种常用的泛型搜索算法:
    1. find()用于搜索无序集合中是否存在某值。
    2. binary_search()用于有序集合的搜索。(效率比find()高)
    3. count()返回数值相符的元素数目。
    4. search()比对某个容器内是否存在某个子序列。
  • binary_search()要求其作用对象必须经过排序(sorted)。

3.6 如何设计一个泛型算法

Function Object

  • 所谓function object是某种class的实例对象,这类class对function call运算符做了重载操作,这样一来可使function object被当成一般函数来使用。
  • function object实现了我们原本可能以独立函数加以定义的事物。但何必如此?主要是为了效率。我们可以令call运算符称为inline,从而消除“通过函数指针来调用函数”时付出的额外代价。
  • <functional>标准库事先定义了一组function object,分为算术运算(arithmetic)、关系运算符(relational)和逻辑运算(logical)三大类:
    1. 六个算术运算:plus<type>minus<type>negate<type>multiplies<type>divides<type>modules<type>
    2. 六个关系运算:less<type>less_equal<type>greater<type>greater_equal<type>equal_to<type>not_equal_to<type>
    3. 三个逻辑运算,分别对应&&、||、!运算符:logical_and<type>logical_or<type>logical_not<type>

Function Object Adapter

  • function object adapter会对function object进行修改操作。所谓binder adapter(绑定适配器)会将function object的参数绑定至特定值,使binary(二元)function object转化为unary(一元)function object。
  • 标准库提供了两个binder adapter:bind1st会将指定值绑定至第一操作数,bind2nd则将指定值绑定至第二操作数。
  • 另一种adapter是所谓的negator,它会对function object的真伪值取反。not1可对unary function object的真伪值取反,not2可对binary function object的真伪值取反。

3.7 使用Map

  • map被定义为一对(pair)数值,其中的key通常是个字符串,扮演索引的角色,另一个数值是value。
  • 任何一个key值在map内最多只会有一份。如果我们需要储存多份相同的key值,就必须使用multimap。

3.8 使用Set

  • Set由一群key组合而成。如果我们想知道某值是否存在于某个集合内,就可以使用set。
  • 默认情形下,set元素一句其所属类型默认的less-than运算符进行排列。例如,如果给定
    1
    2
    3
    int ia[10] = {1,3,5,8,5,3,1,5,8,1};
    vector<int> vec(ia, ia+10);
    set<int> set_i(vec.begin(), vec.end());
    set_i的元素将为{1,3,5,8}。
  • 如果要为set加入单一元素,可使用单一参数的insert()。
  • 如果要为set加入某一范围的元素,可使用双参数的insert()。

3.9 如何使用Iterator Inserter

  • 标准库提供了三个所谓的insertion adapter,这些adapter让我们得以避免使用容器的assignment运算符:
    1. back_inserter()会以容器的push_back()函数取代assignment运算符。
    2. inserter()会以容器的insert() 函数取代assignment运算符。
    3. front_inserter()会以容器的push_front()函数取代assignment运算符。仅适用于list和deque。
  • 欲使用上述三种adapter,必须首先包含iterator头文件。
  • 这些adapter不能用在array上,因为array不支持元素插入操作。

3.10 使用iostream Iterator

  • 标准库定义有供输入及输出使用的iostream iterator类,称为istream_iterator和ostream_iterator,分别支持单一类型的元素读取和写入。使用这两个iterator class之前,先得包含iterator头文件。
  • 如果希望从文件中读取,写入文件中去。只需要将istream_iterator绑定至ifstream object,将ostream_iterator绑定至ofstream object即可。

第4章 基于对象的编程风格

  • 本章节将带你亲身了解class机制的设计与使用过程。在这个过程中,你会看到如何为自身的应用系统建立起专属的数据类型。
  • 一般而言,class由两部分组成:一组公开的(public)操作函数和运算符,以及一组私有的(private)实现细节。
  • 这些操作函数和运算符称为class的member function(成员函数),并代表这个class的公开接口。身为class用户,只能访问公开接口。
  • Class的private实现细节可由member function的定义以及与此class相关的任何数据组成。
  • Class用户通常不会关心细节的实现。身为一个用户,我们只利用其公开接口来进行编程。这种情形下,只要接口没有更改,即使实现细节重新打造,所有的应用程序代码也不需要变动。
  • 这一章我们的境界将从class的使用提升至class的设计与实现。这正是C++程序员的主要工作。

4.1 如何实现一个Class

  • Class定义由两部分组成:class的声明,以及紧接着在声明之后的主体。主体部分由一对大括号括住,并以一对分号结尾。主题内的两个关键字public和private,用来标示每个块的“member访问权限”。
    1
    2
    3
    4
    5
    6
    class Stack {
    public:
    // public接口
    private:
    // private的实现部分
    };
  • 所有member function必须在class主体内进行声明。如果在class主体内定义,这个member function会自动被视为inline函数。

4.2 什么是构造函数和解析函数

  • 构造函数(constructor):编译器在每次class object被定义出来时使用的初始函数。
  • constructor的函数名必须与class名相同。语法规定,constructor不应指定返回类型,亦不用返回任何值。它可以被重载。
  • 以下代码无法成功定义一个Triangular object:
    1
    Triangular t5();
    因为C++必须兼容C。对C而言,t5之后带有小括号,会使t5被视为函数。 ### Member Initialization List(成员初始化列表)
  • Member initialization list紧接在参数列表最后的冒号后面,是个以逗号分隔的列表。其中,欲赋值给member的数值被放在member名称后面的小括号中;这使它们看起来像是在调用constructor。Member initialization list主要用于将参数传给member class object的constructor
  • 析构函数(destructor):一旦某个class提供有destructor,当其object结束生命时,便会自动调用destructor处理善后。Destructor主要用来释放在constructor中或对象生命周期中的资源。
  • Destructor的名称有严格的规定:class名称再加上'~'前缀。它绝不会有返回值,也没有任何参数。由于其参数列表为空,也不可能被重载。
  • Destructor并非绝对必要。事实上,C++编程的最难部分之一,便是了解何时需要定义destructor而何时不需要。 ### Memberwise Initialization(成员逐一初始化)
  • 默认情况下,当我们以某个class object作为另一个object的初值,class data member会被依次复制。此即所谓的Default Memberwise Initialization(默认成员逐一初始化)。
  • 当我们设计class时,必须问问自己,在此class上进行“成员逐一初始化”的行为模式是否合适?如果答案是肯定的,我们就不需要另外提供copy constructor。否则,我们必须另行定义copy constructor。

4.3 何谓mutable(可变)和const(不变)。

  • class设计者必须在member function身上标注const,以此告诉编译器:这个member function不会更改class object内容。
  • const修饰符必须在函数参数列表之后。
  • 编译器会检查每个声明为const的member function,看看它们是否真的没有更改class object内容。 ### Mutable Data Member(可变的数据成员)
  • 关键字mutable可以让我们声明对某一变量的改变不会破坏class object的常量性。

4.4 什么是this指针

  • this指针系在member function内用来指向其调用者(一个对象)。

4.5 静态类成员

  • static(静态)data member用来表示唯一的、可共享的member。它可以在同一类的所有对象中被访问。 ### Static Member Function(静态成员函数)
  • member function只有在“不访问任何non-static member”的条件下才能够被声明为static,声明方式是在声明之前加上关键字static。
  • 当我们在class主体外定义member function时,无序重复加上关键字static(这个规则同样适用于static data member)。

4.6 打造一个Iterator Class

  • 运算符重载的规则:
    • 不可以引入新的运算符。除了...*:::?四个运算符,其他的运算符皆可被重载。
    • 运算符的操作数(operand)个数不可改变。
    • 运算符的优先级(precedence)不可改变。
    • 运算符函数的参数列表中,必须至少有一个参数为class类型。
  • 后置运算符重载必须提供一个int参数。 ### 嵌套类型(Nested Type)
  • 一个类可以定义在另一个类的内部,前者称为嵌套类。(CSDN里要比书上讲的清晰的多)[1]
  • typedef可以为某个类型设定另一个不同的名称。其通用形式为
    1
    typedef existing_type new_name;

4.7 合作关系必须建立在友谊的基础上

  • 任何class都可以将其它function或class指定为朋友(friend)。而所谓friend,具备了让任何non-member function与class member function相同的访问权限,可以访问class的private member。

4.8 实现一个copy assignment operator

  • 只要class设计者明确提供了copy assignment operator,它就会被用来取代default memberwise copy行为。

4.9 实现一个function object

  • 所谓function object乃是一种“提供有function call运算符”(()运算符的重载)的class。

4.10 重载iostream运算符

  • output运算符(<<)的重载。
  • input运算符(>>)的重载。

4.11 指针,指向Class Member Function

  • maximal munch编译规则:每个符号序列(symbol sequence)总是以“合法符号序列”中最长的哪个解释。
  • Pointer to member function和pointer to function的一个不同点是,前者必须通过同一类的对象加以调用,而该对象便是此member function内的this指针所指之物。

第5章 面向对象编程风格

第5章介绍如何扩展class,使多个相关的class形成族系,支持面向对象的class层次体系。

5.1 面向对象编程概念

  • 继承(inheritance):使得我们将一群相关的类组织起来,并让我们得以分享其间的共通数据和操作行为。
  • 多态(polymorphism):可以让我们在这些类上进行编程时,可以如同操控单一个体,而非相互独立的类,并赋予我们更多弹性来加入或移除任何特定类。
  • 在C++中,父类被称为基类(base class),子类被称为派生类(derived class)。
  • 多态(polymorphism)让基类的pointer或reference得以十分透明地(transparently)指向其任何一个派生类的对象。
  • 静态绑定(static binding):程序执行之前就已解析出应该调用哪一个函数。
  • 动态绑定(dynamic binding):“找出实际被调用的是哪一个派生类的目标函数”这一解析操作会延迟至运行时(run-time)才进行。

5.2 漫游:面向对象编程思维

  • 默认情况下,member function的解析(resolution)皆在编译时静态地进行。若要令其在运行时动态进行,我们就得在它的声明前加上关键字virtual。
  • 当程序定义出一个派生对象,基类和派生类的constructor都会被执行。(当派生对象被销毁,基类和派生类的destructor也都会被执行[但次序颠倒]。)

5.3 不带继承的多态

  • 面向对象编程模式消除了这种(不带继承的多态)方式的维护负担,使我们的程序得以精简,更具扩展性。

5.4 定义一个抽象基类

  • 定义抽象类的第一个步骤就是找出所有子类共通的操作行为。
  • 设计抽象基类的下一步,便是设法找出哪些操作行为与类型相关(type-dependent)。这些操作行为应该成为整个类继承体系中的虚函数(virtual function)。
  • 设计抽象基类的第三步,便是试着找出每个操作行为的访问层级(access level)。
  • 每个虚函数,要么得有其定义,要么可设为“纯”虚函数(pure virtual function)——如果对于该类而言,这个虚函数并无实质意义的话,将虚函数赋值为0,意为它是一个纯虚函数。
  • 根据一般规则,凡基类定义有一个(或多个)虚函数,应该将其destructor声明为virtual。

5.5 定义一个派生类

  • 派生类名称后紧跟冒号、关键字public,以及基类的名称。
  • 派生类必须为从基类继承而来的每个纯虚数提供对应的实现。此外,它还必须声明其专属的member。
  • 一般来说,继承而来的public和protected成员,不论在继承体系中深度如何,都可被视为派生类自身拥有的成员。

5.6 运用继承体系

5.7 基类应该多么抽象

  • 基类的另一种设计方式:将所有派生类共有的实现内容剥离出来移至基类中,接口仍旧没有变动。

5.8 初始化、析构、复制

  • 派生类的constructor,不仅必须为派生类的data member进行初始化操作,还需要为其基类的data member提供适当的值。

5.9 在派生类中定义一个虚函数

  • 当我们定义派生类时,我们必须决定,究竟要将基类中的虚函数覆盖掉,还是原封不动地加以继承。
    • 如果我们继承了纯虚函数(pure virtual function),那么这个派生类也会被视为抽象类,也就无法为它定义任何对象。
    • 如果我们决定覆盖基类所提供的虚函数,那么派生类提供新的定义,其函数原型必须完全符合基类所声明的函数原型,包括:参数列表、返回类型、常量性(const-ness)。
  • “返回类型必须完全吻合”这一规则有个例外——当基类的虚函数返回某个基类形式(通常是pointer或reference时),派生类的同名函数可以返回该基类所派生出来的类型。 ### 虚函数的静态解析(Static Resolution)
  • 有两种情况,虚函数机制不会出现预期行为:
    1. 基类的constructor和destructor内;
    2. 当我们使用的是基类对象,而非基类对象的pointer或reference时。
  • 为了能够“在单一对象中展现多种类型”,多态(polymorphism)需要一层间接性。在C++中,唯有用基类的pointer或reference才能够支持面向对象的编程概念。

5.10 运行时的类型鉴定机制

  • typeid运算符是所谓运行时类型鉴定机制(Run-Time Type Identification, RTTI)的一部分,由程序语言支持。它让我们得以查询多态化的class pointer或class reference,获得其所指对象的实际类型。
  • 使用typeid运算符前,必须先包含头文件<typeinfo>
  • typeid运算符会返回一个type_info对象,其中储存着与类型相关的种种信息。
  • static_cast可以进行强制转换,但有潜在威胁,因为编译器无法确认我们所进行的转换操作是否完全正确。
  • dynamic_cast也是一个RTTI运算符,它会进行运行时检验操作,检验强转对象是否为目标值。

第6章 以template进行编程

  • 第6章的重头戏是class template,那是建立class时的一种先行描述,让我们得以将class用到的一个(或多个)数据类型或数据值,抽离并参数化。以vector为例,可能需要将其元素的类型加以参数化,而buffer的设计不仅得将元素类型参数化,还得将其缓冲区容量参数化。本章的行进路线围绕在二分树(binary tree)class template的实现上。
  • 在数据结构中,所谓树(tree)乃是由节点(node,或谓vertice)以及连接不同节点的链接(link)组成。所谓二叉树,维护着每个节点与下层另两个节点间的两条链接,一般将此下层二节点称为左子节点(left child)和右子节点(right child)。最上层第一个节点称为根节点(root)。无论是左子节点或右子节点,都可能扮演另一颗“子树(subtree)”的根节点。一个节点如果不再有任何子节点,便称为叶节点(leaf)。

6.1 被参数化的类型

  • template机制能帮助我们将类定义中“与类型相关(type-dependent)”和“独立于类型之外”的两部分分离开。

6.2 Class Template的定义

  • 在class scope运算符出现之后,其后所有东西都被视为位于class定义范围内。

6.3 Template类型参数的处理

  • 建议将所有的template类型参数视为“class类型”来处理,不要选择在constructor内初始化。这意味着我们会把它声明为一个const reference,而非以by value的方式传递。

6.4 实现一个Class Template

  • new表达式可以分解为两个操作:
    1. 向程序的空闲空间(free store)请求内存。如果分配到足够的空间,就分配一个指针,指向新对象。
    2. 如果第一步成功,并且外界指定了一个初值,这个新对象便会以最适当的方式被初始化。
  • 二叉树中移除某值的一般算法是,以节点的右子节点取代节点本身,然后搬移左子节点,使它成为右子节点的左子树的叶节点。
  • 声明一个reference to pointer,我们不但可以改变pointer本身,也可以改变由此pointer指向的对象。

6.5 一个以Function Template完成的Output运算符

6.6 常量表达式与默认参数值

  • Template参数并不是非得某种类型(type)不可。我们也可以用常量表达式(constant expression)作为template参数。
  • 全局作用域(global scope)内的函数及对象,其地址也是一种常量表达式,因此也可以被拿来表达这一形式的参数。

6.7 以Template参数作为一种设计策略

  • class template无法基于参数列表的不同而重载。

6.8 Member Template Function

  • Non-template class或class template内都可定义member template function。

第7章 异常处理

第7章介绍了如何使用C++的异常处理机制(exception handling facility),并示范如何将它融入标准库所定义的异常体系中。

7.1 抛出异常

  • 异常处理机制有两个主要成分:异常的鉴定与发出,以及异常的处理方式。
  • 异常出现之后,正常程序的执行便被暂停(suspended)。与此同时,异常处理机制开始搜索程序中有能力处理这一异常的地点。异常被处理完毕之后,程序的执行便会继续(resume),从异常处理点接着执行下去。
  • 所谓异常(exception)是某种对象。大部分时候,被抛出的异常都属于特定的异常类(也许形成一个继承体系)。我们只是令它得以储存某些必要数据,用以表示异常的性质,以便我们得以在不同程序的不同调用点上相互传递这些性质。

7.2 捕获异常

  • 我们可以利用单条或一连串的catch子句来捕获(catch)被抛出的异常对象。catch子句由三部分组成:关键字catch、小括号内的一个类型或对象、大括号内的一组语句(用以处理异常)。
  • 异常对象的类型会被拿来逐一地和每个catch子句比对。如果类型符合,那么该catch子句的内容便会被执行。
  • 有时候我们可能无法完成异常的完整处理。在记录信息之外,我们或许需要重新抛出(rethrow)异常,以寻求其它catch子句的协助,做进一步的处理。重新抛出时,只需要写下关键字throw即可。它只能出现于catch子句中。它会将捕获的异常对象再一次抛出,并由另一个类型吻合的catch子句接受处理。
  • 如果我们想要捕获任何类型的异常,可以使用一网打尽(catch-all)的方式。只需在异常声明部分指定省略号(…)即可,像这样:
    1
    2
    3
    4
    5
    //捕获任何类型的异常
    catch( ... )
    {
    ...
    }

7.3 提炼异常

  • catch子句应该和try块相应而生。try块是以关键字try作为开始,然后是大括号内的一连串程序语句。catch子句放在try块的末尾,这表示如果try块内有任何异常发生,便由接下来的catch子句加以处理。
  • 如果“函数调用链”不断地被解开,一直回到了main()还是找不到合适的catch子句。C++规定,每个异常都应该被处理。因此,标准库提供的terminate()便被调用——其默认行为时是中断整个程序的执行。

7.4 局部资源管理

  • Resource acquisition initialization(在初始化阶段即进行资源请求):对对象而言,初始化操作发生在constructor中,资源的请求亦发生在constructor内。资源的释放则应在destructor内完成。在异常处理机制终结某个函数之前,C++保证,函数中的所有局部对象的destructor都会被调用。
  • auto_ptr是标准库提供的class template,它会自动删除通过new表达式分配的对象。使用它前,必须包含相应的memory头文件:
    1
    #include <memory>

7.5 标准异常

  • 如果new表达式无法从程序的空闲空间(free store)分配到足够的内存,它会抛出bad_alloc异常对象。
  • 标准库定义了一套异常类体系(exception class hierarchy),其根部是名为exception的抽象基类。
  • bad_alloc派生自exception基类。
    1. https://blog.csdn.net/Poo_Chai/article/details/91596538 ↩︎