用于大型程序的工具:异常处理、命名空间和多重继承
特殊要求:
- 在独立开发的子系统之间协同处理错误的能力
- 使用各种库(可能包含独立开发的库)进行协同开发的能力
- 对比较复杂的应用概念建模的能力
1.异常处理:
- 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并作出相应的处理
- 异常使得我们能够将问题的检测与解决过程分离开来
1.1 抛出异常
- 通过抛出(throw)一条表达式来引发一个异常,被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码将被用来处理该异常
- 执行throw时,跟在throw后面的语句将不再被执行,程序控制权由throw转移到与之匹配的catch模块
- 当throw出现在一个try语句块内时,检查与try关联的catch子句,如果找到匹配的catch,就用该catch处理异常;如果该步没找到,则找外层try匹配的catch子句,如果还是找不到,则到外层函数中查找;上述过程称为栈展开
- 一个异常如果没有被捕获,则程序将调用标准库函数terminate终止当前程序
析构函数与异常
- 如果在栈展开过程中退出了某个块,则编译器负责确保构造或初始化的元素被正确销毁(类则采用析构函数进行析构)
- 一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止
- 如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块中,并且在析构函数内部得到处理
异常对象
- 一种特殊对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化
- 抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象必须存在(不能返回局部对象的指针)
- 当我们抛出一条表达式时,该表达式的静态编译类型决定了异常对象的类型(即不发生动态绑定)
1.2 捕获异常
- catch子句的异常声明的类型决定了所能捕获的异常类型(该类型必须是完全类型,可以是左值引用,但不能是右值引用)
- 如果catch的参数类型是非引用类型,则该参数是异常对象的副本;如果是引用,则该参数是异常对象的一个别名
- 如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化(catch无法使用派生类特有的任何成员);如果catch的参数是非引用类型,则异常对象将被切掉一部分
- 如果catch接受的异常与某个继承体系有关,则最好将catch的参数定义成引用类型
查找匹配的处理代码
- 在搜索catch时,首先挑选出来的应该是第一个与异常匹配的catch语句,越是特例化的catch越应该置于整个catch列表的前端(catch语句按照其出现的顺序逐一进行匹配的)
- 如果多个catch语句的类型之间存在着继承关系,则我们应该把继承链最底端的类放在前面,而将继承链最顶端的类放在前面
重新抛出
- 一条catch语句通过重新抛出的操作将异常传递给另外一个catch语句(重新抛出的语句throw;)
- 如果在处理代码之外的区域遇到了空throw语句,编译器将调用terminate
捕捉所有异常
catch(...) { //处理异常的某些特殊操作 throw; }
- 要想处理构造函数初始值抛出的异常,必须将构造函数写成函数try语句块的形式:
//例
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :
data(std::make_shared<std::vector<T>>(il)) {
/*...*/
} catch(xonst std::bad_alloc &e) { handle_out_of_memory(e); }
1.3 noexcept异常说明
- 提供noexcept说明指定某个函数不会抛出异常,关键字noexcept紧跟在函数的参数列表后面
void recoup(int) noexcept; //不会抛出异常 void alloc(int); //可能抛出异常
违反异常声明
- 一个noexcept函数抛出了异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺
- noexcept可以用在两种情况:一种是确认函数不会抛出异常,二是我们根本不知道该如何处理异常
异常说明的实参/\noexcept运算符
- noexcept说明符接受一个可选的实参,如果该实参是true则函数不会抛出异常;否则可能抛出异常
void recoup(int) noexcept(true); //不会抛出异常 void alloc(int) noexcept(false); //可能抛出异常
2.noexcept运算符:
noexcept(e); //当e不抛出异常时为true
异常说明与指针、虚函数和拷贝控制
- 函数指针及该指针所指函数必须具有相同的异常声明
- 若隐式或显示说明某个指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以
- 如果虚函数承若了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常
- 使用自定义异常类的方式与使用标准异常类的方式一样,一处抛出在另一处捕获并处理
2.命名空间:
2.1 定义命名空间
- 多个库将名字放置在全局命名空间中将引发命名空间污染;命名空间为防止名字冲突提供了更加可控的机制
- ~命名空间由关键字namespace+名字构成;~能出现在全局作用域中的声明就能置于命名空间内;~命名空间作用域后面无须分号
//例 namespace cplusplus_primer { class Sales_data { /*...*/}; Sales_data operator+(const Sales_data&, const Sales_data&); class Query {/*...*/}; class Query_base {/*...*/}; }
- 定义在命名空间内部的名字可以被该命名空间内的其他成员直接访问,位于命名空间外的代码必须明确指出所用的属于哪个命名空间:
cplusplus_primer::Query q = cplusplus_primer::Query("hello");
- 命名空间可以不连续:使得可以将几个独立的接口和实现文件组成一个命名空间
- 模板特例化必须定义在原始模板所属的命名空间中,如果在命名空间中声明了特例化,就能在外部定义它了:
//例 namespace std { template <> struct hash<Sales_data>; //在模板原始空间声明特例化 } template <> struct std::hash<Sales_data> { /*...*/ }
全局命名空间
- 全局命名空间以隐式的方式声明,并且在所有程序中都存在:
::member_name; //访问全局作用域成员
嵌套的命名空间
- 定义在其他命名空间中的命名空间
- 访问其中的成员需加上多层限定符
内联的命名空间
- 在关键字namespace前添加关键字inline
- 内联命名空间中的名字可以被外层命名空间直接使用
未命名的命名空间
- 关键字namespace+{}
namespace { int i; }
- 未命名的命名空间中定义的变量拥有静态生命周期,即第一次使用前创建,直到程序结束才销毁
- 定义在未命名的命名空间中的名字可以直接使用
- 未命名的命名空间定义在最外层作用域中,其中的名字要与全局作用域中的名字有所区别
2.2 使用命名空间成员:
- 为命名空间设定一个短的别名:
namespace cplusplus_primer {/*...*/} namespace primer = cplusplus_primer; //primer是当前命名空间的别名
using声明/using指示
- 一条using声明语句一次只引入命名空间的一个成员,有效范围从using声明的地方开始,一直using声明所在的作用域结束为止
- using指示以关键字using开始,后面是关键字namespace以及命名空间的名字,将命名空间的所有成员引入
namespace blip { int ival; double dval; } void manip() { /*...*/ using blip::dval; //using声明 using namespace blip; //using指示 }
2.3 类、命名空间与作用域
- 对命名空间内部名字的查找遵循常规的查找规则:由内向外依次查找每个外层作用域
- 可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域
- 当我们给函数传递一个类类型的对象时,除了常规的作用域查找还会查找实参类所属的命名空间
- 标准库move和forward函数,都接受一个右值引用的函数形参,该形参可以匹配任何类型,容易与其他同名的函数产生名字冲突
- 一个另外的未声明的类或函数如果第一次出现在友元声明中,则我们认为它时最近的外层命名空间的成员
2.4 重载与命名空间
- using声明与using指示能将某些函数添加到候选函数集(函数匹配过程)
- using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数;如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明
- using指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中:
namespace libs_R_us { ertern void print(int); ertern void print(double); } void print(const string &); using namespace libs_R_us; //print调用此时的候选函数包括: //libs_R_us的print(int) //libs_R_us的print(double) //显示声明的print(const string &) void fooBar(int ival) { print("Value:"); print(ival); }
3.多继承与虚继承:
3.1多重继承
- 多重继承,顾名思义,是指从多个直接基类中产生派生类的能力
- 每个基类包含一个可选的访问说明符,如果访问说明符被忽略掉了,则关键字class对应的默认访问说明符是private,关键字struct对应的是public
class CADVehicle : public CAD, Vehicle {...} //公有继承CAD,私有继承Vehicle
- ~~构造一个派生类的对象将同时构造并初始化它的所有基类子对象,~~多重派生的派生类的构造函数初始值也只能初始化它的直接基类,~~如不是显示初始化,则会使用基类的默认构造函数进行初始化
- 如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本
- 派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的
类型转换与多个基类
- 在只有一个基类的情况下,派生类的指针或引用能自动转换成一个可访问基类的指针或引用
- 可以令某个可访问基类的指针或引用直接指向一个派生类对象,但该指针只能访问其对应的基类部分或者基类的基类部分
多重继承下的类作用域
- 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性
- 要想避免潜在的二义性,最好的方法是在派生类中为该函数定义一个新版本
3.2虚继承
- 通过虚继承解决派生类中包含多个间接基类的子对象问题
- 共享的基类子对象称为虚基类,定义虚基类的方式是在派生列表中添加关键字virtual
- 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身
- 不管基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作
- 成员被多于一个基类覆盖,需进行分类讨论
构造函数和虚继承
- 在虚派生中,虚基类是由最低层的派生类初始化的,否则虚基类将会在多条继承路径上被重复初始化
- 含有虚基类的对象的构造顺序与一般的顺序有区别:先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部份,接下来按照直接基类在派生列表中出现的次序对其进行初始化
- 对象的销毁顺序与构造顺序相反