18.1 异常处理
- 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。异常使得问题的检测与解决过程分离开来。
18.1.1 抛出异常
- 当执行一个throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的catch模块。
- 当程序抛出一个异常时,程序暂停当前函数的执行过程并立即开始查找最邻近的与异常匹配的 catch 子句。
- 一个异常如果没有被捕获,程序将调用标准库函数 terminate,它将终止当前的程序。
- 异常传递过程中,当退出了某些作用域时,该作用域内异常发生前创建的局部对象会被销毁。
- 栈展开过程中对象被自动销毁。
- 栈展开过程沿着嵌套函数的调用链不断查找,直到找到匹配的catch子语句。
- 在栈展开的过程中,运行类类型的局部对象的析构函数。因为这些析构函数是自动执行的,所以它们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序将被终止。
- 当抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。
- 抛出指针要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在。
18.1.2 捕获异常
- 通常情况下,如果catch接受的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。
- 如果在多个catch语句的类型之间存在着继承关系,则应该把继承链最底端的类放在前面,而将继承链最顶端的类放在后面。
- 一条catch语句通过重新抛出的操作将异常传递给另外一个catch语句。
- 如果catch(…)与其他几个catch语句一起出现,则catch(…)必须在最后的位置。出现在捕获所有异常语句后面的catch语句将永远不会被匹配。
18.1.3 函数try语句块与构造函数
- 处理构造函数初始值异常的唯一方法是将构造函数写成函数try语句块(函数测试块)。
18.1.4 noexcept异常说明
- 通过提供noexcept说明指定某个函数不会抛出异常。(C++11)
- noexcept可以用在两种情况下:一是确认函数不会抛出异常,二是根本不知道该如何处理异常。
- 通常情况下,编译器不能也不必在编译时验证异常说明。
- noexcept有两层含义:当跟在函数参数列表后面时它是异常说明符;而当作为noexcept异常说明的bool实参出现时,它是一个运算符。
- 函数指针声明为noexcept,那么指向的函数必须一致。
void alloc(int) noexcept(false); //alloc可能抛出异常
void recoup(int) noexcept(true); //recoup不会抛出异常
//recoup和pf1都承诺不会抛出异常
void (*pf1)(int) noexcept = recoup;
//正确:recoup不会抛出异常,pf2可能抛出异常,二者之间互不干扰
void (*pf2)(int) = recoup;
pf1 = alloc; //错误:alloc可能抛出异常,但是pf1已经说明了它不会抛出异常
pf2 = alloc; //正确:pf2和alloc都可能抛出异常
- 如果虚函数承诺不会抛出异常,则派生出来的虚函数也必须做出相同的承诺。
class Base {
public:
virtual double f1(double) noexcept; //不会抛出异常
virtual int f2() noexcept(false); //可能会抛出异常
virtual void f3(); //可能抛出异常
};
class Derived: public Base {
public:
double f1(double); //错误:Base::f1承诺不会抛出异常
int f2() noexcept(false); //正确:与Base::f2的异常说明一致
void f3() noexcept; //正确:Derived的f3做了更严格的限定,是允许的
};
18.1.5 异常类层次
- 和其他继承体系一样,异常类也可以看作按照层次关系组织的。层次越低,表示的异常情况就越特殊。
18.2 命名空间
- 多个库将名字放置在全局命名空间中将引发命名空间污染。
- 命名空间分割了全局命名空间,其中每个命名空间是一个作用域。
namespace cplusplus_primer {
class Sales_data { /*...*/};
Sales_data operator+(const Sales_data&, const Sales_data&);
class Query{/*...*/};
} //命名空间结束后无须分号
cplusplus_primer::Query q = cplusplus_primer::Query("hello");
//假设还有另一个命名空间AddisonWesley也提供了一个Query类
AddisonWesley::Query q = AddisonWesley::Query("hello");
18.2.1 命名空间定义
- 只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间。
- 命名空间不能定义在函数或类的内部。
- 命名空间作用域后面无须分号。
- 命名空间可以是不连续的。
//----Sales_data.h----
//#include应该出现在打开命名空间的操作之前
#include <string>
namespace cplusplus_primer {
class Sales_data {/*...*/};
Sales_data operator+{const Sales_data&, const Sales_data&);
//...
}
//----Sales_data.cc----
//确保#include出现在打开命名空间的操作之前
#include "Sales_data.h"
namespace cplusplus_primer{
//Sales_data成员及重载运算符的定义
}
//---user.cc---
#include "Sales_data.h" {
using cplusplus_primer::Sales_data;
Sales_data trans1, trns2;
//...
return 0;
}
- 定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型(或关联类型构成的集合)。
- 通常情况下,不把#include放在命名空间内部。
- 模板特例化必须定义在原始模板所属的命名空间中。
- 全局命名空间以隐式的方式声明,并且在所有程序中都存在。访问方式为::member_name。
namespace cplusplus_primer {
//第一个嵌套的命名空间:定义了库的Query部分
namespace QueryLib {
class Query {/*...*/};
Query operator&(const Query&, const Query&);
//...
}
//第二个嵌套的命名空间:定义了库Sales_data部分
namespace Bookstore {
class Quote{/*...*/};
class Disc_quote:public Quote{/*...*/};
//...
}
}
cplusplus_primer::QueryLib::Query
- 和普通的嵌套命名空间不同,内联命名空间(C++11)中的名字可以被外层命名空间直接使用。
//关键字inline必须出现在命名空间第一次定义的地方
inline namespace FifthEd {
//该命名空间表示本书第5版的代码
}
namespace FifthEd { //隐式内联
class Query_base {/*...*/};
//其他与Query有关的声明
}
namespace FourthEd {
class Item_base {/*...*/};
class Query_base {/*...*/};
//本书第四版用到的其他代码
}
namespace cplusplus_primer {
#include "FifthEd.h"
#include "FourthEd.h"
}
- 当应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到内联命名空间。
- 未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。
int i; //i的全局声明
namespace {
int i;
}
//定义在未命名空间中的名字可以直接使用
//二义性:i的定义出现在全局作用域、又出现在未命名的作用域
i = 10;
namespace local {
namespace {
int i;
}
}
//正确:与全局作用域中的i不同
local::i = 42;
- 和其他命名空间不同,未命名的命名空间仅在特定的文件内部有效,其作用范围不会横跨多个不同的文件。
- 如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。
- 使用未命名的命名空间取代文件中的静态声明。
- 命名空间的别名
namespace cplusplus_primer {/*...*/};
namespace primer = cplusplus_primer;
//指向一个嵌套的命名空间
namespace Qlib = cplusplus_primer::Querylib;
Qlib::Query q;
18.2.2 使用命名空间成员
- 不能在命名空间还没有定义前就声明别名,否则将产生错误。
- 一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。
- 一条using声明语句一次只引入命名空间的一个成员。
- using指示与声明不同,所有名字都可见。
- using指示不能出现在类的作用域中。
- 如果提供了一个对std等命名空间的using指示而未做任何特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题。
- 头文件最多只能在它的函数或命名空间内使用using指示或using声明。
- using指示也并非一无是处,例如在命名空间本身的实现文件中就可以使用using指示。
//命名空间A和函数f定义在全局作用域中
namespace A {
int i,j;
}
void f() {
using namespace A; //把A中的名字注入到全局作用域中
cout << i * j << endl; //使用命名空间A中的i和j
//...
}
namespace blip {
int i = 16, j = 15, k = 23;
//其他声明
}
int j = 0; //正确:blip的j隐藏在命名空间中
void manip() {
//using指示,blip中的名字被“添加”到全局作用域中
using namesapce blip; //如果使用了j,将在::j和blip::j之间产生冲突
++i; //将blip::i设定为17
++j; //二义性错误
++::j; //正确:将全局的j设定为1
++blip::j; //正确:16
int k = 97; //当前局部的k隐藏了blip::k
++k; //将当前局部的k设定为98
}
18.2.3 类、命名空间与作用域
- 命名空间内部名字查找规则:即由内向外依次查找每个外层作用域。
- 可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域。
- 对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行。
std::string s;
std::cin >>s;
//等价于operator>>(std::cin,s);
//为什么operator>>可以被直接调用
- 友元声明与实参相关的查找
namespace A {
class C {
//两个友元,在友元声明之外没有其他的声明
//这些函数隐式地成为命名空间A的成员
friend void f2(); //除非另有声明,否则不会被找到
friend void f(const C&); //根据实参相关的查找规则可以被找到
};
}
int main() {
A::C cobj;
f(cobj); //正确:通过在A::C中的友元声明找到A::f
f2(); //错误:A::f2没有被声明
}
18.2.4 重载与命名空间
- 一个using声明囊括了重载函数的所有版本以确保不违反命名空间的接口。
- 一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。
18.3 多重继承与虚继承
18.3.1 多重继承
- 多重继承关系中,派生类的对象包含有每个基类的子对象。
//派生类的派生列表中可以包含多个基类
class Bear:public zooAnimal { };
class Panda:public Bear, public Endangred {/*...*/};
- 构造一个派生类的对象将同时构造并初始化它的所有基类子对象。
//显式地初始化所有基类
Panda::Panda(std::string name, bool onExhibit)
:Bear(name, onExhibit, "Panda"), Endangered(Endangered::critical) { }
//隐式地使用Bear的默认构造函数初始化Bear子对象
Panda::Panda()
:Endangered(Endangered::critical) { }
- 与单继承一样,多重继承的派生类构造函数初始值也只能初始化它的直接基类。
//panda对象按如下次序进行初始化:
ZooAnimal --> Bear --> Endangered --> Panda
//panda对象析构函数的调用顺序:
Panda --> Endangered --> Bear --> ZooAnimal
- 基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。
- 如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本。
struct Base1 {
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};
struct Base2 {
Base2() = default;
Base2(const std::string);
Base2(int);
};
//错误:D1试图从两个基类中都继承D1::D1(const string&)
struct D1:public Base1,public Base2 {
using Base1::Base1; //从Base1继承构造函数
using Base2::Base2; //从Base2继承构造函数
};
//正确:为构造函数定义它自己的版本
struct D2:public Base1,public Base2 {
using Base1::Base1; //从Base1继承构造函数
using Base2::Base2; //从Base2继承构造函数
//D2必须自定义一个接受string的构造函数
D2(const string &s):Base1(s),Base2(s) { }
D2() = default;//D2定义了它自己的构造函数
//则必须出现
};
18.3.2 类型转化与多个基类
- 多重继承中,可以令某个可访问基类的指针或引用直接指向一个派生类对象。
- 与单继承一样,对象、指针和引用的静态类型决定了能够使用哪些成员。
//接受Panda的基类引用的一系列操作
void print(const Bear&);
void highlight(const Endangered&);
ostream& operator<<(ostream&, const zooAnimal&);
Panda ying_yang("ying_yang");
print(ying_yang); //把一个panda对象传递给一个Bear的引用
hightlight(ying_yang); //把一个panda传递给一个Endangered的引用
cout << ying_yang << endl; //把一个panda对象传递给zooAnimal的引用
//对编译器而言,转换到任意一种基类都一样好
void print(const Bear&);
void print(const Endangered&);
Panda ying_yang("ying_yang");
print(ying_yang); //二义性错误
18.3.3 多重继承下的类作用域
- 当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性。
- 要想避免潜在的二义性,最好的办法是在派生类中为该函数定义一个新版本。
//如果zooAnimal和Endangered都定义了名为max_weight的成员
//并且Panda没有定义该成员,则下面的调用是错误的:
double d = ying_yang.max_weight();
//为了避免二义性错误,最好的办法是为派生类提供一个版本
double Panda::max_weight() const
{
return std::max(zooAnimal::max_weight(),Endangered::max_weight());
}
18.3.4 虚继承
-
虚继承的目的是令某个类做出声明,承诺愿意共享它的基类。其中,共享的基类子对象称为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。
-
必须在虚派生的真实需求出现前就已经完成虚派生的操作。
-
虚派生只影响从指定了虚基类的派生类中进一步派生出的类,它不会影响派生类本身。
-
如果某个类指定了虚基类,则该类的派生仍按常规方式进行。
-
不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。
//关键字public和virtual的顺序随意
class Raccoon:public virtual zooAnimal {/*...*/};
class Bear:virtual public zooAnimal {/*...*/};
class Panda:public Bear, public Raccoon, public Endangered{}
void dance(const Bear&);
void rummage(const Raccoon&);
osgream& operator<<(ostream&,const zooAnimal&);
Panda ying_yang;
dance(ying_yang); //正确:把一个Panda对象当成Bear传递
rummage(ying_yang); //正确:把一个Panda对象当成Raccoon传递
cout << ying_yang; //正确:把一个Panda当成zooAnimal传递
18.3.5 构造函数与虚继承
- 在虚派生中,虚基类是由最低层的派生类初始化的。
- 含有虚基类的对象的构造顺序:首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。
- 虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关。
Bear::Bear(std::string name, bool onExhibit):
zooAnimal(name, onExhibit, "Bear") { }
Raccoon::Raccoon(std::string name, bool onExhibit):
zooAnimal(name, onExhibit, "Raccoon") { }
Panda::Panda(std::string name, bool onExhibit) :
ZooAnimal(name, onExhibit, "Panda"),
Bear(name,onExhibit),
Raccoon(name,onExhibit),
Endangered(Endangered::critical),
sleeping_flag(false) { }