一、异常处理
-
抛出异常
当执行一个throw时,跟在throw后面的语句将不再执行。相反,程序的控制权从throw转移到与之匹配的catch模块。该catch可能是同一个函数的局部catch,也可能位于直接或间接调用了发生异常的函数的另一个函数中。控制权从一处转移到另一处,这有两个重要的含义:
- 沿着调用链的函数可能会提早退出
- 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁
-
栈展开
当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch字句。如果对抛出异常的函数的调用位于try语句块内,则检查与该try块关联的catch子句。如果找到了匹配的catch,就使用该catch处理异常。否则,如果该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch字句。如果仍然没有找到匹配的catch,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。
上述过程被称为栈展开过程。栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句为止;或者也可能一直没找到匹配的catch,则退出主函数后查找过程终止。
如果没找到匹配的catch子句,程序将退出。因为异常通常被认为是妨碍程序正常执行的事件,所以一旦引发了某个异常,就不能对它置之不理。当找不到匹配的catch时,程序将调用terminate。
一个异常如果没有被捕获,则它将终止当前的程序。
栈展开过程中对象被自动销毁:如果在栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正确地销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。与往常一样,编译器在销毁内置类型的对象时不需要做任何事情。(析构函数仅仅是释放资源,一定要确保它们的析构函数不会引发异常)
-
异常对象
当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。如果一条throw表达式解引用一个基类指针,而给指针实际指向的是派生类对象,则抛出的对象将被切掉一部分。
-
查找匹配的处理代码
与实参和形参的匹配规则相比,异常和catch异常声明的匹配规则受到更多限制。此时,绝大多数类型转换都不被允许,要求异常的类型和catch声明的类型是精确匹配的:
- 允许从非常量向常量的类型转换,也就是说,一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句。
- 允许从派生类向基类的类型转换。
- 数组被转换成指向数组类型的指针,函数被转换指向该函数类型的指针。
-
重新抛出
有时,一个单独的catch语句不能完整地处理某个异常。在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。
这里的重新抛出仍然是一条throw语句,只不过不包含任何表达式:
throw;
-
捕获所有异常的处理代码
void mainp() { try { } catch(...) { } }
catch(...)既能单独出现,也能与其他几个catch语句一起出现。
如果catch(...)与其他几个catch语句一起出现,则catch(...)必须在最后的位置。出现在捕获所有异常语句后面的catch语句将永远不会被匹配。
-
虚函数处理
如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常。
class Base { public: virtual double f1(double) noexcept; virtual int f2() noexcept(false); virtual void f3(); }; class Derived : public Base { public: double f1(double); //错误 int f2() noexcept(false); void f3() noexcept; };
-
异常类层次
二、命名空间
-
未命名的命名空间
#include<bits/stdc++.h> using namespace std; namespace { int i; } namespace local { namespace { int i; } } int main() { i = 0; local::i = 1; cout << i << endl; cout << local::i << endl; }
-
using指示与作用域
#include<bits/stdc++.h> using namespace std; namespace blip { int i = 16,j = 15,k = 23; } int j = 0; int main() { using namespace blip; ++i; //++j;//错误 ++::j; ++blip::j; int k = 97; ++k; }
-
有元声明与实参相关的查找
#include<iostream> using namespace std; namespace A { class C { friend void f2(); friend void f(const C&); }; } int main() { A::C cobj; f(cobj); f2(); //错误 }
因为f接受一个类类型的实参,而且f在C所属的命名空间进行了隐式的声明,所以f能被找到。相反,因为f2没有形参,所以它无法被找到。
-
重载与命名空间
-
与实参相关的查找与重载
#include<iostream> using namespace std; namespace NS { class Quote { }; void display(const Quote&) { } } class Bulk_item : public NS::Quote { }; int main() { Bulk_item book1; display(book1); return 0; }
-
重载与using声明
using声明语句声明的是一个名字,而非一个特定的函数
using NS::print(int);//错误 using NS::print;
-
跨越多个using指示的重载
#include<iostream> using namespace std; namespace AW { int print(int); } namespace Primer { double print(double); } using namespace AW; using namespace Primer; long double print(long double); int main() { print(1); //AW print(3.1);//Primer }
-
三、多重继承与虚继承
-
继承的构造函数与多重继承
允许派生类从它的一个或几个基类中继承构造函数。但是如果从多个基类中继承了相同的构造函数,则程序将产生错误。
struct Base1 { Base1() = default; Base1(const string&); }; struct Base2 { Base2() = default; Base2(const string&); }; struct D1 : public Base1 , public Base2{ using Base1::Base1; using Base2::Base2; }; int main() { D1 d("123"); //错误 都继承D1::D1(const string&) }
#include<iostream> using namespace std; struct Base1 { Base1() = default; Base1(const string&) { } }; struct Base2 { Base2() = default; Base2(const string&) { } }; struct D2:public Base1,public Base2 { using Base1::Base1; using Base2::Base2; D2(const string&s):Base1(s),Base2(s) { } //D2() = default;//一旦D2定义它自己的构造函数,则必须出现 }; int main() { D2 d("123"); return 0; //cout << "hello world" << endl; }
-
基于指针类型或引用类型的查找
#include<iostream> using namespace std; class Bear { public: Bear() { cout << 1 << endl; } void print() { cout << 4 << endl; } }; class Panda : public Bear { public: Panda() { cout << 2 << endl; } void print() { cout << 3 << endl; } }; int main() { Bear *b = new Panda(); b->print(); } //1 2 4
与只有一个基类的继承一样,对象、指针和引用的静态类型决定了我们能够使用哪些成员。如果我们使用一个Bear指针,则只有定义在Bear中的操作是可以使用的,Panda接口中的Bear特有的部分都不可见。类似一个派生类指针或引用只能访问自身和基类的成员。
-
多重继承下的类作用域
class Bear { public: Bear() { cout << 1 << endl; } void print() { cout << 4 << endl; } int age; }; class ZooAnimal { public: int age; }; class Panda : public Bear,public ZooAnimal { public: Panda() { cout << 2 << endl; } void print() { cout << 3 << endl; } int max_age() { return max(ZooAnimal::age,Bear::age); } };
-
虚继承
虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生类层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
必须在虚派生的真实需求出现前就完成虚派生的操作。
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
-
使用虚基类
class Raccoon : public virtual ZooAnimal {}; class Bear : virtual public ZooAnimal {};
virtual说明符表明了一种愿望,即在后续的派生类当中共享虚基类的同一份实例。至于什么样的类能够作为虚基类并没有特殊规定。
如果某个类指定了虚基类,则该类的派生仍按常规方式进行:
class Panda: public Bear,public Raccoon, public Endangered{}
#include<bits/stdc++.h> using namespace std; class ZooAnimal { public: void print1() { cout << 1 << endl; } }; class Bear : public virtual ZooAnimal { public: void print2() { cout << 2 << endl; } }; class Panda : public Bear { public: void print3() { cout << 3 << endl; } }; int main() { Panda *b = new Panda(); b->print1(); b->print2(); b->print3(); }
-
虚基类成员的可见性
因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是如果成员被多于一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本。
例如,假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性:
- 如果在D1和D2中都没有x的定义,则x将被解析为B的成员,此时不存在二义性,一个D的对象只含有x的一个实例。
- 如果x是B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。
- 如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。
-
当创建Panda对象的时候:
(1)首先使用构造函数初始化列表中指定的初始化式构造ZooAnimal部分。
(2)接下来,构造Bear部分。忽略Bear的用于ZooAnimal构造函数初始化列表的初始化式。
(3)然后,构造Raccoon部分。再次忽略ZooAnimal初始化式。
(4)最后,构造Panda部分。
如果Panda构造函数不显式初始化ZooAnimal基类,就使用ZooAnimal默认构造函数;如果ZooAnimal没有默认构造函数,则代码出错。 -
无论虚基类出现在继承层次中任何地方,总是在构造非基类之前构造虚基类:
class Character{ /*....*/ }; class BookCharacter: public Character{ /*....*/ }; class ToyAnimal{ /*...*/ }; class TeddyBear: public BookChatacter,public Bear,public virtual ToyAnimal{ /*...*/ };
按声明次序检查直接基类,确定是否在虚基类。例中,首先检查BookChatacter的继承子树,然后检查Bear的继承子树,最后检查ToyAnimal的继承子树。按从根类开始向下倒最低层派生类的次序检查每个子树。
TeddyBear的虚基类的构造次序是先ZooAnimal再ToyAnimal。一旦构造了虚基类,就按声明次序调用非虚基类的构造函数:首先是BookChatacter,它导致调用Character的构造函数,然后是Bear。因此,为了创建TeddyBear对象,按下面次序调用构造函数:- ZooAnimal();
- ToyAnimal();
- Character();
- BookChatacter();
- Bear();
- TeddyBear();
在这里,由最低层派生类TeddyBear指定用于ZooAnimal和ToyAnimal的初始化式。