C++ 学习笔记之(18)-大型工程工具(异常处理、命名空间和多重继承与虚继承)
异常处理
异常处理(exception handling)机制能够对程序在运行时就出现的问题进行通信并作出相应的处理。
抛出异常
C++语言通过 抛出(throwing)表达式来 引发(raised)异常。throw
后,程序控制权交给对应的catch
模块,即throw
后的语句将不再被执行。
- 栈展开(stack unwinding):沿着嵌套函数的调用链不断查找,直到找到与异常匹配的
catch
子句继续执行。或者最终没有找到,调用标准库函数terminate
, 终止程序的执行过程 - 析构函数中,对于某个可能抛出异常的操作,应放置在
try
语句块中,并在析构函数内部处理。否则可能无法正确释放资源 - 异常对象(exception object):特殊的对象,编译器使用异常抛出表达式对异常对象进行拷贝初始化
- 若表达式是类类型,则类必须含有可一个可访问的析构函数和一个可访问的拷贝或移动构造函数
- 若表达式是数组类型或函数类型,则表达式将被转换成与之对应的指针类型
- 异常对象位于编译器管理的空间中,处理完毕后,异常对象被销毁
- 抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在
捕获异常
- 通常,如果
catch
接受的异常与某个继承体系有关,则最好将该catch
的参数定义成引用类型 - 异常的类型与
catch
声明的类型是精确匹配的
- 允许从非常量向常量的类型转换
- 允许从派生类向基类的类型转换
- 数组/函数被转换成指向数组(元素)/函数类型的指针
- 包括标准算数类型转换和类类型转换在内的其他所有转换不能在匹配
catch
的过程中使用
- 重新抛出(rethrowing):将异常传递给另一个
catch
语句,只需throw
语句,不包含表达式 - 捕获所有异常(catch-all):形如
catch(...)
, 若catch(...)
与其他catch
语句一起出现,则catch()
必须在最后的位置。否则出现在捕获所有异常语句后面的catch
语句将永远不会被匹配
函数try
语句块与构造函数
若想处理构造函数初始值抛出的异常,必须将构造函数写成 函数
try
语句块也称为 函数测试块(function try block)的形式初始化构造函数参数时也可能发生异常,但这种情况不属于函数
try
语句块template <typename T> Blob<T>::Blob(std::initializer_list<T> il) try: data(std::mnake_shared<std::vector<T>>(il)) {} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }
noexcept
异常说明
若预先知道函数不会抛出异常有助于简化调用该函数的代码,且编译器可执行某些特殊的优化操作
C++11新标准,可通过提供noexcept说明(noexcept specification)指定某个函数不会抛出异常。其形式是关键字
noexcept
紧跟在函数参数列表后若函数说明了
noexcept
的同事又含有throw
语句或者调用了可能抛出异常的其它函数,编译器可能会通过。一旦抛出异常,程序会调用terminate
以确保遵守不在运行时抛出异常的承诺noexcept
说明符接受一个可选的实参,该市餐必须能转换为bool
类型noexcept运算符:一元运算符,返回一个
bool
类型的右值常量表达式,表示给定的表达式是否会抛出异常void recoup(int) noexcept; // 不会抛出异常 void recoup(int) throw(); // 等价声明 void recoup(int) noexcept(true); // recoup 不会抛出异常 void alloc(int) noexcept(false); // alloc 可能抛出异常 noexcept(recoup(i)); // 如果 recoup 不抛出异常则结果为 true, 否则为 false noexcept(e); // 判断 e 调用的所有函数是否都做了不抛出异常说明且 e 本身不含 throw 语句时
函数指针与该指针所指的函数必须具有一致的异常说明。若函数指针声明不抛出异常,则只能指向不抛出异常的函数;若函数指针可能抛出异常,则可以指向任何函数
void (*pf1)(int) noexcept = recoup; // recoup 和 pf1 都不会抛出异常 void (*pf2)(int) = recoup; // 正确:recoup 不会抛出异常, pf2 可能抛出异常,二者互不干扰 pf1 = alloc; // 错误:alloc 可能抛出异常,但是 pf1 已声明不会抛出异常 pf2 = alloc; // 正确:pf2 和 alloc 都可能抛出异常
若虚函数声明不抛出异常,则派生类的虚函数必须声明
异常类层次
标准库异常类的继承体系如下
- 运行时错误:程序运行时才能检测到的错误
- 逻辑错误:程序代码中发现的错误
命名空间
命名空间污染(namespace pollution)由多个库将名字放置在全局命名空间中引发。 命名空间(namespace)为防止名字冲突提供了可控机制,分割了全局命名空间,其中每个命名空间是一个作用域。
命名空间定义
命名空间的名字在定义它的作用域中需保持唯一,命名空间可定义在全局作用域内,也可定义在其他命名空间中,但不能定义在函数或类的内部
每个命名空间都是一个作用域, 且命名空间可以不连续,
全局命名空间(global namespace):全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)定义在全局命名空间,隐式声明,且在所有程序都存在,
::member_name
命名空间可嵌套,内层命名空间作用域中隐藏外层命名空间同名成员
内联命名空间(inline namespace):C++11定义,成员可被外层命名空间直接使用
namespace A{ namespace B { int x; } inline namespace C { int y; } } A::B::x; A::j; // 可直接使用内联空间成员
未命名的命名空间:关键字
namespace
后紧跟花括号,其内变量拥有静态生命周期,第一次使用前创建,程序结束时销毁,仅可在文件内不连续,不能跨越多个文件静态声明
static
已被取消,现在的做法是使用未命名的命名空间
使用命名空间成员
命名空间别名
namespace primer = cplusplus_primer
using声明(using declaration)
:一次只引入命名空间的一个成员,using std::endl;
using指示(using directive)
:无法控制那些名字课件,因为都可见using namespace std;
对于命名空间中名字的隐藏规则有个例外:即给函数传递一个类类型的对象时,除了在常规作用域查找,还会查找实参类所属的命名空间
std::string s; std::cin >> s; operator>>(std::cin, s); // 等价于上式。由于`形参为类类型,故对 operator>> 的查找会包括 cin 和类所属的命名空间,即查找定义了 istream 和 string 的命名空间 std
对标准库模板函数
std::move
和std::forward
来说,尽量书写完整
重载与命名空间
using
声明引入的是名字,而非特定函数,即如果为函数书写using
声明,会将函数的所有版本引入。若using
声明作用域已出现同名函数,则会重载;若出现在局部作用域,则会隐藏外层作用域相关声明;若所在作用域有同名且形参列表相同的函数,则引发错误
多重继承与虚继承
多重继承(multiple inheritance):指从多个直接基类中产生派生类的能力。派生类继承了所有父类的属性
多重继承
- 构造派生类对象会同事构造并初始化所有基类子对象,且顺序与派生列表中基类的出现顺序一致
- C++11允许派生类从一个或多个基类中继承构造函数,但若构造函数相同,则错误。需要自定义
- 多个基类的情况下,任意一种基类的指针或引用都可直接指向一个派生类对象
- 单基类继承,派生类作用域嵌套在直接基类和间接基类的作用域下。而多重继承,有可能出现派生类从两个或多个基类中继承同名成员的情况,此时不加前缀限定符直接使用该名字会引发二义性
虚继承
派生类会多次或直接或间接地继承同一基类,会导致基类的多份拷贝。故通过 虚继承(virtual inheritance)的机制解决,共享的基类子对象称为 虚基类(virtual base class)。 在这种机制下,不论虚基类在集成体系出现多少次,派生类中只会包含唯一一个共享的虚基类子对象
// 关键字 public 和 virual 顺序随意, 下列代码将 A 定义为 B 和 C 的虚基类
class B: public virtual A { /* ... */ };
class C: public virtual A { /* ... */ };
- 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关,其后再按声明顺序逐一构造其他非虚基类
结语
C++的异常处理、命名空间以及多重继承或虚继承适合处理大规模问题。
- 异常处理将程序的错误检测部分与错误处理部分分割开
- 命名空间用啦管理大规模复杂应用程序,一个命名空间是一个作用域,可在其中定义对象、类型、函数、模板以及其他命名空间。标准库定义在名为
std
的命名空间中 - 多重继承即一个派生类可从多个直接基类继承而来。派生类对象中既包含派生类部分,也包含每个基类的基类部分,可能会引入新的名字冲突并造成来自于基类部分的二义性问题
- 使用虚继承,可使继承同一基类的多个类共享虚基类,派生类中只会有一个共享虚基类的副本