S18用于大型程序的工具
一、异常处理
1、抛出异常
C++中,通过抛出(throwing)一条表达式来引发(raised)一个异常,被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码(handler)将被用来处理该异常
注意:当执行一个throw
时跟在throw
后的语句不再被执行,程序控制权将转移到与throw
匹配的catch
模块,意味着1.沿着调用链的函数可能会提早退出;2.一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁
(1)栈展开
- 当抛出异常后,程序暂停执行并立刻开始寻找处理代码,若处于一个
try
块内,则检查与之关联的catch
子句,若找到匹配的catch
子句则进入执行,否则若没有找到匹配的catch
子句且这个try
块外还有try
块,则到上一层try
块的catch
子句中继续查找(查找会从当前函数退出到上一层函数中),不断向外层try
块展开查找catch
子句的过程为栈展开 - 最终找到了匹配的
catch
子句,则执行完毕后从当前try
关联的所有catch
子句后开始继续执行 - 最终没有找到,程序将调用标准库函数
terminate
终止程序的执行
注意:一个异常如果没有被捕获,则它将终止当前的程序
(2)栈展开过程中对象被自动销毁
栈展开可能伴随着从一个函数回退到上一个函数,则局部对象会被自动销毁,类类型的析构函数会被自动调用,若是在构造过程中发生异常,也要确保已构造的那一部分被正确销毁
(3)析构函数与异常
析构函数可能在栈展开过程中被调用,因此析构函数不能抛出自身无法处理的异常,否则若析构函数抛出异常且自身无法处理则程序会被终止
(4)异常对象
异常对象是一种特殊的对象,编译器使用异常表达式对异常对象进行拷贝初始化,throw
语句的表达式必须是完全类型的,且如果表达式是类类型则必须有一个可访问的析构函数和一个可访问的拷贝/移动构造函数,当异常处理结束后异常对象会被销毁
注意:当抛出一条表达式时,其静态编译类型决定了异常对象的类型,因此当一条表达式解引用一个基类指针时,若实际指向派生类则抛出的对象会被切掉一部分,只有基类的部分被抛出
注意:若抛出指针则要求在任何对应的处理代码存在的地方,指针所指的对象都必须存在
2、捕获异常
(1)异常声明
catch
子句中的异常声明类似一个包含形参的函数形参列表,声明的类型决定了处理代码能捕获的异常类型,当进入一个catch
语句后通过异常对象来初始化异常声明中的参数
注意:通常情况下,如果catch
接受的异常与某个继承体系有关,则尽可能将该catch
的参数定义成引用类型,否则若用某派生类的异常对象初始化基类的异常声明,会导致异常对象被切掉一部分
(2)查找匹配的处理代码
throw
语句与catch
的匹配是基于栈展开顺序的,因此最终选择的catch
未必是最佳匹配,若存在继承关系,则处理派生类异常的代码必须在处理基类异常的代码之前,与函数匹配不同,绝大多数类型转换在catch
匹配中是不允许的,只允许以下:
- 允许
non-const
向const
的类型转换,即非常量对象的throw
语句可以匹配接受常量引用的catch
语句 - 允许从派生类向基类的类型转换
- 数组被转换成指向首元素的指针,函数被转换成指向函数类型的指针
(3)重新抛出
一条catch
语句通过重新抛出可以将异常继续传递给另一个catch
语句,重新抛出通过一条不包含任何表达式的throw;
完成,只能在catch
语句或catch
直接/间接调用的函数内重新抛出,异常将沿着调用链向上传递
(4)捕获所有异常的处理代码
catch
的异常声明用...
可以捕获所有异常,与任意类型的异常匹配
注意:由于catch(...)
能捕获任意异常,当与多个catch
一同出现时必须位于最底部
3、函数try
语句块与构造函数
由于构造函数是先执行初始值列表,再执行函数体的,因此在初始值列表抛出的异常无法在函数体中catch
执行,必须采用函数try
语句块(函数测试块)
template <typename T>
Blob<T>::Blob(initialize_list<T> il)
try: data(make_shared<vector<T>>(il)
{...}
catch(const bad_alloc &e)
{handle_err(e);}
**注意:若是在初始化构造函数的参数时发生异常,例如调用Blob
的构造函数并正在初始化参数il
时发生的异常,则不属于后面的try
,而是属于函数调用者的部分,应该在函数调用者的上下文中得到处理
4、noexcept
异常说明
(1)不抛出异常
noexcept
显式说明一个函数不会抛出异常,有利于编译器执行一些特殊的优化,noexcept
说明符应出现在对应函数的所有声明和定义中,位于尾置返回类型之前,类的成员函数中noexcept
应位于const
/引用限定符&
和&&
之后,位于final
/override
/虚函数=0
之前
(2)违反异常说明
一旦一个noexcept
函数抛出了异常,程序会调用terminate
以确保遵守不在运行时抛出异常的承诺
(3)异常说明的实参
noexcept
可以接受一个bool
类型的实参,若实参等价于true
则不会抛出异常,若等价于false
则可能会抛出异常
(4)noexcept
运算符
noexcept
也是一个一元运算符,返回一个bool
类型的右值常量表达式,用于表示给定的表达式是否抛出异常
void f() noexcept(noexcept(g())); //若g()及g()调用的函数不会抛出异常,则noexcept(g())为真,则f()也不抛
(5)异常说明与指针、虚函数和拷贝控制
- 若函数指针有
noexcept
声明,则其只能指向也有noexcept
的函数,若函数指针没有noexcept
,则两者都可指向 - 若虚函数有
noexcept
声明,则其派生的虚函数也必须noexcept
,若虚函数没有noexcept
,则派生函数都可有无noexcept
- 合成拷贝控制成员时,也生成一个异常说明,若对所有成员和基类的所有操作都承诺
noexcept
,则合成的也是noexcept
- 定义析构函数而没有给定异常说明时,会生成一个异常说明
5、异常类层次
(1)标准库的异常类
继承体系最顶层是exception
,表示某处出错但细节未知,runtime_error
一般指只有运行时才会检测到的错误,logic_error
一般指在代码中检查会发现的错误
(2)书店应用程序的异常类
class out_of_stock :public runtime_error //栈溢出只有运行时才会发生,继承自runtime_error
{
public:
explicit out_of_stock(const string &s) :runtime_error(s) {}
};
class isbn_mismatch :public logic_error //isbn不匹配一般是代码检测就可以发现的,继承自logic_error
{
public:
explicit isbn_mismatch(const string &s) :logic_error(s) {}
isbn_mismatch(const string &s, const string &lhs, const string &rhs) :logic_error(s), left(s), right(s) {}
const string left, right;
};
(3)使用自定义的异常类型
二、命名空间
注意:多个库将名字放置在全局命名空间中将引发命名空间污染
1、命名空间定义
(1)格式
由namespace
起头,随后是命名空间的名字,然后在一对花括号中声明各种会出现在全局作用域中的名字,例如类、变量及其初始化操作、函数及其定义、模板和其他命名空间(嵌套),命名空间的花括号后不需要分号
注意:命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部
(2)每个命名空间都是一个作用域
注意:通常不把#include
放在命名空间内部
(3)命名空间可以是不连续的
与其他作用域不同,同一个名字的命名空间可以不连续的在不同位置,使得可以将几个独立的接口和实现文件组成一个命名空间
- 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用这些成员的文件中
- 命名空间成员的定义部分则置于另外的源文件中
(4)定义命名空间成员
命名空间中定义的成员可以在命名空间中直接使用名字无须前缀,命名空间外定义的成员必须使用有前缀的名字
(5)模板特例化
模板特例化必须定义在原始模板所属的命名空间中,和其他命名空间类似,只要在命名空间中声明了特例化,就能在命名空间外部定义它了,但是需要带上命名空间前缀
(6)全局命名空间
全局作用域中定义的名字(即在所有类、函数及命名空间外)被隐式的定义在全局命名空间中,同样可以用作用域运算符,但没有前缀只有运算符
::member_name; //member_name是全局的
(7)嵌套的命名空间
内层命名空间声明的名字将隐藏外层命名空间声明的同名成员,在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码要想访问它必须在名字前添加限定符(除了内联命名空间)
(8)内联命名空间
在命名空间第一次定义的位置前加上inline
,后续再打开这个命名空间时就不必写inline
,而是自动隐式内联,外层命名空间访问内层内联命名空间时不需要前缀,可以直接使用
(9)未命名的命名空间
在namespace
后没有名字的是未命名的命名空间,拥有静态生命周期,一个未命名的命名空间在一个文件内可以不连续,但是不能跨越多个文件,若在头文件中有未命名的命名空间,则所有包含该头文件的各自有这个命名空间里名字的不同实体,与其他命名空间不同
注意:未命名的命名空间没有前缀,也不需要作用域运算符来访问
2、使用命名空间的成员
(1)命名空间的别名
在命名空间定义后使用namespace space_name = alias_name;
来建立别名,必须在命名空间定义后才可以声明别名
(2)using
声明:扼要概述
using
声明引入的名字从声明的位置开始,到using
声明所在的作用域结束为止
注意:在类的作用域中,这样的声明语句只能指向基类成员
(3)using
指示
using
指示以using namespace space_name;
使得命名空间中的名字全部可见,但是不能出现在类的作用域中
(4)using
指示与作用域
using
指示一般被看作是出现在最近的外层作用域中
namespace blip
{
int i = 16, j = 15, k = 23;
}
int j = 0;
void manip()
{
using namespace blip; //using指示,将blip中的名字“添加”到了最近的外层作用域,即全局作用域中
++i; //blip::i变为17
++j; //二义性错误,由于using指示使j也有全局作用域的效果,此时是::j还是blip::j?
++::j; //::指出是全局作用域的j,此时::j为1
++blip::j; //blip::指出是blip空间中的j,此时blip::j为16
int k = 97; //隐藏掉blip::k
++k; //局部的k为98
}
注意:当using
指示导致一个命名空间中的名字与被注入的作用域中名字存在冲突是允许的,但是当使用冲突的名字时需要指名所处的命名空间,否则会导致二义性错误
(5)头文件与using
声明或指示
头文件若有using
则会导致所有包含该头文件的文件都被注入这些名字,因此头文件最多只在其函数内部或命名空间内使用using
注意:尽可能避免using
指示,但是在命名空间本身的实现文件中就可以使用using
指示
3、类、命名空间与作用域
(1)命名空间内部的名字查找:遵循常规规则,由内向外逐层查找到最外层才终止,且只有位于开放的块中且在使用点之前声明的名字才被考虑
注意:除了类内部出现的成员函数定义之外,总是向上查找作用域
(2)实参相关的查找与类类型形参
当给函数传递一个类类型对象的对象/引用/指针时,除了在常规的作用域查找外还会查找实参类所属的命名空间,这个特别规则使得允许概念上作为类接口一部分的非成员函数无须单独的using
声明就能被程序使用
(3)查找与std::move
和std::forward
由于move
和forward
是接受右值引用形参的函数模板,可以匹配任意类型,因此move
和forward
引起的名字冲突非常常见,且由于move/forward
本身执行非常特殊的功能,因此使用时必须带上std
前缀,即std::move/std::forward
(4)友元声明与实参相关的查找
当类声明一个友元时,该友元声明并没有使得友元本身可见,然而若友元接受一个类的参数,则会因为这个参数而能找到函数
namespace A
{
class C
{
//两个友元,在友元声明之外没有其他声明
//这些函数隐式称为namespace A的成员
friend void f2(); //除非另有声明,否则不会被找到
friend void f(const C&); //根据实参有C,可以查找到f
}
}
int main()
{
A::C cobj;
f(cobj); //正确,通过A::C中的友元声明找到A::f
f2(); //错误,A::f2未被声明
}
4、重载与命名空间
(1)与实参相关的查找与重载
函数调用时,由于会到实参所属的命名空间查找函数,因此每个实参类(包括实参类的基类)所属命名空间找到的同名函数都将被添加到候选集中(即使某些函数在调用语句所在处不可见也是如此)
(2)重载与using
声明
using
只是声明一个名字,不应该带上参数列表等其他元素
- 进行
using
声明时,函数的所有版本都被引入到当前作用域 using
声明出现在某个作用域时,将隐藏外层作用域的同名声明- 若
using
所在作用域已经有一个与using
将要引入的函数同名且同形参列表,则没有指名命名空间的调用将导致函数调用二义性错误
(3)重载与using
指示
类似using
声明,using
指示则是将命名空间中所有内容都提升到所在作用域中
(4)跨越多个using
指示的重载
若有多个using
指示,则每个命名空间的所有内容都提升到所在作用域,来自每个命名空间的同名函数都会成为候选函数集
三、多重继承与虚继承
多重继承是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有父类的属性
1、多重继承
(1)多重继承的派生类从每个基类中继承状态
每个基类包含一个可选的访问说明符,多重继承的派生类表同样只能包含已经定义过的类(且这些类不能是final
的),并且在具体某个给定的派生列表中,同一个基类只能出现一次,派生类的对象包含每个基类的子对象
(2)派生类构造函数初始化所有基类
构造一个派生类对象将同时构造并初始化其所有基类子对象,同样,派生类的构造函数也只能初始化其直接基类
class Bear : public ZooAnimal {...};
class Panda : public Bear, public Endangered {...};
//隐式调用Bear默认构造函数,显式调用Endangered构造函数,Panda的构造顺序ZooAnimal->Bear->Endangered->Panda
Panda::Panda(string name, bool onExhibit) : Endangered(Endangered::critical) { }
注意:基类的构造顺序与派生列表中基类的出现顺序保持一致(参考以派生类为根,一个/多个最终基类为叶节点的DFS顺序,就是构造顺序),与构造函数中初始化顺序无关
(3)继承的构造函数与多重继承
C++11允许使用using class_name::class_name
来继承非默认、非拷贝、非移动的构造函数,若在多重继承中继承了不同基类的具有相同形参列表的构造函数,则出现错误,此时必须显式定义具有相同形参列表的派生类构造函数
struct Base1
{
Base1() = default;
Base1(const string &);
Base1(shared_ptr<int>);
};
struct Base2
{
Base2() = default;
Base2(const string &);
Base2(int);
};
struct D1: public Base1, public Base2
{
using Base1::Base1; //从Base1继承构造函数
using Base2::Base2; //从Base2继承构造函数
//发生冲突,Base1(const string &)和Base2(const string &)具有相同形参列表
};
struct D2: public Base1, public Base2
{
using Base1::Base1; //从Base1继承构造函数
using Base2::Base2; //从Base2继承构造函数
D2(const string &s): Base1(s), Base2(s) { } //重新定义自己的构造函数
D2() = default; //D2定义了自己的构造函数,编译器就不会合成默认构造函数,必须指定默认构造函数
};
(4)析构函数与多重继承
与单继承相同,派生类的析构函数只负责清楚派生类本身分配的资源,派生类的成员及基类都是自动销毁的,析构函数的调用顺序与构造函数相反
(5)多重继承的派生类的拷贝与移动操作
与单继承相同,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝/移动/赋值操作(即每个基类也必须有这些操作)
2、类型转换与多个基类
(1)与单继承类似,可以令某个可访问基类的指针/引用直接指向一个派生类对象
注意:由于派生类向多个基类的转换是同样好的,因此对于接受不同基类引用/指针的同名函数,调用会出错
(2)基于指针类型或引用类型的查找
一个对象/引用/指针的静态类型决定了哪些成员是可见的,名字查找的顺序,参考S15面向对象程序设计.六.1如下
假定调用p->mem()或p.mem()
1.首先确定p的静态类型(引用、指针、对象)
2.在p的静态类型对应的类中查找mem,如果找不到则依次到直接基类中不断查找直至顶端,若一直找不到则编译器报错
3.若找到了mem则进行函数匹配的常规类型检查,已确认本次调用是否合法
4.若调用合法,则编译器根据mem是否是虚函数产生不同的代码:如果mem是虚函数且是通过指针调用的(p->mem()),则将在运行时确定到底运行该虚函数的哪个版本,依据对象的动态类型;如果mem不是虚函数或是通过对象调用的(p.mem(),p不是引用,若引用则会动态绑定),则进行常规函数调用
3、多重继承下的类作用域
在单继承的情况下,查找过程沿继承体系自底向上,派生类的名字隐藏基类的同名成员;在多继承的情况下,查找过程在多条继承链中并行自底向上查找,(可以视当前类为树根,多个直接基类就是多个树的分支)若在超过一个直接基类的链中找到同名成员,则不加前缀的调用具有二义性,需要明确指出使用哪个版本(从多个基类分别继承同名成员本身是合法的,只是调用需要指名具体版本)
4、虚继承
(1)同一个类的多个子对象
虽然继承列表不允许出现多个相同的类,但是可以间接继承自同一个类
注意:由于有多个相同的子对象,因此使用这个子对象中名字时若不指名具体继承链,则二义性错误
(2)虚继承
虚继承的目的是令某个类做出声明,愿意共享它的基类,则共享的基类子对象称为虚基类,不论虚基类在多少继承链上出现多少次,最终在派生类中只包含唯一一个虚基类子对象
注意:虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,而不会影响派生类本身的,即例如A
是基类,虚继承A->B
,虚继承A->C
,虚继承并不影响B
或C
,多重继承B+C->D
则D
受影响只有一份A
(3)使用虚基类
在派生列表中添加关键字virtual
(与访问限定符的相对先后不影响)
(4)支持向基类的常规类型转换
无论基类是不是虚基类,派生类对象都能被可访问基类的指针/引用操作
(5)虚基类成员的可见性
假设有继承关系如下虚继承B->D1
, 虚继承B->D2
, D1+D2->D
,且B
中定义了成员x
,则通过D
访问x
可能有
- 若
D1
/D2
都没有x
,则D
访问x
解析为B
的成员,不存在二义性,D
只有一个B
对象(虚基类) - 若
D1
或D2
有一个有x
,则D
访问x
解析为D1
或D2
中的成员,不存在二义性,由于继承中名字隐藏,D1
或D2
中的x
隐藏了虚基类B的x
- 若
D1
/D2
都有x,则D
访问x
二义性错误,两条继承链上都有x
,解决方式是D
定义自己的x
5、构造函数与虚继承
(1)虚继承的对象的构造方式
在虚派生中,虚基类是由最底层的派生类初始化的(而常规继承中每个类都只能调用直接基类的构造函数进行初始化),含有虚基类的对象的构造顺序:首先使用提供给最底层派生类的构造函数的初始值初始化该对象的虚基类子对象,接下来按照直接基类在派生列表中出现的次序对其进行初始化
注意:虚基类总是先于非虚基类构造,与它们在继承体系中的次序和位置无关,并且若没有显示调用虚基类的构造函数,则会自动调用虚基类的默认构造函数
(2)构造函数与析构函数的次序
一个类可以有多个虚基类,这些虚基类的子对象按照它们在派生列表中出现的顺序依次构造;
编译器按照直接基类的声明顺序检查,若有虚基类,则按顺序及层次先构造虚基类,然后再按顺序及层次构造非虚基类;合成的拷贝/移动构造函数及合成的移动赋值运算符中的成员也按该顺序执行,析构函数调用顺序是构造的逆序
//虚继承练习18.29-18.30:
/* v for virtual
*
* Class---------|
* \ |
* Base |
* v/ v\ |
* D2 D1 |
* \ / |
* MI /-----
* \ /
* Final
*/
class Class
{
public:
Class() { cout << "Class()" << endl; }
};
class Base : public Class
{
public:
Base() : ival(0) { cout << "Base()" << endl; }
Base(int i) : ival(i) {}
Base(const Base &b) : ival(b.ival) {}
protected:
int ival;
};
class D1 : virtual public Base
{
public:
D1() : Base() { cout << "D1()" << endl; } //控制直接基类的初始化
D1(int i) : Base(i) {}
D1(const D1 &d) : Base(d) {}
};
class D2 : virtual public Base
{
public:
D2() : Base() { cout << "D2()" << endl; }
D2(int i) : Base(i) {}
D2(const D2 &d) : Base(d) {}
};
class MI : public D1, public D2
{
public:
MI() : Base() { cout << "MI()" << endl; } //注意!除了控制直接基类的初始化,还要控制虚基类的初始化
//虚基类由最底层派生类控制初始化
MI(int i) : Base(i), D1(i), D2(i) {}
MI(const MI &m) : Base(m), D1(m), D2(m) {}
};
class Final : public MI, public Class //warning C4584: 'Final': base-class
//'Class' is already a base-class of 'MI'
{
public:
Final() : Base() { cout << "Final()" << endl; }
Final(int i) : Base(i), MI(i) {}
Final(const Final &f) : Base(f), MI(f) {}
};
int main()
{
Final F; //Final中有1个Base,2个Class
return 0;
}
//输出如下:
Class()
Base()
D1()
D2()
MI()
Class()
Final()