《Essential C++》读书笔记
本文最后更新于:4 个月前
前言
Lippman在前言部分明确的阐明了本书的写作目的:“C++ Primer第三版其实无法扮演人们在初学C++时的导师角色。它太庞大了”。
结构与组织
结构与组织部分可能很好的说明了此书的组织架构,但我现在还看不懂,就先不重复了。之后会讲每一部分的内容附加到相应章节之前。
关于源代码
本书所有的程序,以及习题解答中的完整代码,都可以从网上获得。可以在Addison Wesley的网站或博文视点网站取得。
第1章 C++编程基础
第1章借着撰写一个具有互动性质的小程序,描绘C++语言预先定义的部分。这一章涵盖了内置的数据类型、语言预定义的运算符(operator)、标准库中的vector和string、条件语句和循环语句、输入和输出用的iostream库。
1.1 如何撰写C++程序
每个C++程序都是从一个名为main的函数开始执行,其通用形式如下:
1
2
3
4int 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
3expr
? 如果expr为true,就执行这里
: 如果expr为false,则执行这里 - OR逻辑运算符(||)可以同时检验多个表达式的结果。左侧表达式会被先求值,如果其值为
true
,剩下的另一个表达式就不需要再被求值(此所谓短路求值法)。 ### 运算符优先级(优先级从上到下)
- 逻辑运算符NOT
- 算术运算符(*, /, %)
- 算术运算符(+, -)
- 关系运算符(<, >, <=, >=)
- 关系运算符(==, !=)
- 逻辑运算符AND
- 逻辑运算符OR
- 赋值运算符
- 如果想要改变内置的运算符优先级,可利用小括号。
- 运算优先级是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 如何编写函数
- 每一个函数必须定义以下四个部分:
- 返回类型
- 函数名
- 参数列表
- 函数体
- 函数必须先被声明,然后才能被调用(被使用)。函数声明不必提供函数体,但必须明确返回类型、函数名,以及参数列表。
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提供默认参数
- 默认参数值的提供有两个不直观的规则:
- 默认值的解析(resolve)操作由最右边开始进行。如果我们为某个参数提供了默认值,那么这一参数右侧的所有参数都必须也具有默认参数才行。下面这样实属非法:
1
2//错误:没有为vec提供默认值
void display( ostream &os = cout, const vector<int> &vec); - 默认值只能指定一次,可以在声明处,亦可在函数定义处,但不能够在两个地方都指定。(头文件可为函数带来更高的可见性(visibility)为了更高的可见性,我们决定将默认值放在函数声明处而非定义处。)
- 默认值的解析(resolve)操作由最右边开始进行。如果我们为某个参数提供了默认值,那么这一参数右侧的所有参数都必须也具有默认参数才行。下面这样实属非法:
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
2const int seq_cnt = 6;
const vector<int>* (*seq_array[seq_cnt])( int );seq_array
定义前加上关键字extern
,便成为一个声明1
2const 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
2for (iter = svec.begin(); iter != svec.end(); ++iter)
cout << *iter << ' '; - iterator的定义形式: 此处iter被定义为一个iterator,指向一个vector,后者的元素类型为string。其初值指向svec的第一个元素。双冒号表示此iterator乃是位于string vector定义内的嵌套[nested]类型。
1
2vector<string> svec;
vector<string>::iterator iter = svec.begin(); - 如果要通过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 使用泛型算法
- 以下为四种常用的泛型搜索算法:
- find()用于搜索无序集合中是否存在某值。
- binary_search()用于有序集合的搜索。(效率比find()高)
- count()返回数值相符的元素数目。
- 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)三大类:- 六个算术运算:
plus<type>
,minus<type>
,negate<type>
,multiplies<type>
,divides<type>
,modules<type>
。 - 六个关系运算:
less<type>
,less_equal<type>
,greater<type>
,greater_equal<type>
、equal_to<type>
,not_equal_to<type>
。 - 三个逻辑运算,分别对应&&、||、!运算符:
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
3int 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运算符:
- back_inserter()会以容器的push_back()函数取代assignment运算符。
- inserter()会以容器的insert() 函数取代assignment运算符。
- 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
6class Stack {
public:
// public接口
private:
// private的实现部分
}; - 所有member function必须在class主体内进行声明。如果在class主体内定义,这个member function会自动被视为inline函数。
4.2 什么是构造函数和解析函数
- 构造函数(constructor):编译器在每次class object被定义出来时使用的初始函数。
- constructor的函数名必须与class名相同。语法规定,constructor不应指定返回类型,亦不用返回任何值。它可以被重载。
- 以下代码无法成功定义一个Triangular object: 因为C++必须兼容C。对C而言,t5之后带有小括号,会使t5被视为函数。 ### Member Initialization List(成员初始化列表)
1
Triangular t5();
- 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)
- 有两种情况,虚函数机制不会出现预期行为:
- 基类的constructor和destructor内;
- 当我们使用的是基类对象,而非基类对象的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表达式可以分解为两个操作:
- 向程序的空闲空间(free store)请求内存。如果分配到足够的空间,就分配一个指针,指向新对象。
- 如果第一步成功,并且外界指定了一个初值,这个新对象便会以最适当的方式被初始化。
- 二叉树中移除某值的一般算法是,以节点的右子节点取代节点本身,然后搬移左子节点,使它成为右子节点的左子树的叶节点。
- 声明一个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基类。
- https://blog.csdn.net/Poo_Chai/article/details/91596538 ↩︎