异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。异常使得我们能够将问题的检测与解决过程分离开来。
在C++语言中,我们通过抛出一条表达式来引发一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码将被用来处理该异常。被选中的处理代码是在调用链终于抛出对象类型匹配的最近的处理代码,处理完后继续执行处理代码块之后的代码。
异常的处理过程是根据栈展开的过程进行的。栈展开的过程中如退出了某些块某些局部对象会被自动销毁。
析构函数不应该抛出不能被它自身处理的异常。
异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此throw语句中的表达式必须拥有完全类型(拥有可见定义)。而且如果该表达式是是类类型的话需要有一个可访问的析构函数和可访问的拷贝或移动构造函数。如果该表达式是数组或函数类型,则会转换成对应的指针类型。
异常对象位于由编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问该空间。当异常处理完毕后,异常对象被销毁。
当我们抛出一条表达式时,该表达式的静态类型决定了异常对象的类型。
当我们推出一个块时对应对象会销毁,如果我们抛出的表达式是指向对应对象的指针则会出错。抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在。
catch子句中的异常声明看起来像是只包含一个形参的函数形参列表。声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型。它可以是左值引用但不能是右值引用。
当进入一个catch子句后,通过异常对象初始化异常声明中的参数。和函数的参数类似,也有引用和非引用的差别。
如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。如果是非引用类型,则异常对象将被切掉一部分。如果是引用也不能使用派生类特有的成员,但是可以使用派生类对应派生的虚函数。
最终找到的catch未必是最佳匹配而一定是第一个可以的匹配。因此,派生类异常的处理代码要放在基类异常的处理代码之前。在匹配的过程中绝大多数类型转换都是不允许的。
一条catch语句通过重新抛出的操作将异常传递给另外一个catch语句。这里的重新抛出仍然是一条throw语句,只不过不包含任何表达式。空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内。如果在其他区域出现了空throw语句直接terminate。一个重新抛出语句不指定新的表达式,只是将当前的异常对象沿着调用链向上传递。
为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码成为捕获所有异常。形如catch(…)。
在构造函数的初始值列表抛出异常函数体内的try语句块还未生效无法处理,要想处理,需要将构造函数写成函数try语句块。即将try写在初始值列表的冒号之前,catch写在函数体之后。
在C++11新标准中,我们可以通过提供noexcept说明指定某个函数不会抛出异常。对于一个函数来说,noexcept要么出现在所有声明和定义之后,要么一次也不出现。
编译器不会检查noexcept。如何我们指定了noexcept但是又抛出了异常程序会terminate。
noexcept接收一个可选的实参,如果为true则不会抛出异常,否则可能会。
noexcept同时也是一个一元运算符,返回值是一个bool,用于表示给定的表达式是否会抛出异常。不抛出则为true。和sizeof类似,不会求给定表达式的值。
一个做了不抛出异常的函数指针不能指向可能抛出异常的函数,一个没有做特殊说明或可以抛出异常的函数指针可以指向任何函数。
一个承诺了不会抛出异常的虚函数的后续派生也必须做出同样的声明。
我们也可以通过继承已有的标准异常类来编写我们自己的异常类。
多个库将名字放置在全局命名空间中将引发命名空间污染。
一个命名空间的定义包含两部分:首先是关键字namespace,然后是命名空间的名字。再然后是花括号括起来的声明和定义。只要能出现在全局作用域中的声明就能置于命名空间内。
命名空间既可以定义在全局作用域,也可以定义在其他命名空间内,但是不能定义在函数或类的内部。命名空间后无须分号。
命名空间可以定义在不同的部分。
通常我们不把#include放在命名空间内,因为这样意味着把头文件中的所有名字定义成该命名空间的成员。
命名空间可以定义在命名空间外部,但是这样的定义必须出现在所属命名空间的外层空间中,不能再一个不相关的作用域中定义。
模板特例化必须声明在原始模板所属的命名空间中。
全局作用域中定义的名字在全局命名空间中。全局命名空间是隐式的,没有名字,所有要使用全局命名空间的成员直接::xx即可。
C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间。和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。定义内联命名空间的方式是在关键字namespace前添加关键字inline。inline必须出现在命名空间第一次定义的地方,其他时候可以写也可以不写。
未命名的命名空间是指namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态声明周期:它们在第一次使用前创建,并且到程序结束才销毁。未命名的命名空间中的成员可以通过外层命名空间的名字来访问。未命名空间在不同文件中对应不同实体,即使包含的是同一个头文件或者名字相同。
我们可以通过一些更简便的方法使用命名空间的成员。例如using声明,别名,using指示等。
别名就是用namespace 别名 = 原名的方式起一个名字,using声明就是using std::xx这种,using指示就是using namespace std这种。
在一个文件中使用using指示,很有可能会和全局作用域中同名的事物产生二义性的问题,这个时候要加上域作用符::来指明。
尽量不要使用using指示。
命名空间中名字的查找遵循向上的原则,即在该点之后声明的名字不可见。
对于命名空间中名字的隐藏规则有一个重要的例外,当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类(及其)所属的命名空间。这一规则对于传递类的引用和指针同样有效。这个例外允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。同时,在命名空间中的一个类中如果有一个友元函数,函数形参有类类型,那么结合这个例外,该友元自动成为外面的命名空间的成员。我们可以在任意作用域直接使用这个函数。
如果在应用程序中定义了一个标准库中已有的名字,则要么按照重载的规则决定执行哪个,要么根本不会执行函数的标准库版本(所以std::move和std::forward要加限定符,因为这两个可以接受任意参数,很容易冲突)。
using声明和using指示引入的函数都会成为重载函数候选集的一部分。
当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。如果using声明所在的作用域中已经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误。
与using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数不会引发错误,只需要指明调用的是哪个空间的版本即可。
多重继承是指从多个基类直接基类中产生派生类的能力。
多重继承的派生类的构造函数初始值也只能初始化它的直接基类。派生类的构造函数初始值列表将实参分别传递给每个直接基类,其中基类的构造顺序与派生列表中基类的出现顺序一致。
在C++11新标准中,允许派生类从他的一个或几个基类中继承构造函数,但是如果有相同的构造函数,那么派生类必须定义自己的版本。
和单个基类的继承一样,多重继承的派生如果要实现自己的拷贝/赋值构造函数,那么也要考虑到直接基类的拷贝/赋值。
与单个基类类似,我们可以令某个可访问的基类(包括间接基类)的指针或引用直接指向一个派生类对象。编译器不会再派生类向基类的几种转换中进行比较和选择,在它看来转换到任意一种基类都一样好,如果有多个可以转换的,将产生编译错误。
与单个基类继承相同,对象、指针和引用的静态类型决定了我们能够使用哪些成员,在此基础上才有虚函数的多态。
当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用这些名字将引发二义性。派生类的查找过程和单基类继承类似,由下到上,只不过在多个基类中同时查找,所以会冲突。
尽管在派生列表中一个基类只能出现一次,但一个类依旧可能多次继承某个类,例如菱形继承,这种情况派生类就有某个类多个子对象了,可能出现问题。我们可以通过虚继承的方式来解决。虚继承是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象成为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚类子对象。
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
我们指定虚基类的方式是在派生列表中添加关键字virtual。
在虚派生中,虚基类是由最底层的派生类初始化的。
含有虚基类的对象的构造顺序与一般的顺序稍有区别:首先使用提供给最底层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生类列表中出现的顺序依次对其进行初始化。
虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。
一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。
对象的销毁顺序总是与构造顺序相反。
1:10
2:10
3:12
4:8
5:9
6:9
7:10
8:8
9:8
10:8
11:11
12:
13:10