文章目录
- 第八章 继承 (Inheritance)
- 第九章 多态 (Polymorphism)
- 第十章 模板 (Template)
- 十一章 IO 流 (IO Stream)
- 十二章 异常 (Exception)
第八章 继承 (Inheritance)
8.1 引入
8.1.1 为什么需要继承 why inherit?
在 C++中代码的可重用性(software reusability)是通过继承(inheritance)这一机制来实现的。
如果没有掌握继承性,就没有掌握类与对象的精华。
8.1.2 引例
在”老师“与”学生“之前,代码重用性的思考。
//引例
//老师:姓名,性别,年龄,教课,吃饭
//学生:姓名,性别,年龄,学习,吃饭
//人类:姓名,性别,年龄,吃饭
//父类(继承关系) or 基类(派生关系):
class Human {
public:
void Eating(string food)
{
cout << "i am eating " << food << endl;
}
private:
string _name;
char _sex;
int _age;
};
//子类 or 派生类:
class Teacher:public Human {
public:
Teacher(string name ="NULL", char sex = 'N', int age = 0) :
_name(name), _sex(sex), _age(age)
{
}
void Teach(string course)
{
cout << "i am a teacher, and i am teaching " << course << endl;
}
private:
string _name;
char _sex;
int _age;
};
//子类 or 派生类:
class Student: public Human {
public:
Student(string name = "NULL", char sex = 'N', int age = 0):
_name(name),_sex(sex),_age(age)
{
}
void Learing(string course)
{
cout << "i am a student, and i am studying " << course << endl;
}
private:
string _name;
char _sex;
int _age;
};
int main(int argc, char** argv)
{
Teacher duan("DuanZhiQiang", 'M', 35);
duan.Teach("Chinese");
duan.Eating("BugerKing");
Student zheng("ZhengShaoJIe", 'M', 23);
zheng.Learing("C++");
zheng.Eating("KFC");
system("pause");
return 0;
}
Human是从学生和讲师中抽象出来的共同属性。让学生和老师均继承自Human的话, 就可以实现代码的重用性了。
8.1.3 结论
继承是一种设计的结果,通常是发生于一套类库中的,设计代码重用的方式,这种关系是一种设计而为之,不是想继承,就可随便继承的。
8.2 继承
8.2.1 定义
类的继承,是新的类从已有类那里得到已有的特性。或从已有类产生新类的过程就是 类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。 派生与继承,是同一种意义两种称谓。
8.2.1 定性 is-a Not has-a
is-a 是一种属于关系,如:狗属于一种动物,车属于一种交通工具(DogisanAnimal.Car is a Vehicle.)在面向对象中表现为一种继承关系。可以设计一个 Animal 类,Dog 类作为 Animal 类(基类)的派生类;设计一个Vehicle类,Car类作为Vehicle类(基类)的派生类。
has-a是一种包含、组合关系。如:车包含方向盘、轮胎、发动机(Car hassteering-wheel, wheels, engine),但不能说方向盘/轮胎/发动机是一种车;狗包含腿、尾巴,但不能说腿、 尾巴是一种狗。正确的应该说车聚合(包含)了方向盘、轮胎、发动机。 因此,如果 A is a B,则 B 是 A 的基类,A 是 B 的派生类。为继承关系。如果 A 包含 B, 则B 是 A 的组成部分。为聚合关系,可以由组成部分聚合成为一个类。 宏观意义上来讲, is-a 和 has-a 均可以实现代码的可重用性。
8.2.3 语法
class 派生类名:[继承方式] 基类名
{
派生类成员声明;
}
默认的继承方式是 private 私有继承。
一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类,称为单继承。 下面从单继承讲起。
8.2.4 继承方式
8.2.4.1 分类
公有继承(public):基类的公有成员(public)和保护成员(protected)在派生类中保持原有访问属性,其私有成员仍为基类的私有成员。
私有继承(privated):基类的公有成员(public)和保护成员(protected)在派生类中成了私有成员,其私有成员仍为基类的私有成员。
保护继承(protected):基类的公有成员(public)和保护成员(protected)在派生类中成了保护成员,其私有成员仍为基类的私有成员。
本章节只讨论公有继承。其他两种之后学习。
8.2.4.2 集成方式影响了什么
继承方式规定了子类如何访问从基类继承的成员。
继承方式有public,private,protected。继承方式不影响派生类的原访问权限,影响了从基类继承而来的成员的访问权限,包括派生类内的访问权限和派生类对象的访问权限。
可以发现,卡在了子类中父类的private成员上。这种现象称之为不可见。protected成员在外部访问的时候等价于privated;但是在继承中是可见的。
8.2.4.3 结论
如何验证,父类中 protected 成员,在 public 继承后,仍然是 protected 的呢?答案是, 再次用public派生出孙子类,在其类内考查其可见性。
8.2.5 派生类的组成
8.2.5.1 组成图示
派生类中的成员,包含两大部分,一类是从基类继承过来的,一类是自己增加的成员。 从基类继承过过来的表现其共性,而新增的成员体现了其个性。
这种派生类与基类的关系,我们称之为全盘接受。不管你基类里有什么,派生类通同继承下来,包括私有成员(除开构造器与析构器)。。基类有可能会造成派生类的成员冗余,比如,基类有20个功能,但是我只需要基类的一个功能,所以说基类是需设计的。
派生类有了自己的个性,使派生类有了意义。意思是,有自己的个性,这个派生类才有意义。
8.2.5.2 sizeof(父/子)
#include <iostream>
#include <typeinfo>
using namespace std;
//szieof(父/子)
class AA {
public:
AA()
{
cout << "AA构造" << endl;
cout << "&a " << &a << endl;
cout << "AA-this " << this << endl;
cout << typeid(this).name() << endl;
}
int a;
};
class BB: public AA {
public:
BB()
{
cout << "BB构造" << endl;
cout << "&b " << &b << endl;
cout << "BB-this " << this << endl;
cout << typeid(this).name() << endl;
}
int b;
};
class CC : public BB {
public:
CC()
{
cout << "CC构造" << endl;
cout << "&c " << &c << endl;
cout << "CC-this " << this << endl;
cout << typeid(this).name() << endl;
}
int c;
};
int main(int argc, char** argv)
{
CC cc;
cout << "&cc " << &cc << endl;
cout << typeid(cc).name() << endl;
system("pause");
return 0;
}
输出如下:
8.3 派生类的构造
派生类中,由基类继承而来的成员的初始化工作,还是由基类的构造函数完成,然后派生类中新增的成员在派生类的构造函数中初始化。
8.3.1 语法格式
派生类名 :: 派生类名( 总参列表 )
:基类名(参数表), 内嵌子对象( 参数表 )
{
派生类新增成员的初始化语句;
}
8.3.2 构造原理
8.3.2.1 图示
8.3.2.2 注释
由于子类中,包含了两部分内容,一部分来自父类,一部分来自子类。父类的部分,要由调用父类的构造器来完成,子类的部分,在子类的构造器中来完成始化。子类中, 有内嵌的子对象也需要构造。
顺序如下:
- 标配:1 默认无参构造器 2 重载(包含无参) 3 默参(包含无参)
- 先调用了基类的构造器, 或隐 “标配” 或显 “标配"也可以显,但无"标配”,必须得显。
- 然后内嵌子对象的构造, 或隐 “标配” 或显 “标配"也可以显,但无"标配”,必须得显。
- 必须得显,指得显示的在初始化参数列表调用, 初始化参数列表中的顺序,不代表真实的调用顺序。
就像家里来了客人,我们先给老爸倒水,再给客人倒水,最后再给自己倒水。
//派生类的构造
class AAA {
public:
AAA(int j):_a(j)
{
cout << "AAA构造" << endl;
//_a = 0;
}
int _a;
};
class CCC {
public:
CCC(int c) :_c(c)
{
cout << "CCC构造" << endl;
}
int _c;
};
class BBB:public AAA {
public:
BBB(int a, int b, int c):AAA(a), cc(c), _b(b)
{
cout << "BBB构造" << endl;
//_b = b;
}
int _b;
CCC cc;
};
int main(int argc, char** argv)
{
BBB b(3,5,10);
cout << b._a << b._b << b.cc._c << endl;
return 0;
}
输出如下:
8.3.3 实战
class Birth {
public:
Birth(int year, int month, int day)
:_year(year),_month(month),_day(day)
{
}
void DisBirth()
{
cout << "生日:" << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class Stu {
public:
Stu(string name,char sex,float score)
:_name(name),_sex(sex),_score(score)
{
}
void DisInfo()
{
cout << "姓名:" << _name << endl;
cout << "性别:" << _sex << endl;
cout << "分数:" << _score << endl;
}
private:
string _name;
char _sex;
float _score;
};
class GraStu:public Stu {
public:
GraStu(string name, char sex, float score, float salary,int year,
int month, int day)
: Stu(name, sex, score), birth(year, month, day)
{
_salary = salary;
}
void Dis()
{
DisInfo();
cout << "工资:" << _salary << endl;
birth.DisBirth();
}
Birth birth;
private:
float _salary;
};
class Doctor :public GraStu {
public:
Doctor(string title, string name, char sex, float score, float salary, int year,
int month, int day)
:GraStu(name, sex, score, salary, year,
month, day)
{
_title = title;
}
void DisDoctorInfo()
{
cout << "学位:" << _title << endl;
Dis();
}
private:
string _title;
};
int main(int argc, char** argv)
{
GraStu damon("Damon", 'M', 100.0, 30000, 1996, 12, 18);
damon.Dis();
putchar(10);
Doctor wang("王博士", "Wang", 'F', 100.0, 40000, 1993, 12, 18);
wang.DisDoctorInfo();
return 0;
}
继承层次化的设计,本层次初始化,仅需要考虑上次层,而不是考虑上上层次或是更上层次。
8.4 派生类的拷贝构造
拷贝构造,也是一种构造函数,也没有被继承下来。
8.4.1 语法
派生类::派生类(const 派生类& another)
:基类(another),派生类新成员(another.新成员)
{
//派生类新成员(another.新成员);
}
8.4.2 构造顺序
8.4.2.1 图示
8.4.2.2 注释
父类中,一部分成员需要拷贝构造来完成,子类中,也有一部成员需要拷贝构造来完成。子类中的内嵌子对象也需要拷贝构造来完成。
8.4.3 实战
class Birth {
public:
Birth(int year, int month, int day)
:_year(year),_month(month),_day(day)
{
}
Birth(const Birth& another)
{
this->_year = another._year;
this->_month = another._month;
this->_day = another._day;
}
void DisBirth()
{
cout << "生日:" << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class Stu {
public:
Stu(string name,char sex,float score)
:_name(name),_sex(sex),_score(score)
{
}
Stu(const Stu& another)
{
this->_name = another._name;
this->_score = another._score;
this->_sex = another._score;
}
void DisInfo()
{
cout << "姓名:" << _name << endl;
cout << "性别:" << _sex << endl;
cout << "分数:" << _score << endl;
}
private:
string _name;
char _sex;
float _score;
};
class GraStu:public Stu {
public:
GraStu(string name, char sex, float score, float salary,int year,
int month, int day)
: Stu(name, sex, score), birth(year, month, day)
{
this->_salary = salary;
}
void Dis()
{
DisInfo();
cout << "工资:" << _salary << endl;
birth.DisBirth();
}
GraStu(const GraStu& another)
:Stu(another), birth(another.birth)
{
this->_salary = another._salary;
}
Birth birth;
private:
float _salary;
};
class Doctor :public GraStu {
public:
Doctor(string title, string name, char sex, float score, float salary, int year,
int month, int day)
:GraStu(name, sex, score, salary, year,
month, day)
{
_title = title;
}
void DisDoctorInfo()
{
cout << "学位:" << _title << endl;
Dis();
}
private:
string _title;
};
int main(int argc, char** argv)
{
GraStu dian(damon);
dian.Dis();
system("pause");
return 0;
}
8.5 派生类的赋值重载
赋值运算符重载函数,不是构造器,可以看作是一个普通的函数,故可以被重载,语法上就没有构造器那么严格。
8.5.1 语法
子类& 子类::operator=(const 子类& another)
{
...
父类::operator =(another); // 调用父类的赋值运算符重载
...
}
8.5.2 赋值顺序
8.5.2.1 图示
//派生类的赋值运算符重载
class AAAAA {
public:
AAAAA(int a)
{
cout << "AAAAA" << endl;
this->_a = a;
}
AAAAA& operator=(const AAAAA & another)
{
cout << "AAAAA=" << endl;
this->_a = another._a;
return *this;
}
int _a;
};
class BBBBB :public AAAAA
{
public:
BBBBB(int a, int b)
:AAAAA(a)
{
cout << "BBBBB" << endl;
this->_b = b;
}
BBBBB& operator=(const BBBBB & another)
{
AAAAA::operator=(another);
cout << "BBBBB=" << endl;
this->_b = another._b;
return *this;
}
int _b;
};
int main(int argc, char** argv)
{
BBBBB b(10, 20);
BBBBB bb(30, 40);
b = bb;
cout << bb._a << bb._b << endl;
cout << b._a << b._b << endl;
return 0;
}
8.6 派生类的友元函数
8.6.1 友元在派生类
由于友元函数并非类成员,因引不能被继承,在某种需求下,可能希望派生类的友元函数能够使用基类中的友元函数。为此可以通过强制类型转换,将派生类的指针或是引用强转为其父类的引用或是指针,然后使用转换后的引用或是指针来调用基类中的友元函数。
8.7 派生类析构函数
析构函数的执行顺序与构造函数相反。 派生类的析构函数的功能,保证层级内与其对应的构造函数,完成清理工作。无需指明析构关系。 why? 析构函数无参,故无重载,无默参,故无需指明析构关系。
8.8 继承方式
8.8.1 类别
8.8.1.1 public
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。即基类的公有成员和保护成员被继承到派生类中仍作为派生类的公有成员和保护成员。派生类的其他成员可以直接访问它们。无论派生类的成员还是派生类的对象都无法访问基类的私有成员。
8.8.1.2 protected
保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可访问。派生类的其他成员可以直接访问从基类继承来的公有和保护成员, 但是类外部通过派生类的对象无法访问它们,无论派生类的成员还是派生类的对象,都无 法访问基类的私有成员。
8.8.1.3 private
**当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。**基类的公有成员和保护成员被继承 后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类外部通过派生 类的对象无法访问。无论是派生类的成员还是通过派生类的对象,都无法访问从基类继承 的私有成员。通过多次私有继承后,对于基类的成员都会成为不可访问。因此私有继承比较少用。
8.8.2 派生类成员标识
8.8.2.1. 图示(表格/图)
8.8.3 类的作用域运算符
8.8.3.1 隐藏(Shadow)
如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在 这种情况下,派生类成员将 shadow(隐藏) 所有基类的同名成员。这时就需要基类名+作用 域运算符的方式才能调用基类的同名成员。
8.8.3.2 作用域运算符
class Shadow {
public:
void Foo()
{
cout << "Shadow::void Foo()" << endl;
}
};
class BS :public Shadow {
public:
void Foo()
{
cout << "BS::void Func()" << endl;
Shadow::Foo();
}
};
8.8.8.3 小结
重载:同一作用域,函数同名不同参(个数,类型,顺序)
隐藏:父子类中,标识符(函数,变量)相同,无关乎返回值和参数(函数),或声明类型(变量)。
8.9 why public
所有继承必须是 public 的,如果想私有继承的话,应该采用将基类实例作为成员的方式作为替代。(引自 Google C++ 编程规范)。
8.9.2 继承测试
实际中,我们一般在 public 里放入我们的接口,protected 里一般放入我们的数据(类内部可见,内外部依然不可见),private 里一般放入绝对不能让其他任何人知道的东西。
在public继承中,所有的性质都被很好的继承下来了,这就是为什么大多数只用 public 继承的原因。
8.9.3 结论
继承方式定论
- public: 传承接口 间接的传承了数据(protected)。
- protected: 传承数据,间接封杀了对外接口(public)。
- private: 统杀了数据和接口。
综上所述,记住public继承足矣。
使用三方库,往往遵循这样一个原则,先继承,后添加新元素,体现子类的个性需求。
8.10 多重继承 (Multiple Inheritance)
从继承类别上分,继承可分为单继承和多继承,前面讲的都是单继承,下面的章节主要讲多继承,即父类不止一个。
8.10.1 多继承的意义
俗话讲,鱼与熊掌不可兼得,而在计算机就可以实现,生成一种新的对象,叫熊掌鱼,多继承自鱼和熊掌即可。还比如生活中"兼"。
8.10.2 语法格式
8.10.2.1 继承语法
派生类名:public 基类名 1, public 基类名 2,..., protected 基类名 n
8.10.2.2 构造器格式
派生类名::派生类名(总参列表) :基类名 1(参数表 1),基类名(参数名 2)....基类名 n(参数名 n),
内嵌子对象 1(参数表 1),内嵌子对象 2(参数表 2)...内嵌子对象 n(参数表 n)
{
派生类新增成员的初始化语句;
}
8.10.3 实战-沙发床
8.10.3.1 继承结构
8.10.3.2 沙发床实现
class Sofa {
public:
void Sit()
{
cout << "take a sit... ";
}
};
class Bed {
public:
void Sleep()
{
cout << "take a sleep Zzz ";
}
};
class SofaBed :public Bed, public Sofa {
public:
void Func()
{
cout << "Sofa: ";
Sit();
Sleep();
}
};
int main(int argc, char** argv)
{
Sofa sf;
sf.Sit();
putchar(10);
Bed bd;
bd.Sleep();
putchar(10);
SofaBed sb;
sb.Func();
putchar(10);
system("pause");
return 0;
}
8.10.4 三角问题
多个父类中重名的成员,继承到子类中后,为了避免冲突,携带了各父类的作用域信 息,子类中要访问继承下来的重名成员,则会产生二义性,为了避免冲突,访问时需要提供父类的作用域信息。
class X {
public:
X(int x = 0):_data(x){
cout << "X()" << endl;
}
protected:
int _data;
};
class Y {
public:
Y(int y = 0):_data(y)
{
cout << "Y()" << endl;
}
protected:
int _data;
};
class Z :public X, public Y {
public:
Z():X(1),Y(2)
{
}
void Dis()
{
cout << X::_data << endl;
cout << Y::_data << endl;
}
};
8.10.5 钻石问题
三角关系中需要解决的问题有两类:
- 数据冗余问题,
- 访问不方便的问题。
解决方案,是三角转四角的问题。具体操作:
- 提取公共成员构成祖父类,即虚基类,
- 各父类虚继承虚基类。
虚继承是继承的一种拓展,在继承上加上修饰:virtual。
//钻石问题
class Data {
protected:
int _data;
};
class X: virtual public Data {
public:
X(int x = 0){
_data = x;
cout << "X()" << endl;
}
void SetData(int data)
{
_data = data;
}
};
class Y: virtual public Data {
public:
Y(int y = 0)
{
_data = y;
cout << "Y()" << endl;
}
int GetData()
{
return _data;
}
};
class Z :public X, public Y {
public:
Z():X(1),Y(2)
{
}
void Dis()
{
cout << _data << endl;
}
};
8.10.5.1 虚基类-虚继承
//虚基类和虚继承
class VA {
protected:
int _data;
};
class VB : virtual public VA {
public:
VB(int data = 0)
{
_data = data;
cout << "VB" << endl;
}
};
class VC : virtual public VA {
public:
VC(int data = 0)
{
_data = data;
cout << "VC" << endl;
}
};
class VD :public VB, public VC {
public:
VD(int d)
{
cout << "VD" << endl;
_data = d;
}
void Dis()
{
cout << _data << endl;
}
};
8.10.6 语法小结
8.10.6.1 虚继承的意义
在多继承中,保存共同基类的多份同名成员,虽然有时是必要的,可以在不同的数据成员中分别存放不同的数据,但在大多数情况下,是我们不希望出现的。 因为保留多份数据成员的拷贝,不仅占有较多的存储空间,还增加了访问的困难。为 此,C++提供了,虚基类和虚继承机制,实现了在多继承中只保留一份共同成员。
8.10.6.2 虚基类
经提取,存有公共元素的,被虚继承的祖父类,称为虚基类。虚基类,需要设计和抽象,虚继承,是一种继承的扩展。
8.10.6.3 虚继承
class 派生类名: vitrual 继承方式 虚基类
8.10.6.4 初始化顺序
先祖父类,父类(从左向右),子类,仅最后一次初始化有效。
8.10.7 改造沙发床
假设,沙发和床,分别有颜色_color 和重量_weight,还有一个描述行为 descript(), 该如何改造我们的沙发床呢?
class Funiture {
public:
void Description()
{
cout << "_weight " << _weight << endl;
cout << "_color " << _color << endl;
}
protected:
float _weight;
int _color;
};
class Sofa : virtual public Funiture {
public:
Sofa(float w = 0, int c = 0)
{
_weight = w;
_color = c;
}
void Sit()
{
cout << "take a sit... ";
}
};
class Bed : virtual public Funiture {
public:
Bed(float w = 0, int c = 0)
{
_weight = w;
_color = c;
}
void Sleep()
{
cout << "take a sleep Zzz ";
}
};
class SofaBed :public Bed, public Sofa {
public:
SofaBed(float w = 0, int c = 0)
{
_weight = w;
_color = c;
}
void Rest()
{
Sit();
Sleep();
}
};
第九章 多态 (Polymorphism)
9.1 什么是多态
9.1.1 生活中的多态
如果有几个相似而不完全相同的对象,有时人们要求在向它们发出同一个消息时,它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。
例如,甲乙丙 3 个班都是高二年级,他们有基本相同的属性和行为,在同时听到上课铃声的时候,他们会分别走向 3 个不同的教室,而不会走向同一个教室。
同样,如果有两支军队,当在战场上听到同种号声,由于事先约定不同,A 军队可能实施进攻,而 B 军队可能准备 kalaok。
又如在 winows 环境下,用鼠标双击一个对象(这就是向对象传递一个消息),如果对象是一个可执行文件,则会执行此程序,如果对象是一个文本文件,则会启动文本编辑器并打开该文件。
9.1.2 C++中的多态
C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。
比如,mspaint 中的单击不同图形,执行同一个拖动动作而绘制不同的图形,就是典型的多态应用。
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统升级,维护,调试的工作量和复杂度。
9.2 赋值兼容(Assign Compatible)
9.2.1 赋值兼容规则
赋值兼容规则是指,在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
只有在公有派生类中才有赋值兼容,赋值兼容是一种默认行为,不需要任何的显示的转化步骤。
赋值兼容细化:
- 派生类的对象可以赋值给基类对象。
- 派生类的对象可以初始化基类的引用。
- 派生类对象的地址可以赋给指向基类的指针。
9.2.2 实现
class Shape {
public:
Shape(int x = 0, int y = 0) :_x(x), _y(y)
{
}
void Draw()
{
cout << "Draw Shape From (" << _x << ", " << _y << ")" << endl;
}
protected:
int _x;
int _y;
};
class Rect : public Shape {
};
class Circle :public Shape {
public:
Circle(int x = 0, int y = 0, int r = 0)
:Shape(x,y),_radius(r)
{
}
void Draw()
{
cout << "Draw Circle From (" << _x << ", " << _y << ") " <<
"radius " << _radius << endl;
}
protected:
int _radius;
};
int main(int argc, char** argv)
{
Shape s(1, 2);
s.Draw();
Circle c(4, 5, 6);
//子类对象地址赋值给父类指针
Shape* ps = &c; // 赋值兼容
ps->Draw();
system("pause");
return 0;
}
9.2.3 安全转化
赋值兼容是安全的。
子类对象初始化的时候,调用父类的构造器,也是发生了赋值兼容的问题。除此之外,其它任何父类成员调用,均是如此。
大范围的指针向小范围的指针转化是安全的。反之,不亦然。
父类也可以通过强转的方式转化为子类。 父类对象强转为子类对象后,访问从父类继承下来的部分是可以的,但访问子类的部分,则会发生越界的风险,越界的结果是未知的,也应当是避免的。
9.3 多态形成的条件 (ConditionsofPolyMorphism)
9.3.1 静多态
静多态,也就是我们说的函数重载。表面来看,是由重载规则来限定的,内部实现却是Namemangling(命名倾轧)。
此种行为,发生在编译期,故称为静多态。
9.3.2 多态
(动)多态,不是在编译器阶段决定,而是在运行阶段决定,故称为动多态。动多态行成的条件如下:
- 父类中有虚函数,即共用接口(加 virtual)。
- 子类 override(覆写)父类中的虚函数(子类中同名同参同返回)。
- 通过己被子类对象赋值的父类指针,调用共用接口。
virtual 是声明型关键字。放在声明,而不是实现,在多文件编程中得注意。虚继承与虚函数毫无关系,前者为了避免多个父类中出现多个同名的函数或成员,后者是为了实现多态。子类覆写了得函数,也是虚函数。
9.3.3 虚函数 virtual function
用于实现多态
9.3.3.1 格式
class 类名
{
virtual 函数声明
}
9.3.3.2 多态三要素实践
class Shape {
public:
Shape(int x = 0, int y = 0) :_x(x), _y(y)
{
}
//虚函数
virtual void Draw()
{
cout << "Draw Shape From (" << _x << ", " << _y << ")" << endl;
}
protected:
int _x;
int _y;
};
class Circle :public Shape {
public:
Circle(int x = 0, int y = 0, int r = 0)
:Shape(x,y),_radius(r)
{
}
//覆写父类虚函数
virtual void Draw()
{
cout << "Draw Circle From (" << _x << ", " << _y << ") " <<
"radius " << _radius << endl;
}
protected:
int _radius;
};
class Rect :public Shape {
public:
Rect(int x = 0, int y = 0, int w = 0, int l = 0)
:Shape(x,y), _width(w), _length(l)
{
}
//覆写父类虚函数
virtual void Draw()
{
cout << "Draw Rect From (" << _x << ", " << _y << ") " <<
"Width " << _width << ", Length " << _length << endl;
}
protected:
int _width;
int _length;
};
int main(int argc, char** argv)
{
Shape s(1, 2);
Circle c(4, 5, 6);
Rect r(7, 8, 9, 10);
Shape* ps = nullptr;
while (1)
{
int choice;
cin >> choice;
switch (choice)
{
case 1:
ps = &c;父类指针被子类对象地址赋值
break;
case 2:
ps = &r;
break;
default:
cout << "无此选项" << endl;
continue;
}
ps->Draw();
}
system("pause");
return 0;
}
9.3.3.3 覆写关键字override(C++11)
override是C++11 中引入的关键字,用于修饰覆写的虚函数。表明父类中有此虚函数,同名,同参,同返回中有一样不满足,则会报错。
这样的好处是,假设在覆写中,写错了,比如,作不到同名,同参,同返中的任一样,编译器都会给出提示。
virtual void Draw() override
{
cout << "Draw Rect From (" << _x << ", " << _y << ") " <<
"Width " << _width << ", Length " << _length << endl;
}
9.3.3.4 虚函数在多继承中
TODO
9.3.3.5 虚函数小结
- virtual 是声明虚函数的关键字,它是一个声明型关键字。
- override 构成的条件,发生在父子类的继承关系中 同名,同参,同返回。
- 虚函数在派生类中仍然为虚函数,若发生覆写,最好显示的标注 virtual。 若无覆写,仍然为虚函数。
- 子类中的覆写的函数,可以为任意访问类型,依子类需求决定。
9.4.3 纯虚函数 (Pure virtual function)
- 纯虚函数,是一种虚函数,virutal 修饰,没有实现体,且被"初始化"为零的函数。
- 纯虚函数存在的意义在于,提供族类接口使用,通常被我们高度抽象出来的 纯接口类 ,就会使用纯虚函数。
- 含有纯虚函数的类,称为抽象基类(Abstract Base Class)。
- 抽象基类都不能实例化。
- 如果一个类中声明了纯虚函数,而在派生类中没有该函数的定义,则该虚函数在派生类中仍然为纯虚函数,派生类仍然为纯虚基类。
9.3.4.1. 格式
class 类名
{
virtual 函数声明 = 0;
}
9.3.4 虚析构 (Virtual Destructor)
含有虚函数的类,析构函数也应该声明为虚函数,称为虚析构函数。在 delete 父类指针,指向的堆上子类对象时,会调用子类的析构函数,实现完整析构。
9.4 多态案例分析
9.4.1 为什么虚析构 virtual destructor
动物园里的好声音。对比栈对象和堆对象在多态中销毁的不同。
代码详见: https://github.com/nzhsoft/cpp_course
为了实现完美析构,但凡在类中有虚函数或者纯虚函数的,将其析构函数也置为virtual。
9.4.2 设计模式之依赖倒置原则
设计模式中有一种很重要的原则之一,叫依赖倒置(Dependency Inversion Principle), 简称 DIP。也是基于多态的。
定义 | 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。 |
---|---|
问题由来 | 类 A 直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类 A 的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻 辑;类 B 和类 C 是低层模块,负责基本的原子操作;假如修改类 A,会给程序带来不必要的风险。 |
解决方案 | 将类 A 修改为依赖接口 I,类 B 和类 C 各自实现接口 I,类 A 通过接口 I 间接与类 B 或者类 C 发生联系,则会大大降低修改类 A 的几率。 |
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在 C++中,抽象 指的是抽象类(C++中称为接口),细节就是具体的实现类,使用接口或者抽象类的目的是制 定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完 成。
传统的过程式设计倾向于使高层次的模块依赖于低层次的模块(自顶向下,逐步细 化),而依据 DIP的设计原则,将中间层抽象为抽象层,让高层模块和低层模块依赖于中 间抽象层。
依赖倒置原则的核心思想是面向接口编程,我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。
首先,说说一些学术词:
- 什么是依赖:在A中使用了B,就叫做A依赖于B。
- 强耦合:依赖再高深一点就是耦合,强耦合就是当B发生改变,A也必须发生改变。
如下:
class Book {
public:
string GetContents()
{
return "从前有座山,山里有座庙,"
"庙里有个老和尚,老和尚在给小和尚讲故事";
}
};
class NewsPaper {
string GetContents()
{
return "某国于今早发生暴动";
}
};
class Mother {
public:
//低耦合
//Mom使用了Book,Mom依赖于Book(耦合)
void TellStory(Book& b)
{
cout << b.GetContents() << endl;
}
};
Mother 依赖于 Book,回头Mother想读报纸了,TellStory里需要改成报纸类型的引用。这就是强耦合。Mother 高度依赖于 Book 和 NewsPaper。
此时我们需要降低这种强耦合关系,所以我们会需要一个中间层,让Mother,Book 以及 NewPaper 分别依赖于这个中间层,从而让Mother和这两个读物间接发生关系。
//此处为中间层
class IReader {
public:
virtual string GetContents() = 0;
};//实现中间层接口
class Book:public IReader {
public:
virtual string GetContents()
{
return "从前有座山,山里有座庙,"
"庙里有个老和尚,老和尚在给小和尚讲故事";
}
};
//实现中间层接口
class NewsPaper: public IReader {
public:
virtual string GetContents()
{
return "某国于今早发生暴动";
}
};
//依赖中间层
class Mother {
public:
//低耦合
//Mom使用了Book,Mom依赖于Book(耦合)
void TellStory(IReader* pi)
{
cout << pi->GetContents() << endl;
}
};
现在,如果妈妈想读电子书了,这事就不需要再修改Mother,NewPaper,或者Book,直接添加Ebook即可:
class IReader {
public:
virtual string GetContents() = 0;
};
class Book :public IReader {
public:
virtual string GetContents()
{
return "从前有座山,山里有座庙,"
"庙里有个老和尚,老和尚在给小和尚讲故事";
}
};
class NewsPaper :public IReader {
public:
virtual string GetContents()
{
return "某国于今早发生暴动";
}
};
//直接添加Ebook,无需修改其他
class Ebook :public IReader {
public:
virtual string GetContents()
{
return "凡人修仙传-韩立修成金丹";
}
};
class Mother {
public:
//低耦合
//Mom使用了Book,Mom依赖于Book(耦合)
void TellStory(IReader* pi)
{
cout << pi->GetContents() << endl;
}
};
9.4.3 公司管理系统设计
9.4.3.1 需求
某小型公司,主要有四类人员:经理、技术人员、推销员和销售经理。现在,需要存储这些人员的姓名、编号、级别、当月薪水。计算月薪总额并显示全部信息。
人员编号基数为 1000 ,每输入一个人员信息编号顺序加 1 。
程序要有对所有人员提升级别的功能。本例中为简单起见,所有人员的初始级别均为 1 级。然后进行升级,经理升为 4 级,技术人员和销售经理升为 3 级,推销员仍为 1 级。
月薪计算办法是: 经理拿固定月薪 8000 元;技术人员按每小时 100 元领取月薪; 推销员的月薪按该推销员当月销售额的 4% 提成;销售经理既拿固定月薪也领取销售提成, 固定月薪为 5000 元,销售提成为所管辖部门当月销售总额的 5%。
9.4.3.2 类架构设计
9.4.3.3 基类详细设计
9.5 RTTI 运行时类型信息
9.5.1 语义
RTTI(run time type identificaiton),允许程序员在运行时进行对象信息识别。typeid / dynamic_cast 是C++ 运行时类型信息重要组成部分。 运行时信息,来自于多态,所以,以下运算符只用于基于多态的继承体系中。
9.5.2 typeid
运算符 typeid ,返回包含操作数数据类型信息的 type_info 对象的一个引用,信息中包括数据类型的名称,要使用 typeid,程序中需要包含头文件 <typeinfo>。
其中 type_info 重载了操作符 == 、!= 分别用来比较是否相等、不等,函数 name() 返回类型名称。type_info 的拷贝和赋值均是私有的,故不可拷贝和赋值。
常用于返回检查,调试之用。
9.5.2.1 基础类型应用
//基本类型
cout<<typeid(char).name()<<endl;
cout<<typeid(short).name()<<endl;
cout<<typeid(int).name()<<endl;
cout<<typeid(long long).name()<<endl;
cout<<typeid(float).name()<<endl;
cout<<typeid(double).name()<<endl;
putchar(10);
//函数类型
void (*pf)(int);
cout<<typeid(pf).name()<<endl;
putchar(10);
//指针类型
cout<<typeid(int*).name()<<endl;
cout<<typeid(const int*).name()<<endl;
cout<<typeid(int* const).name()<<endl;
9.5.2.2 多态体系应用
引用和解引用的指针,在多态体系中,均可以通过 typeid 获得真实有效的信息。注意在多态体系中,要想应用typeid,必须虚析构。
9.5.2.3 小结
- 确保基类定义了至少一个虚函数(虚析构也算)。
- 不要将typeid作用于指针,应该作用于引用,或解引用的指针。
- typeid是一个运算符,而不是函数。
- typeid 运算符返回的 type_info 类型,其拷贝构造函数和赋值运算函数都声明为 private 了,这意味着其不能用于stl容器,所以我们一般不能不直接保存 type_info 信息,而保存 type_info 的name信息
9.5.2.4 type(*) 和 type(&)
**Notice how the type that typeid considers for pointers is the pointer type itself (both a and b are of type class Base *). However, when typeid is applied to objects (like a and b) typeid yields their dynamic type (i.e. the type of their most derived complete object).
当 typeid 作用于指针,得到的是指针类型本身。但是当 typeid 作用于对象时,得到的是动态类型。
If the type typeid evaluates is a pointer preceded by the dereference operator (*), and thispointer has anull value,typeidthrows a bad_typeid exception
如果 typeid作用于对象,且指针为空,则会抛出 bad_typeid异常。
9.5.3 dynamic_cast;
赋值兼容层面的语义,即 upcast(上转),不需要显示的转化。那么存在dynamic_cast 主要意义,就是在于downcast(下转)。
注意在多态体系中,要想应用 dynamic_cast,必须虚析构。
9.5.3.1 语法解析
static_cast | 1. 在一个方向上可以作隐式转换的,在另外一个方向上可以作静态。 2.发生在编译阶段,不保证后序使用的正确性(对于类类型指针)。3. 应用于对象时,需要转化函数的支持。 |
---|---|
reinterpret_cast | 应用于指针、引用、函数指针或者成员指针,算术类型。 二进制层面的重新解释。 |
dynamic_cast | 应用于类的指针、类的引用或者 void*。 dynamic_cast 运算符可以在执行期决定真正的类型。 1. 如果 downcast 是安全的(也就说,如果基类指针或者引用确实 指向一个派生类对象)这个运算符会传回适当转型过的指针。 2. 如果 downcast 不安全,这个运算符会传回空指针(也就是说, 基类指针或者引用没有指向一个派生类对象)。 3. dynamic_cast 主要用于类层次间的上行转换和下行转换,还可 以用于类之间的交叉转换。 |
9.5.3.2 dynamic_cast 之 Downcast
基类指针下转时,如果发现指向的不是一个发生在多态中的子类对象,则会返回NULL指针以后判断使用。
9.6 多态实现原理剖析
C++的多态是通过一张虚函数表(Virtual Table)来实现的,简称为 V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆写的问题,保证其真实反 应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。 C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
如下:
class Base {
public:
virtual void f()
{
cout << "Base::f" << endl;
}
virtual void g()
{
cout << "Base::g" << endl; } virtual void h() {
cout << "Base::h" << endl;
}
};
虚函数地址表如下:
第十章 模板 (Template)
10.1 模板语义
泛型(Generic Programming),即是指具有在多种数据类型上皆可操作的含意。泛型编程的代表作品 STL 是一种高效、泛型、可交互操作的软件组件。
泛型编程最初诞生于 C++中,目的是为了实现 C++的 STL(标准模板库)。其语言支持机制就是模板(Templates)。
模板的精神其实很简单:类型参数化(type parameterized),即,类型也是一种参数, 也是一种静多态。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉, 抽出来做成模板参数 T。
10.2 函数模板 (Func Template)
10.2.1 重载泛型
重载函数,固然实现了泛化的一些特性,但是不彻底,且有**二义性(ambiguous)**的存在。
如下,示例中 long 类型可以隐式的转化为int 或是 double,编译会出现二义性,导致 编译失败。
void MySwap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}
void MySwap(double& a, double& b)
{
double t = a;
a = b;
b = t;
}
int main(int argc, char** argv)
{
long a = 2; long b = 3;
MySwap(a, b);
system("pause");
return 0;
}
不使用模板的情况下,最好的办法就是再写一个针对 long 的重载
10.2.2 函数模板
10.2.2.1 语法
//在一个函数的参数表,返回类型和函数体中使用参数化的类型。
template<typename/class 类型参数 T1,typename/class 类型参数 T2,...>
返回类型 函数模板名(函数参数列表)
{
函数模板定义体
}
10.2.2.2 模板泛化
template<typename T>
既可以与函数同行,也可以将函数另起一行来书写。T即为范化的类型。
其过程,相当于经历了两次编译,先依据实参类型,实例化函数,然后再将实例化的 函数,参与编译。
template<typename T>
void MySwap(T& a, T& b)
{
T t = a;
a = b;
b = t;
}
int main(int argc, char** argv)
{
long a = 2; long b = 3;
MySwap(a, b);
cout << a << " " << b << endl;
string s1("China"), s2("USA");
MySwap(s1, s2);
cout << s1 << " " << s2 << endl;
system("pause");
return 0;
}
以上的大致过程,我们拿 int 来举例:
函数模板 | 实例化 | 模板函数 | 模板函数的调用 | |
---|---|---|---|---|
MySwap |
-> | MySwap<int> |
-> | MySwap<int>(1,2) |
10.2.2.3 模板参数作返回类型
TODO
10.2.2.4 特性小结
- 先实例化,再调用。
- 严格匹配,不存在隐式转化。
- 尺有所短,寸有所长
int i(1); double j(1.1);
MySwap(i,j)
这就是模板的短板所在啦,这样是不行的。
10.2.2.5 原理
编译器并不是把函数模板处理成能够处理任意类的函数。比如,自定义类型,如何处 理呢?
编译器遇到模板方法定义时,会进行语法检查,但是并不编译模板。编译器无法编译 模板定义,因为它不知道使用什么类型。比如 T a,b; 在 不知晓 T 具体类型时,是无法分配内存,更无从谈编译a = b;
T 获取类型的过程,称为模板的实例化,函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译(类型检查),在调用的地方对参数替换后的代码进行编译(代码生成)。
10.2.3 函数模板的应用
10.2.3.1 算法抽象
数据类型的泛化,使得模板特别适合于作算法的抽象。STL中的algorithm 就是典型的 代表。比如,算法 sort 就是支持类型泛化的代表排序算法。
10.2.3.2 void quickSort(T* array, intl eft, int right)
将快速排序算法实现模板化。此时,无论转入的是 int的还是 float的类型,均是可以处理。相同逻辑的函数没有必要写两遍。
template <typename T>
void QuickSort(T* p, int low, int high)
{
if (low < high)
{
int l = low;
int h = high;
T pivot = p[l];
while (l < h)
{
while (p[h] >= pivot && l < h)
{
h--;
}
p[l] = p[h];
while (p[l] <= pivot && l < h)
{
l++;
}
p[h] = p[l];
}
p[l] = pivot;
QuickSort(p, low, l - 1);
QuickSort(p, l + 1, high);
}
}
10.2.4 函数模板默认参数
重在理解,类型参数化,此时的默认参数,不再是一数值,就是类型。
函数模板,在调用时,先实例化为模板函数,然后再调用。当然也可以设置默认类型 的默认值。由于系统强大的自动推导能力,有时默认也没有太大的意义。
template<typename T = int>
void quickSort(T * array,int left, int right)
10.2.5 函数模板的特化
Template Specialization,函数模板的特化,即函数模板的特性情况,个例行为。
就是在实例化模板时,对特定类型的实参进行特殊处理,即实例化一个特殊的实例版
本。
template<typename T> int compare( T &a, T &b)
template<> int compare <const char*>( const char* &a, const char* &b)
当以特化定义时的形参使用模板时,将调用特化版本,模板特化分为全特化和偏特化, 函数模板的特化,只能全特化;
比如,我们在比较两个数的大小时:
template<typename T>
int Compare1(T a, T b)
{
if (a > b)
return 1;
else if (a < b)
return -1;
else
return 0;
}
template<> int Compare1<char*>(char* a, char* b)
{
cout << "特化" << endl;
return strcmp(a, b);
}
10.2.6.函数模板适用场景
函数模板,只适用于函数的参数个数相同而类型不同,且函数体相同的情况。如果个数不同,则不能用函数模板。
10.3 类模板 (Class Template)
10.3.1 Stack 类
Stack 类模板化,可以 push 和 pop 不同的数据类型。主要由几个因素需要把控。栈中的空间元素类型,压入元素类型,弹出元素类型,三者保持一致即可。
template<typename T>
class Stack {
public:
Stack(int size = 1024){
space = new T[size];
top = 0;
}
~Stack()
{
delete[]space;
}
bool isEmpty()
{
return top == 0;
}
bool isFull()
{
return top == 1024;
}
void push(T data)
{
space[top++] = data;
}
T pop()
{
return space[--top];
}
private:
T* space;
int top;
};
10.3.2 类模板
10.3.2.1 格式
template<typename T> class ClassName
{
void func(T );
};
template<typename T> void ClassName<T>::func(T)
{
}
10.3.2.2 应用
ClassName<int> cn; //类模板->模板类->类对象
十一章 IO 流 (IO Stream)
目前为止,cin 和 cout 充当了 scanf 和 printf 的功能。但他们并不是函数。而是类 对象,那么我们就有必要了解一下,他们是哪些类的对像。
11.1 IO class
11.1.1 Hierachy
11.1.2 流类特性
11.1.2.1 不可赋值和复制
11.1.2.2 缓冲
以下情况会导致刷缓存:
- 程序正常结束,作为 main 函数结束的一部分,将清空所有缓冲区。
- 缓冲区满,则会刷缓冲。
- endl,flush 也会刷缓冲。
11.1.2.3 重载了<< >>
11.2 istream status
11.2.1 状态位的意义
大部分情况下,我们可能并不关心这些标志状态位,比如我们以前用到的 cin/cout。 但是在循环读写中,这些标志位确实大用用途。比如,用于判断文件结束标志的位。
11.2.2 状态位操作函数
11.3 cout
11.3.1 格式输出
11.3.2 成员函数
11.4 cin
11.4.1 >>
TODO
十二章 异常 (Exception)
12.1 Error
Error 译为错误,常见的 Error 有编译时 Error 和 运行时 Error。运行时的 Error 又分 为不可预料的逻辑错误和可以预料的运行异常。
12.2 C Error
C语言中错误的处理,通常采用返回值的方式或是置位全局变量的方式。
这就存在两个问题。如果返回值正是我们需要的数据,且返回数据同出错数据容错性不高。全局变量, 在多线程中易引发竞争。而且,当错误发生时,上级函数要出错处理,层层上报,造成过多的出错处理代码,且传递的效率低下。
C++ 通过异常实现了返回与错误的处理的分离。
分析:
- 把可能发生异常的语句放在 try 语句声当中。try 不影响原有语句的执行流程。
- 若未发生异常,catch 子语句并不起作用。程序会流转到 catch 子句的后面执 行。
- 若 try 块中发生异常,则通过 throw 抛出异常。throw 抛出异常后,程序立即离开本函数,转到上一级函数。
- throw抛出数据,类型不限。既可以是基本数据类型,也可以是构造数据类型。
- 程序流转到 main 函数以后, try语句块中抛出进行匹配。匹配成功,执行catch 语句,catch语句执行完毕后。继续执行后面的语句。
- 如无匹配,系统调用 terminate终止程序。
12.3 Exception definition
12.3.1 语法格式
try{
被检查可能抛出异常的语句
}
catch(异常信息类型)[变量名]{
进行异常处理的语句
}
12.3.2 使用条例
- 被检语句必须放在 try块中,否则不起作用。
- try catch 中花括号不可省。
- 一个 try-catch 结构中,只能有一个 try 块,catch块却可以有多个。以便与不同的类型信息匹配。
- throw 抛出的类型,既可以是系统预定义的标准类型也可以是自定义类型。从抛出到catch是一次复制拷贝的过程。如果有自定义类型,要考虑自定义类型的拷贝问题。
- 异常匹配,不作类型转化。如果catch语句没有匹配异常类型信息,就可以用(…) 表示可以捕获任何异常类型的信息。
- try-catch结构可以与throw 在同一个函数中,也可以不在同一个函数中, throw 抛出异常后,先在本函数内寻找与之匹配的 catch块,找不到与之匹配的就转到上一层,如果上一层也没有,则转到更上一层的catch块。如果最终找不到与之匹配的catch块,系统则会调有系统函数 terminate使程序终止。
12.3.3 抛出异常声明
- 一个不抛弃任何异常的函数可以声明为:
void Func() throw();
- 如果在函数声明中没有包含异常接口声明,则函数可以抛掷任何类型的异常,例如:
void func();
- 为了加强程序的可读性,可以在函数声明中列出可能抛出的所有异常类型。 例如:
void func() throw (A, B, C , D);
- 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,该函数默认行为调 用terminate 函数中止程序。
12.4 栈自旋
12.4.1 unwinding
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象, 都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋(unwinding)。
12.4.2 RAII in Exception
而堆上的空间,则会泄漏。利用遵循 RAII思想的智能指针来解决。