第 15 章 友元、异常和其它
15.1. 友元
友元函数用于类的扩展接口 中,类中不仅可以使用友元函数,也可以将类作为友元。
友元类的所有方法都可以访问原始类的私有成员
和保护成员
。
在一个类中将另一个类声明为友元。
15.1.1 友元类
什么时候希望一个类成为另一个类的友元呢?
举个例子:模拟电视机和遥控器的简单程序。要知道遥控器并非电视机,故公有继承is-a关系并不适用。遥控器也非电视机的一部分,因此包含或者私有继承和保护继承的has-a关系也不适用。事实上,遥控器可以改变电视机的状态,这就表明遥控器类应当作为电视类的一个友元。
按照如上描述,决定编写一个模拟电视机和遥控器的简单程序。
首先定义Tv类,可以用一组状态成员(描述电视各个方面的变量)来表示电视机。下面是一些可能的状态:
- 开/关;
- 频道设置;
- 音量设置;
- 有线电视或天线调节模式
- TV调谐或A/V输入
ps:当前的电视机都将控件藏在面板之后(换台只能逐频道换),遥控器的控制能力必须得与电视机内置的控制功能相同,它的大部分方法是根据Tv内置的方法来进行改造的,就比如刚刚所说的利用电视机控制面板切台只能逐个频道切换,遥控器在此基础上进行改造,使其能够按个人意愿进行切台。
为什么说遥控器大部分方法是根据Tv方法进行改造的呢?因为遥控器本身还有其他一些自己单独的功能,就好比如切换工作模式,一种拿来控制电视机、一种拿来控制DVD。
下面语句使得Remote成为友元类
friend class Remote;
友元声明可以位于公有、私有或者保护部分,其所在的位置无关紧要。
遥控器是后来者,所以编译器必须了解电视(Tv)类后,才能处理遥控器(Remote)类。
下面开始模拟遥控器操控电视机,或者电视机自己操控自己:
1、头文件
//
// Created by e on 2022/10/18.
//
#ifndef TEST_TV_H
#define TEST_TV_H
class Tv {
public:
friend class Remote; // Remote can access Tv private parts
enum {
Off, On
};
enum {
MinVal, MaxVal = 20
};
enum {
Antenna, Cable
}; // Antenna->天线 Cable->有线
enum {
TV, DVD
};
Tv(int s = Off, int mc = 125) : state(s), volume(5),
maxChannel(mc), curChannel(2), mode(Cable), input(TV) { }
void onOff() {
state = (state == On) ? Off : On;
}
bool isOn() const {
return state == On;
}
bool volUp(); //声音提高
bool volDown(); //声音降低
void channelUp(); //频道往后调
void channelDown(); //频道往前调
void setMode() {
mode = (mode == Antenna) ? Cable : Antenna;
}
void setInput() {
input = (input == TV) ? DVD : TV;
}
void showSettings() const; //display all TV settings.
private:
int state; //on or off
int volume; //assumed to be digitized-->假设是用数字大小代表音量大小
int maxChannel; //maximum number of channels
int curChannel; //current channel setting
int input; //TV or DVD
int mode; //broadcast or cable
};
class Remote {
private:
int mode; //controls TV or DVD
public:
Remote(int m = Tv::TV) : mode(m) { }
bool volUp(Tv &t) { return t.volUp(); }
bool volDown(Tv &t) { return t.volDown(); }
void onOff(Tv &t) { t.onOff(); }
void channelUp(Tv &t) { t.channelUp(); }
void channelDown(Tv &t) { t.channelDown(); }
void setChannel(Tv &t, int c) { t.curChannel = c; }
void setMode(Tv &t) { t.setMode(); }
void setInput(Tv &t) { t.setInput(); }
};
#endif //TEST_TV_H
2、源文件
//
// Created by e on 2022/10/18.
//
#include <iostream>
#include "tv.h"
bool Tv::volUp() {
if (volume < MaxVal){
volume++;
return true;
}else {
return false;
}
}
bool Tv::volDown() {
if (volume > MinVal){
volume--;
return true;
}else{
return false;
}
}
void Tv::channelUp() {
if (curChannel < maxChannel){
curChannel++;
}else{
curChannel = 1;
}
}
void Tv::channelDown() {
if (curChannel > 1){
curChannel--;
}else{
curChannel = maxChannel;
}
}
void Tv::showSettings() const {
using std::cout;
using std::endl;
cout << "TV is " << (state == On ? "On" : "Off") << endl;
if (state == On){
cout << "Volume setting = " << volume << endl;
cout << "Channel setting = " << curChannel << endl;
cout << "Input settng = " << (input == TV ? "TV" : "DVD") << endl;
cout << "Mode setting = " << (mode == Antenna ? "Antenna":"Cable") << endl;
}
}
3、执行文件
#include <iostream>
#include "tv.h"
int main(){
using std::cout;
Tv s42;
cout << "Initial settings for 42\" TV:\n";
s42.showSettings();
s42.onOff();
s42.channelUp();
cout << "\n Adjusted settings for 42\" TV:\n";
s42.showSettings();
Remote grey;
grey.setChannel(s42,10);
grey.volUp(s42);
grey.volUp(s42);
cout << "\n42\" settings after using remote:\n";
s42.showSettings();
Tv s58(Tv::On);
s58.setMode();
grey.setChannel(s58,28);
cout << "\n58\" settings:\n";
s58.showSettings();
return 0;
}
输出:
Initial settings for 42" TV:
TV is Off
Adjusted settings for 42" TV:
TV is On
Volume setting = 5
Channel setting = 3
Input settng = TV
Mode setting = Cable
42" settings after using remote:
TV is On
Volume setting = 7
Channel setting = 10
Input settng = TV
Mode setting = Cable
58" settings:
TV is On
Volume setting = 5
Channel setting = 28
Input settng = TV
Mode setting = Antenna
15.1.2 友元成员函数
上个例子,Remote中大多数方法都是用Tv类的公有接口实现的。这说明这些方法实际上不太需要作为友元,可以直接地说多此一举了。在Remote方法中,setChannel()是直接访问了Tv的私有成员,所以它被设置为友元是很应该的。实际上,可以让一部分的类成员成为另一个类的友元。
⚠️:如果按照上面那样做,必须小心排列各种声明和定义的顺序。
如果要将友元类
中的特定的类成员
解析作为原始类
的友元
,无需将整个类作为友元。
class Tv
{
friend void Remote::setChannel(tv & t, int c);
...
};
执行以上语句,需要编译器提前知道Remote的定义,所以将类声明为友元时,需要注意❗️:将类(TV)声明放在包含友元类(Remote)的前面。 这种方法称为 前向声明(forward declaration)
。
class Tv; // 前向声明
class Remote {...}
class Tv {...};
思考🤔:能否像以下这样排列声明与定义呢?
class Remote; // 前向声明
class Tv {...};
class Remote {...}
答案是:❌。解释✅:编译器在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和setChannel()的方法声明才行。
我们还需要注意Remote声明包含了内联代码,例如
void onOff(Tv & t){t.onOff();}
由上述的语句可以看出,Remote的成员函数调用Tv类的一个方法,所以,Remote得知道有Tv这个类,并且Tv类有这个方法才行。假如按照第一种排序的话,解决办法✅:让Remote声明仅仅包含方法声明即可,将实际的定义放在Tv类之后即可。
下面是修改后的头文件:
//
// Created by e on 2022/10/19.
//
#ifndef TEST_TVFM_H
#define TEST_TVFM_H
class Tv;
class Remote {
public:
enum {
Off, On
};
enum {
MinVal, MaxVal = 20
};
enum {
Antenna, Cable
}; // Antenna->天线 Cable->有线
enum {
TV, DVD
};
private:
int mode; //controls TV or DVD
public:
Remote(int m = Tv::TV) : mode(m) { }
bool volUp(Tv &t);
bool volDown(Tv &t);
void onOff(Tv &t);
void channelUp(Tv &t);
void channelDown(Tv &t);
void setChannel(Tv &t, int c);
void setMode(Tv &t);
void setInput(Tv &t);
};
class Tv{
public:
friend void Remote::setChannel(Tv &t, int c);
enum {
Off, On
};
enum {
MinVal, MaxVal = 20
};
enum {
Antenna, Cable
}; // Antenna->天线 Cable->有线
enum {
TV, DVD
};
Tv(int s = Off, int mc = 125) : state(s), volume(5),
maxChannel(mc), curChannel(2), mode(Cable), input(TV) { }
void onOff() {
state = (state == On) ? Off : On;
}
bool isOn() const {
return state == On;
}
bool volUp(); //声音提高
bool volDown(); //声音降低
void channelUp(); //频道往后调
void channelDown(); //频道往前调
void setMode() {
mode = (mode == Antenna) ? Cable : Antenna;
}
void setInput() {
input = (input == TV) ? DVD : TV;
}
void showSettings() const; //display all TV settings.
private:
int state; //on or off
int volume; //assumed to be digitized-->假设是用数字大小代表音量大小
int maxChannel; //maximum number of channels
int curChannel; //current channel setting
int input; //TV or DVD
int mode; //broadcast or cable
};
//Remote methods as inline functions
inline bool Remote::volUp(Tv &t) {return t.volUp();}
inline bool Remote::volDown(Tv &t) {return t.volDown();}
inline void Remote::onOff(Tv &t) {t.onOff(); }
inline void Remote::channelUp(Tv &t) {t.channelUp(); }
inline void Remote::channelDown(Tv &t) {t.channelDown(); }
inline void Remote::setMode(Tv &t) {t.setInput(); }
inline void Remote::setChannel(Tv &t, int c) { t.curChannel = c; }
#endif //TEST_TVFM_H
下图说明修改前与修改后头文件的区别:
内联
函数的链接性是内部
的,意味着函数定义必须在使用函数的文件
中。也可将定义放在实现文件
中,但必须删除 关键字inline
,此时的链接性是外部
的。
15.1.3 其它友元关系
随着科技的发达,出现了交互式的遥控器,这启发我们可以让两个类成为彼此的友元。
注意⚠️:对于使用Remote对象的Tv方法,其原型可在Remote类声明之前声明,但必须在Remote类声明之后定义(原因:Tv类在Remote类声明前就声明了,此时Tv类通过friend关键字知道Remote是一个类,但是不知道Remote中有啥成员函数或者方法,所以对于使用Remote对象的Tv方法来说,只能等到Remote类声明完成之后再进行定义,这样可以保证编译器有足够的信息进行该方法的编译)
class Tv
{
friend class Remote;
public:
void buzz(Remote & r);
...
};
class Remote
{
friend class Tv;
public:
void Bool volup(Tv & t) {t.volup();}
};
inline void Tv::buzz(Remote & r)
{
...
};
15.1.4 共同的友元
还有一种需要使用友元的情况:**函数需要访问两个类的私有数据。**首先想到的是,把这个函数设定为这两个类的成员函数,但这是不切实际的。我们可以将这个函数设置为这两个类的友元。
假定有一个Probe类和一个Analyzer类,前者表示某种可编程的测量设备,后者表示某种可编程的分析设备。两者均有内部时钟,且希望它们能够同步。
15.2 嵌套类
在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为 嵌套类(nested class)
。
包含类(包含其他类的类)的成员函数可以创建和使用被嵌套类的对象;当类声明位于公有部分时,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。
✅理解:
Queue::Node::Node(const Item &i):item(i),next(0){}
对类进行嵌套和组合不同。
- 组合将
类对象
作为另一个类的成员
。 - 对类进行嵌套
不创建类成员
,而是定义一种类型
(类型仅包含嵌套类声明的类中有效)。
之前十二章的Queue类示例,嵌套了结构定义,从而实现了一种变相的嵌套类:
class Queue{
private:
//class scope definitions
//Node is a nested structure definition local to this class
struct Node {Item item;struct Node *next;};
}
我们知道结构是一种其成员在默认情况下为公有的类,所以换句话说Node在这里就是一个嵌套类,但是该定义并没有彰显它作为一个类所具有的功能(例如:没有显式构造函数),我们接下来根据唯一创建了Node对象的enqueue()方法进行改造一下:
bool Queue::enqueue(const Item &item){
if(isfull()){
return false;
}
Node *add = new Node; //create node
add->item = item;
add->next = NULL;
}
我们按照上述的赋值来显式构造一个函数
class Queue{
// class scope definitions
//Node is a nested class definition local to this class
class Node{
public:
Item item;
Node *next;
Node(const Item &i):item(i),next(0){}
};
};
由上述声明,我们可以稍微把enqueue()方法修改一下:
bool Queue::enqueue(const Item &item){
if(isfull()){
return false;
}
Node *add = new Node(item); //create node
}
15.2.1 嵌套类和访问权限
对类进行嵌套是为了实现另一个类,并避免名称冲突。
嵌套类的访问权限控制
- 嵌套类的
声明位置
决定了嵌套类的作用域
,决定了程序的哪些部分
可以创建其类的对象
。 - 和其它类一样,嵌套类的公有部分、私有部分和保护部分控制了对类成员的访问。
嵌套类访问的两种方式:
-
作用域
-
如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它。对于从包含类派生出来的类,嵌套类也是不可见的,因为派生类不能直接访问基类的私有部分。
-
如果嵌套类是在另一个类的保护部分声明的,则后者是可以看到它的。对于从包含类派生出来的类也是知道嵌套类的存在,并且可以直接创建这种类型的对象。
-
嵌套类的作用域为包含它的类,在类外部使用,则需要使用
类限定符
。 -
class Team{ public: class Coach{...}; ... }
举个例子,假如有一个失业的教练,他不属于任何球队。要在Team类的外面创建Coach对象,可以这样做:
Team::Coach forhire; //create a Coach object outside the Team class
-
-
访问控制
-
对嵌套类访问控制规则与常规类相同。
类声明的位置决定了类的作用域可见性。
-
类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类成员的访问权限。
-
15.2.2 模版中的嵌套
模版很适合用于实现容器类,将容器类定义转换为模版时候,不会因为它包含嵌套类而带来问题。下面城西演示如何进行这种转换。和类模版一样,改头文件也包含类模版和方法函数模版。
1、头文件
//
// Created by e on 2022/10/19.
//
#ifndef TEST_QUEUETP_H
#define TEST_QUEUETP_H
template<class Item>
class QueueTP{
private:
enum {Q_size = 10};
//Node is a nested class definition
class Node{
public:
Item item;
Node *next;
Node(const Item &i):item(i),next(0){}
};
Node *front; //pointer to front of Queue
Node *rear; //pointer to rear of Queue
int items = 0; //current number of items in Queue
int qSize; //maximum number of items in Queue
public:
QueueTP(int qs = Q_size);
QueueTP(const QueueTP &q);
QueueTP & operator=(const QueueTP & q);//重载赋值运算符
~QueueTP();
bool isEmpty()const{
return items == 0;
}
bool isFull()const{
return items == qSize;
}
int queueCount()const{
return items;
}
bool enqueue(const Item &item); //add item to end
bool dequeue(Item &item); //remove item from front
};
// QueueTP methods
template<class Item>
QueueTP<Item>::QueueTP(int qs):qSize(qs){
front = rear = 0;
items = 0;
}
template <class Item>
QueueTP<Item>::~QueueTP(){
Node *temp;
while (front != 0){
temp = front;
front = front->next;
delete temp;
}
}
//add item to queue
template<class Item>
bool QueueTP<Item>::enqueue(const Item &item) {
if(isFull()){
return false;
}
Node *add = new Node(item);
items++;
if(front == 0){
front = add;
} else{
rear->next = add;
}
rear = add;
return true;
}
//place front item into item variable and remove from queue
template<class Item>
bool QueueTP<Item>::dequeue(Item &item) {
if (front == 0){
return false;
}
item = front->item;
items--;
Node *temp = front;
front = front->next;
delete temp;
if (items == 0){
rear = 0;
}
return true;
}
template<class Item>
QueueTP<Item>::QueueTP(const QueueTP &q){
qSize = q.qSize;
front = rear = 0;
if (!q.isEmpty()) {
Node *cur = q.front;
while (cur) {
Node *add = new Node(cur->item);
items++;
if (front == 0) {
front = add;
} else {
rear->next = add;
}
rear = add;
cur = cur->next;
}
}
}
//QueueTP & operator=(const QueueTP & q);//重载赋值运算符
template<class Item>
QueueTP<Item> & QueueTP<Item>::operator=(const QueueTP<Item> &q) {
if(this == &q){
return *this;
}
qSize = q.qSize;
while (items){ //清理左值,避免内存泄漏
items--;
Node *temp = front;
front = front->next;
delete temp;
if (items == 0){
rear = 0;
}
}
if (!q.isEmpty()) {
Node *cur = q.front;
while (cur) {
Node *add = new Node(cur->item);
items++;
if (front == 0) {
front = add;
} else {
rear->next = add;
}
rear = add;
cur = cur->next;
}
}
return *this;
}
#endif //TEST_QUEUETP_H
2、执行文件
#include <iostream>
#include <string>
#include "queuetp.h"
int main(){
using std::cout;
using std::endl;
using std::cin;
using std::string;
QueueTP<string> cs(5);
string temp;
cout << "Now common function is working :\n";
while (!cs.isFull()){
cout << "Please enter your name.You will be served in the order of arrival.\n";
cout << "Name: ";
getline(cin,temp);
cs.enqueue(temp);
}
cout << "The queue is full.Processing begins!\n";
cout << "Now copy constructor is working :\n";
QueueTP<string>cb(cs);
while (!cb.isEmpty()){
cb.dequeue(temp);
cout << "Now processing " << temp << "...\n";
}
cout << endl;
cout << "Now the assignment operator is working :\n";
QueueTP<string>cd;
cd = cs;
while (!cd.isEmpty()){
cd.dequeue(temp);
cout << "Now processing " << temp << "...\n";
}
return 0;
}
输出:
Now common function is working :
Please enter your name.You will be served in the order of arrival.
Name: Kinsey Millhone
Please enter your name.You will be served in the order of arrival.
Name: Adam Dalgliesh
Please enter your name.You will be served in the order of arrival.
Name: Andrew Dalziel
Please enter your name.You will be served in the order of arrival.
Name: Kay Scarpetta
Please enter your name.You will be served in the order of arrival.
Name: Richard Jury
The queue is full.Processing begins!
Now copy constructor is working :
Now processing Kinsey Millhone...
Now processing Adam Dalgliesh...
Now processing Andrew Dalziel...
Now processing Kay Scarpetta...
Now processing Richard Jury...
Now the assignment operator is working :
Now processing Kinsey Millhone...
Now processing Adam Dalgliesh...
Now processing Andrew Dalziel...
Now processing Kay Scarpetta...
Now processing Richard Jury...
假如忘记里面的知识点,例如:重载赋值运算符(回到第11章和第12章进行复习)…
15.3 异常
异常是C++相对较新的功能,早期老编译器中可能会没有实现,但新的编译器中则是默认关闭了该特性。所以需要使用编译器选项来开启。
讨论异常前,先搞一个试验:计算两个数的调和平均数(即两个数倒数的平均值的倒数),表达式大概如下:
2.0 * x * y / (x + y);
如果x与y互为相反数,那么上述公式的分母变为0了(上面的表达式是不被允许计算的)。对于被零整除的情况,许多新的编译器通过生成一个表示无穷大的特殊浮点值来处理,cout将这种值显示为Inf,inf,INF或类似的东西(其实还挺智能的);而其他老式的编译器可能会生产在发生被零除时崩溃的程序。
15.3.1 返回abort()
调用位于头文件cstdlib(或stdlib.h)的Abort()函数。
典型实现:想标准错误(即cerr使用的错误流)发送消息 abnormal program termination(程序异常终止)
,然后 终止程序
。还返回一个随实现而异的值,告知OS(如果程序是由另一个程序调用,则告诉父进程),处理失败。
abort()
是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。exit()
:会刷新文件缓冲区,则不显示消息。
一般情况下,显示程序的异常中断消息随编译器而不同。
下面是一个使用abort()的小程序:
#include <iostream>
#include <cstdlib>
double hmean(double a,double b);
int main(){
double x , y ,z;
std::cout << "Enter two numbers: ";
while (std::cin >> x >> y){
z = hmean(x,y);
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}
double hmean(double a,double b){
if(a == -b){
std::cout << "untenable arguments to hmean()\n";
std::abort();
}
return 2.0 * a * b / (a + b);
}
输出:
Enter two numbers: 3 -3
untenable arguments to hmean()
进程已结束,退出代码为 134 (interrupted by signal 6: SIGABRT)
⚠️:在hmean()中调用abort()函数将直接终止程序,而不是先返回到main()。一般而言,显示的程序异常中断消息随编译器而异。这种异常终止,是不安全的。
15.3.2 程序错误码
一种比异常终止更灵活的方式:使用函数的返回值来指出问题。
任何数值都是有效的返回值,所以不存在可用于指出问题的特殊值。
一般使用指针参数
或者引用参数
来将值返回
给调用程序
,并使用函数的返回值
来指出成功
还是失败
。
下面就是一个使用这种方式的例子,它将hmean()的返回值重新定义为bool,让返回值指出成功了还是失败了,另外还给函数增加了第三个参数,用于提供答案。
#include <iostream>
#include <cfloat> // for DBL_MAX
bool hmean(double a,double b,double *ans);
int main(){
double x,y,z;
std::cout << "Enter two numbers: ";
while (std::cin >> x >> y){
if (hmean(x,y,&z)){
std::cout << "Harmonic mean of " << x << " and " << y << " is "
<< z << std::endl;
}else{
std::cout << "One value should not be the negative "
<< "of the other - try again.\n";
}
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}
bool hmean(double a,double b,double *ans){
if(a == -b){
*ans = DBL_MAX;
return false;
}else{
*ans = 2.0 * a * b / (a + b);
return true;
}
}
Enter two numbers: 3 3
Harmonic mean of 3 and 3 is 3
Enter next set of numbers <q to quit>: 1 1
Harmonic mean of 1 and 1 is 1
Enter next set of numbers <q to quit>: 2 0
Harmonic mean of 2 and 0 is 0
Enter next set of numbers <q to quit>: 1 9
Harmonic mean of 1 and 9 is 1.8
Enter next set of numbers <q to quit>: 3 -3
One value should not be the negative of the other - try again.
Enter next set of numbers <q to quit>: q
Bye!
第三个参数可以是指针或者是引用。对内置类型的参数,很多人都倾向于使用指针,因为这样可以明显看出哪个参数用于提供答案。
15.3.3 异常机制
C++异常是对程序运行过程中发生的异常情况的一种响应。
对异常的处理有3个组成部分:
-
引发异常
关键字throw
表示引发异常
,后面紧跟值
用来指出异常特征(描述语句,假如是string)。
-
使用处理程序捕获异常
关键字catch
表示捕获异常
,后面括号中紧跟类型声明
来指出异常处理程序要响应的异常类型(对应throw的异常特征的类型,假如也是string)
。其后的代码块则指出采取的措施。
-
使用try块
- 标识特定的异常
可能被激活的代码块
,后面紧跟一个
或多个catch块
。
- 标识特定的异常
-
表面需要注意代码引起的异常。
#include <iostream>
double hmean(double a,double b);
int main()
{
double x,y,z;
std::cout << "Enter tow number :";
while (std::cin >> x >> y)
{
try{
z = hmean(x,y); // 如果程序输入的值不对,则会使用catch块来对异常进行处理
}
catch (const char *s){
std::cout << s << std::endl;
std::cout << "Enter a new pair of numbers :";
continue; // 结束while循环的剩余部分,重新从while语句开始
}
std::cout <<"Harmonic mean of " << x << " and " << y << " is " << z <<std::endl;
std::cout << "Enter next set of number <q to quit> : ";
}
std::cout << "Bye !! \n";
return 0;
}
double hmean(double a ,doubel b)
{
if (a == -b)
// throw 用于执行返回语句,会终止函数的执行
// throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数
throw "bad hmean() arguments : a = -b not allowed ";
return 2.0 * a * b / (a + b);
}
输出:
Enter two numbers: 3 3
Harmonic mean of 3 and 3 is 3
Enter next set of numbers <q to quit>: 1 1
Harmonic mean of 1 and 1 is 1
Enter next set of numbers <q to quit>: 3 -3
bad hmean() arguments : a = -b not allowed
Enter a new pair of numbers: q
Bye!
上述例子中,被引发异常的是throw后的字符串"bad hmean() arguments : a = -b not allowed";异常类型可以是字符串(就像这个例子中那样)或其他C++类型;通常为类类型。
关键字catch表明这是一个处理程序,而char *s则表明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋值给s。
执行完try块中的语句后,如果没有引发任何异常,则程序跳过try块后面的catch块。
下面的图是程序的执行流程图:
如果函数引发了异常,而没有try块或没有匹配的处理程序时,程序会默认调用abort()函数。
15.3.4 将对象用作异常类型
通常,引发异常的函数将传递一个对象,这样做有一个很重要的优点:可以使用不同的异常类型来区分不同函数在不同情况下引发的异常。另外,对象可以携带信息,开发人员可以根据这些信息来确定引发异常的原因。
下面是针对函数hmean()引发的异常而提供的一种设计:bad_hmean。将一个bad_hmean对象初始化**(这个初始化存储俩参数值)**为传递给函数hmean()的值,而方法mesg()可用于报告问题。
1、头文件
//
// Created by e on 2022/10/22.
//
#ifndef TEST_EXC_MEAN_H
#define TEST_EXC_MEAN_H
class bad_hmean{
private:
double val_1;
double val_2;
public:
bad_hmean(double a = 0, double b = 0):val_1(a),val_2(b){}
void mesg();
};
inline void bad_hmean::mesg() {
std::cout << "hmean(" << val_1 << " , " << val_2 << "):"
<< "invalid arguments: a = -b\n";
}
class bad_gmean{
public:
double val_1;
double val_2;
bad_gmean(double a = 0 , double b = 0):val_1(a),val_2(b){}
const char *mesg();
};
inline const char * bad_gmean::mesg() {
return "gmean() arguments should be >= 0\n";
}
#endif //TEST_EXC_MEAN_H
2、执行文件
#include <iostream>
#include <cmath>
#include "exc_mean.h"
//函数原型
double hmean(double a,double b);
double gmean(double a,double b);
int main(){
using std::cout;
using std::endl;
using std::cin;
double x,y,z;
cout << "Enter two numbers: ";
while (cin >> x >> y){
try {
z = hmean(x,y);
//调和平均数
cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << endl;
//几何平均数
cout << "Geometric mean of " << x << " and " << y
<< " is " << gmean(x,y) << endl;
}
catch (bad_hmean &bg) {
bg.mesg();
cout << "Try again.\n";
continue;
}
catch (bad_gmean &hg) {
cout << hg.mesg();
cout << "Values used: " << hg.val_1 << ", "
<< hg.val_2 << endl;
cout << "Sorry,you dont get to play any more.\n";
break;
}
cout << "Enter next set of numbers <q to quit>: ";
}
cout << "Bye.\n";
return 0;
}
double hmean(double a,double b){
if (a == -b){
throw bad_hmean(a,b);
}
return 2.0 * a * b / (a + b);
}
double gmean(double a,double b){
if (a < 0 || b < 0){
throw bad_gmean(a,b);
}
return sqrt(a*b);
}
输出:
Enter two numbers: 4 12
Harmonic mean of 4 and 12 is 6
Geometric mean of 4 and 12 is 6.9282
Enter next set of numbers <q to quit>: 5 -5
hmean(5 , -5):invalid arguments: a = -b
Try again.
5 -2
Harmonic mean of 5 and -2 is -6.66667
Geometric mean of 5 and -2 is gmean() arguments should be >= 0
Values used: 5, -2
Sorry,you dont get to play any more.
Bye.
如果函数hmean()引发bad_hmean异常,第一个catch块将捕获该异常;如果gmean()引发bad_gmean异常,异常将逃过第一个catch块,被第二个catch块捕获。
bad_hmean异常处理程序使用了一条continue语句,而bad_gmean异常处理程序使用了一条break语句。如果用户给函数hmaen()提供的参数不正确,将导致程序跳过循环中余下的代码,直接进入下次循环;而用户给函数gmean()提供的参数不正确时将结束循环。
15.3.5 异常规范和C++11
异常规范是C++98中的一项功能,在C++11中已摒弃。但也需要了解:
double harm(double a) throw(bad_thing); // 可能会抛出异常,只会抛出 bad_thing 类型的异常
double marm(double) throw(); // 不会抛出异常
throw()
部分是异常规范,可能出现在函数原型
和函数定义
中,
可包含类型列表
,也可不包含。
异常规范的两个作用:
- 告知可能需要使用
try块
。 - 让编译器添加执行运行阶段
检查代码
是否违反异常规范。
C++11中支持了一种特殊的异常规范:使用新增的 关键字noexcept
指出函数不会引发异常。
double marm() noexcept; //
15.3.6 栈解退
提要:程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里继续执行程序。在调用过程中,函数参数被视为自动变量,被放到栈中,如果自动变量是类对象,则其相应的析构函数会被调用,函数执行结束时,函数对应的自动变量也被释放。
当函数调用出现异常而终止,则程序释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址,这个过程叫 栈解退
。函数返回仅仅处理该函数放在栈中的对象(释放操作),而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。
没有栈解退这种特性的后果❗️❗️:引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。
下面程序是一个栈解退的示例:
main()和means()都创建demo类型的对象,它指出什么时候构造函数和析构函数被调用。
函数main()中的try块能够捕获bad_hmean 和 bad_gmean异常,而函数means()中的try块只能捕获bad_hmean异常
1、头文件
//
// Created by e on 2022/10/22.
//
#ifndef TEST_EXC_MEAN_H
#define TEST_EXC_MEAN_H
class bad_hmean{
private:
double val_1;
double val_2;
public:
bad_hmean(double a = 0, double b = 0):val_1(a),val_2(b){}
void mesg();
};
inline void bad_hmean::mesg() {
std::cout << "hmean(" << val_1 << " , " << val_2 << "):"
<< "invalid arguments: a = -b\n";
}
class bad_gmean{
public:
double val_1;
double val_2;
bad_gmean(double a = 0 , double b = 0):val_1(a),val_2(b){}
const char *mesg();
};
inline const char * bad_gmean::mesg() {
return "gmean() arguments should be >= 0\n";
}
#endif //TEST_EXC_MEAN_H
2、执行文件
#include <iostream>
#include <cmath>
#include <string>
#include "exc_mean.h"
class demo{
private:
std::string word;
public:
demo(const std::string &str){
word = str;
std::cout << "demo " << word << " created\n";
}
~demo(){
std::cout << "demo " << word << " destroyed\n";
}
void show() const{
std::cout << "demo " << word << " lives!\n";
}
};
//function prototypes
double hmean(double a,double b);
double gmean(double a,double b);
double means(double a,double b);
int main(){
using std::endl;
using std::cout;
using std::cin;
double x,y,z;
{
demo d1("found in block in main()");
cout << "Enter two numbers: ";
while (cin >> x >> y) {
try {
z = means(x, y);
cout << "The mean of " << x << " and " << y
<< " is " << z << endl;
cout << "Enter next pair: ";
}
catch (bad_hmean &bh) {
cout << bh.mesg();
cout << "Try again.\n";
continue;
} catch (bad_gmean &bg) {
bg.mesg();
cout << "Values used: " << bg.val_1 << " and " << bg.val_2 << endl;
cout << "Sorry,you dont get play any more.\n";
break;
}
}
d1.show();
}
cout << "Bye!\n";
cin.get();
cin.get();
return 0;
}
double hmean(double a,double b){
if (a == -b){
throw bad_hmean(a,b);
}
return 2.0 * a * b / (a + b);
}
double gmean(double a,double b){
if (a < 0 || b < 0){
throw bad_gmean(a,b);
}
return sqrt(a*b);
}
double means(double a,double b){
double am,hm,gm;
demo d2("found in means()");
am = (a + b) / 2.0;
try{
hm = hmean(a,b);
gm = gmean(a,b);
} catch (bad_hmean & bh) {
bh.mesg();
std::cout << "Caught in means()\n";
throw ; // rethrows the exception
}
d2.show();
return (am + hm + gm) / 3.0;
}
输出:
demo found in block in main() created
Enter two numbers: 6 12
demo found in means() created
demo found in means()lives!
demo found in means() destroyed
The mean of 6 and 12 is 8.49509
Enter next pair: 6 -6
demo found in means() created
hmean(6 , -6):invalid arguments: a = -b
Caught in means()
demo found in means() destroyed
hmean(6 , -6):invalid arguments: a = -b
Try again.
6 -8
demo found in means() created
demo found in means() destroyed
gmean() arguments should be >= 0
Values used: 6 and -8
Sorry,you dont get play any more.
demo found in block in main()lives!
demo found in block in main() destroyed
Bye!
分析上述程序过程:
首先在main()函数中创建一个demo对象。然后进入代码块。执行下面语句时候会调用means()函数
z = means(x,y);
1⃣️进入means()函数中,又创建了另一个demo对象。函数means()使用6和12来调用hmean()和gmean(),它们将结果返回给means(),后者计算一个结果并将其返回(由于没出现异常,所以执行完try块后直接跳过catch块)。在返回结果前调用d2.show();返回结果后,函数means()执行完毕,因此自动为d2调用析构函数。
demo found in means() lives!
demo found in means() destroyed
2⃣️接下来将6与-6发送给函数means(),通过means()继续创建一个新的demo对象,然后进入try块计算hm与gm,可惜,这次就没那么顺利了,在调用hm时,引发了bad_hmean异常,该异常被means()中的catch块获取,下面输出语句说明这点:
demo found in means() created
hmean(6 , -6):invalid arguments: a = -b
当前的catch块中的throw语句使得means()函数终止执行,并将异常传回到main()函数中。然而尽管函数被终止执行,但是它仍为d2调用了析构函数(泪目)。
❗️❗️恰恰这就是本案例主要想展示的点:程序进行栈解退以回到能够捕获异常的地方,将释放栈中的自动存储型变量。同时,在means()中重新引发的异常被传递给main()中,又再次被适合的catch块给捕获了。
hmean(6 , -6):invalid arguments: a = -b
Try again.
3⃣️最后将6和-8发送给函数means()。同样,means()创建一个新的demo对象,然后将6、-8传递给hmean(),后者在处理它们时没有出现问题,但是传给gmean时候就引发了bad_gmean异常。由于means()没有捕获bad_gmean异常的功能,所以异常会被传递给main(),同时不再执行means其余的代码。同样,程序进行栈解退的时候,将释放局部的动态变量,因此,处在栈中的d2对象便调用其析构函数了。
demo found in means() destroyed
最后,main()中捕获了bad_gmean异常,循环结束:
gmean() arguments should be >= 0
Values used: 6 and -8
Sorry,you dont get play any more.
显示完消息后,并自动为d1调用析构函数:
demo found in block in main()lives!
demo found in block in main() destroyed
Bye!
为什么主函数中的对象会在函数结束前就调用析构函数呢?
✅:因为这个d1对象是在代码块中初始化的,所以它在代码块执行完毕前就要调用析构函数了。
15.3.7 其它异常特性
引发异常时编译器总是创建一个临时拷贝
,即使异常规范和catch块中指定的是引用
引用作为返回值的原因:避免创建副本以提高效率。但其有另一个重要特征‼️:基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与所有派生类对象匹配。
使用基类将能够捕获任何异常对象,而使用派生类对象只能捕获它所属类及其派生出来的类对象。看下面代码:
class bad_1{...};
class bad_2 : public bad_1 {...};
class bad_3 : public bad_2 {...};
...
void duper(){
...
if(oh_no){
throw bad_1();
}
if(rats){
throw bad_2();
}
if(drat){
throw bad_3();
}
}
...
try {
duper();
}
catch(bad_3 &be)
{ //statements }
catch (bad_2 &be)
{ //statements }
catch (bad_1 &be)
{ //statements }
如果有一个异常类继承层次结构,排序catch块的规则:将捕获位于层次结构最下面
的异常类的catch语句
放在最前面
,将捕获基类异常
的catch语句
放在最后面
。
如果不知道异常的类型,方法是省略号来捕获任何异常。
catch () {/* statement */} // catch any type exception
如果可以预知一些异常类型,类似于switch语句的使用。
try{
duper();
}
catch(bad_3 &be)
{
// statement
}
catch(bad_2 &be)
{
// statement
}
catch(bad_1 &be)
{
// statement
}
catch(bad_hmean &h)
{
//statement
}
catch(...)
{
// statement
}
15.3.8 exception类
C++异常的主要目的:设计容错程序时避免一些错误处理方式。
在C++中 exception头文件
中定义了 exception类
,类中的 what() 虚拟成员函数
,会返回一个字符串
,字符串的特征随实现而异。
#include <exception>
// 派生多个异常来处理
class bad_hmean : public std::exception
{
public:
const char * what() {return "bad arguments to hmean()";}
...
};
class bad_gmean : public std::exception
{
public:
const char * what() {return "bad arguments to gmean() ";}
...
};
// 直接使用一个基类来处理
try {
...
}
catch(std::exception & e)
{
cout << e.what() << std::endl;
}
C++定义的基于exception
的异常类型
-
stdexcept
异常类-
头文件stdexcept
定义的其它几个异常类,例如:logic_error
和runtime_error类
,都是从公有方式
从exception派生而来。class logic_error : public exception { public: exception logic_error(const string& what_arg); ... }; class domain_error : public exception { public: explicit domain_error(const string& what_arg); ... }
-
这些类的构造函数接受一个string对象作为参数,参数提供了方法
what()
以 C风格字符串方式返回字符数据
。-
logic_error
派生出来用于报告错误类型的类还有:
- 逻辑错误,
任何阶段
domain_error
:传递给函数的参数不在定义域内而引发异常,例如要传一个参数给函数sin(),如果参数不在[-1,1]中则会引发此错误。invalid_error
:传递了一个意料之外的值length_error
:指出没有足够的空间来执行所需的操作。out_of_bounds
:用于指示索引错误。,例如:定义一个类似于数组的类时,其operator()[]在使用的索引无效时引发out_of_bounds异常。
try{ ... } catch(out_of_bounds %oe){ //catch out_of_bounds error ... } catch(logic_error %oe){ //catch remaining logic_error family ... } catch(exception &oe){ // catch runtime_error,exception objects ... }
- 逻辑错误,
-
runtime_error 类型派生出来的类:
- 错误发生在
运行阶段
- range_error:不在函数允许的范围内,和上溢、下溢无关。
- overflow_error:上溢。
整型
和浮点型
都有可能。 - underflow_error:下溢。要发生在
浮点数计算
。
- 错误发生在
-
-
每一个类
都有自己的构造函数
,使what()方法
能够返回的字符串。
-
-
bad_alloc异常和new
- new 请求的内存(简单地说就是:一方太贪心,一方给不起)
分配失败
,则会引发bad_alloc
的异常错误。 - 数组中最为常见。
#include <iostream> #include <new> #include <cstdlib> // for exit(),EXIT_FAILURE using namespace std; struct Big { double stuff[200000000]; }; int main() { Big *pb; try { cout << "Trying to get a big block of memory:\n"; pb = new Big[100000]; // 160000000000000 bytes cout << "Got past the new request:\n"; } catch (bad_alloc &ba) { cout << "Caught the exception!\n"; cout << ba.what() << endl; exit(EXIT_FAILURE); } cout << "Memory successfully allocated\n"; pb[0].stuff[0] = 4; cout << pb[0].stuff[0] << endl; delete []pb; return 0; } 输出: Trying to get a big block of memory: Caught the exception! std::bad_alloc
- new 请求的内存(简单地说就是:一方太贪心,一方给不起)
-
空指针和new
-
new 分配内存失败,则会返回一个空指针
。也是从exception类
派生而来。所以C++标准提供了用法:
int * pi = new (std::nothrow) int; int * pa = new (std::nowthrow) int[500];
-
15.3.9 异常、类和继承
异常、类和继承以三种方式相互联合。首先,从一个异常类派生出另一个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可以被继承,还可以用作基类。
下面程序带领我们演示上述可能性的探索之旅。这个头文件声明了一个Sales类,它用于存储一个年份以及一个包含12个月的销售数据的数组。LabeledSales类是Sales派生而来的,新增一个用于存储数据标签的成员。
1、头文件
#ifndef TEST_SALES_H
#define TEST_SALES_H
#include <stdexcept>
#include <string>
class Sales {
public:
enum {
MONTHS = 12
}; // could be a static const
class bad_index : public std::logic_error {
private:
int badIndex; //bad index value
public:
explicit bad_index(int index, const std::string &s = "Index error in Sales object\n");
int bi_val() const { return badIndex; }
virtual ~bad_index() throw() { }
};
explicit Sales(int yy = 0);
Sales(int yy, const double *gr, int n);
virtual ~Sales() { }
int Year() const { return year; }
virtual double operator[](int i) const;
virtual double &operator[](int i);
private:
double gross[MONTHS];
int year;
};
class LabeledSales : public Sales {
public:
class nbad_index : public Sales::bad_index {
private:
std::string lbl;
public:
nbad_index(const std::string &lb, int index, const std::string &s = "Index error in LabeledSales object\n");
const std::string &label_val() const { return lbl; }
virtual ~nbad_index() throw() { }
};
explicit LabeledSales(const std::string &lb = "none", int yy = 0);
LabeledSales(const std::string &lb, int yy, const double *gr, int n);
virtual ~LabeledSales() { }
const std::string &Label() const { return label; }
virtual double operator[](int i) const;
virtual double & operator[](int i);
private:
std::string label;
};
#endif //TEST_SALES_H
解释用意:
1⃣️bad_index类被嵌套在Sales的公有部分,这使得客户类的catch块可以使用这个类作为类型。这个类是从logic_error类派生出来的,能够存储和报告数组索引的超界值(out_of_bounds value)
2⃣️bad_index类被嵌套在LabeledSales的公有部分。它是从bad_index类派生而来的,存储和报告LabeledSales对象的标签功能。由于bad_index是从logic_error派生而来的,因此nbad_index归根到底也是从logic_error派生而来的。
3⃣️我们可以看到两个嵌套类都使用了异常规范throw(),这是因为它们归根结底是从基类exception派生而来的,而exception的虚构造函数使用了异常规范throw()“c++98有、c++11无”。
4⃣️如果基类中的一个成员被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。
2、源文件
//
// Created by e on 2022/10/23.
//
#include "sales.h"
using std::string;
Sales::bad_index::bad_index(int index, const std::string &s) : std::logic_error(s), badIndex(index) {}
Sales::Sales(int yy) {
year = yy;
for (int i = 0; i < MONTHS; ++i) { //首先初始化数组gross
gross[i] = 0;
}
}
Sales::Sales(int yy, const double *gr, int n) {
year = yy;
int lim = (n < MONTHS) ? n : MONTHS; //控制数组长度,gr是传入的数组,n为数组的长度
int i;
for (i = 0; i < lim; ++i) {
gross[i] = gr[i];
}
for (; i < MONTHS; ++i) { //假如传入的数组长度小于既定的数组长度MONTHS,那么其余的数组置为0
gross[i] = 0;
}
}
double Sales::operator[](int i) const {
if (i < 0 || i >= MONTHS) {
throw bad_index(i);
}
return gross[i];
}
double &Sales::operator[](int i) {
if (i < 0 || i >= MONTHS) {
throw bad_index(i);
}
return gross[i];
}
LabeledSales::nbad_index::nbad_index(const std::string &lb, int index, const std::string &s) : Sales::bad_index(index, s) {
lbl = lb;
}
LabeledSales::LabeledSales(const std::string &lb, int yy) : Sales(yy) {
label = lb;
}
LabeledSales::LabeledSales(const std::string &lb, int yy, const double *gr, int n) : Sales(yy, gr, n) {
label = lb;
}
double LabeledSales::operator[](int i) const {
if (i < 0 || i >= MONTHS) {
throw nbad_index(Label(), i);
}
return Sales::operator[](i);
}
double &LabeledSales::operator[](int i) {
if (i < 0 || i >= MONTHS) {
throw nbad_index(Label(), i);
}
return Sales::operator[](i);
}
源文件说明:
1⃣️初始化列表,子类初始化父类的私有成员,子类可以通过调用父类的成员方法,可以读取到先前初始化的父类私有成员的值。就好像下面初始化了一个子类LabeledSales,通过调用父类的成员方法 int Year()const{},间接读取到了所初始化的年份。
2⃣️ 如同1⃣️nbad_index类继承bad_index,通过初始化列表将bad_index中的私有成员badIndex初始化,利用父类成员方法间接访问。
3⃣️ what函数用来表示异常的具体信息的, bad_index与nbad_index通过对logic_error初始化为s,就有了对应的报错信息。line44,nbad_index是通过初始化列表对bad_index中的有参构造函数中s默认参数进行覆盖,变成了Index error in LabeledSales object。
3、执行文件
#include <iostream>
#include "sales.h"
int main(){
using std::cout;
using std::endl;
using std::cin;
double vals1[12] = {
1220,1100,1122,2212,1232,2334,
2884,2393,3302,2922,3002,3544
};
double vals2[12] = {
12,11,22,21,32,34,
28,29,33,29,32,35
};
Sales sales1(2011,vals1,12);
LabeledSales sales2("Blogstar",2012,vals2,12);
cout << "First try block:\n";
try {
int i;
cout << "Year = " << sales1.Year() << endl;
for (i = 0; i < 12 ;++i){
cout << sales1[i] << ' ';
if (i % 6 == 5){
cout << endl;
}
}
cout << "Year = " << sales2.Year() << endl;
cout << "Label = " << sales2.Label() << endl;
for (i = 0; i <= 12; ++i) {
cout << sales2[i] << ' ';
if (i % 6 == 5){
cout << endl;
}
}
cout << "End of try block 1.\n";
}
catch (LabeledSales::nbad_index & bad) {
cout << bad.what();
cout << "Company: " << bad.label_val() << endl;
cout << "bad index: " << bad.bi_val() << endl;
}
catch (Sales::bad_index &bad) {
cout << bad.what();
cout << "bad index: " << bad.bi_val() << endl;
}
cout << "\nNext try block:\n";
try {
sales2[2] = 37.5;
sales1[20] = 23345;
cout << "End of try block 2.\n";
}
catch (LabeledSales::nbad_index &bad) {
cout << bad.what();
cout << "Company: " << bad.label_val() << endl;
cout << "bad index: " << bad.bi_val() << endl;
}
catch (Sales::bad_index &bad) {
cout << bad.what();
cout << "bad index: " << bad.bi_val() << endl;
}
cout << "done\n";
return 0;
}
输出:
First try block:
Year = 2011
1220 1100 1122 2212 1232 2334
2884 2393 3302 2922 3002 3544
Year = 2012
Label = Blogstar
12 11 22 21 32 34
28 29 33 29 32 35
Index error in LabeledSales object
Company: Blogstar
bad index: 12
Next try block:
Index error in Sales object
bad index: 20
done
解释说明:
catch的抓捕异常的顺序是:派生类->基类,因为派生类能捕获自身对象或者派生类对象所触发的异常,而父类可以捕获自身或者自己派生的任何对象的任何异常。
15.3.10 异常丢失
异常被引发后,会导致问题的两种情况:
-
意外异常(unexpected exception):在带异常规范的函数中引发,但必须与规范列表中的某种异常匹配。
- 可以通过调用
terminate()
(默认行为)、abort()
或者exit()
来终止程序 - 引发异常
- 可以通过调用
-
未捕获异常(uncaught exception):在没有try和catch块外抛出的异常。
- 不会导致程序立即异常停止。程序将首先调用terminate(),在默认情况下调用abort()函数,我们可以指定terminate()所调用的函数(而不是abort()),为此可以调用set_terminate()函数
//terminate_handler指向没有参数和返回值的函数指针 typedef void (*terminate_handler); terminate_handler set_terminate(terminate_handler f) throw(); //c++98 terminate_handler set_terminate(terminate_handler f) throw noexcept; //c++11 void terminate();//c++98 void terminate() noexcept;//c++11
-
下面举例
#include <exception> using namespace std; void myQuit(){ cout << "Terminating due to uncaught exception\n"; exit(5); } //终止操作指定为调用该函数 set_terminate(myQuit); try{ x = Argh(a,b); } catch(out_of_bounds &ex){ ... }
‼️原则上,异常规范应包含函数调用的其他函数引发的异常。举个例子:如果Argh()调用了Duh()函数,而后者可能引发retort对象异常,则Argh()和Duh()的异常规范都应该包含retort,除非前者自己编写所有函数,
引发异常(第二种选择)的结果取决于unexpected_handler函数
所引发的异常以及引发意外异常的函数的异常规范:
- 如果新引发的异常与原来的异常规范
匹配
,则程序将从那里开始进行正常处理,即寻找与新引发的异常匹配的catch块。基本上是**用预期的异常取代意外异常
**。 - 如果新引发的异常和原来的异常规范
不匹配
,切异常规范中没包括std::bad_exception
类型,则程序调用terminate()
。bad_exception
是从exception类
派生而来。声明位于头文件execption
中。 - 如果新引发的异常与原来
不匹配
,且原来的异常规范
中包含std::bad_exception
类型,则不匹配的异常被std::bad_exception
异常所取代。
如果要捕获所有的异常,则方法如下:
// 1. 确保已声明异常头文件
#include <exception>
using namespace std;
// 2. 设计替代函数,将意外异常转换为 bad_exception 异常
void myexception()
{
throw std::bad_exception {};
}
// 3. 将bad_exception 类型包含在异常规范中,并添加到catch块中
double Argh(double,double) throw(out_of_bounds, bad_exception);
...
try {
x = Argh(a,b);
}
catch (out_of_bounds & ex)
{
...
}
catch (bad_exception & ex)
{
...
}
15.3.11 有关异常的注意事项
使用异常会增加程序代码,降低程序的运行速度。应在设计程序时候就加入异常处理功能,而不是以后再添加。
下面进一步讨论动态内存分配和异常。先看下面的函数:
void test01(int n){
string mesg("I'm trapped in an endless loop");
...
if(oh_no)
throw exception();
...
return;
}
string类采用动态内存分配。通常,当函数结束的时候,将为mesg调用string的析构函数。虽然throw语句过早地终止函数,但它仍然使用析构函数被调用,这要归功于栈解退。
接下来看第二个例子:
void test02(int n){
double *ar = new double[n];
...
if(oh_no){
throw exception();
}
...
delete []ar;
return;
}
⚠️这里有一个严重的问题:栈解退时,将删除栈中的变量ar(数组名为数组的首地址),但函数过早终止意味着函数末尾的delete[]语句被忽略了。现在指针消失了,它所指向的内存未被释放,所以这些内存被泄漏了。
当然这个问题是可以避免的,看如下:
void test03(int n){
double *ar = new double[n];
...
try {
if(oh_no){
throw exception();
}
}
catch(exception &ex){
delete []ar;
..
}
...
delete []ar;
return;
}
尽管异常处理会降低程序的性能,但是不进行异常处理的代价对于某些大项目来说代价是非常高的。
15.4. RTTI
RTTI(Runtime Type Identification,运行阶段类型识别)
,C++11中新添加的新特性。
RTTI旨在为程序在运行阶段确定对象的类型
提供一种标准方式。
RTTI只适用于包含虚函数的类
。
C++中支持RTTI的3个元素
dynamic_cast
运算符:将使用一个指向基类的指针类生成一个指向派生类的指针,否则该运算返回0 ---- 空指针。typeid
运算符:返回一个指出对象的类型的值type_info
结构:存储有关特定类型的信息。
只有将RTTI用于包含虚函数
的类层次结构
,原因在于只有对于这种类层次结构,才应该将派生类对象的地址赋给基类指针。
15.4.1 dynamic_cast
运算符
是最常用的RTTI架构,能够回答“是否可以安全地将对象的地址赋值给特定类型的指针”。
只有指针类型与对象的类型(或对象的直接或间接基类的类型)相同的类型转换才一定是安全的。
看下面的类层次结构:
class Grand {// has virtual methods};
class Superb:public Grand{..};
class Manificent:public Superb{...};
Grand *pg = new Grand;
Grand *ps = new Superb;
Grand *pm = new Magnificent;
//类型转换
Magnificent *p1 = (Magnificent *)pm; #1
Magnificent *p2 = (Magnificent *)pg; #2
Grand *p3 = (Magnificent *)pm; #3
只有那些指针类型与对象的类型(或者独享的直接或者间接基类的类型)相同的类型转换才一定是安全的。
1⃣️类型转换#1就是安全的,因为它将Magnificent类型的指针指向类型为Magnificent的对象。
2⃣️类型转换#2不安全,因为它将基类对象(Grand)的地址赋给派生类(Magnificent)的指针。(Magnificent对象可能包含一些Grand对象没有的数据成员。)
3⃣️类型转换#3安全的。因为它将派生对象的地址赋给基类指针。公有派生确保Magnificent对象同时也是一个Superb对象(直接基类)和一个Grand对象(间接基类)。
语法格式:
Superb *pm = dynamic_cast<Superb *>(pg) ;
如果指针pg能够安全地被转换为Superb*,那么运算符将返回对象的地址,否则返回一个空指针。
结合下面语句从大范围地讲:如果指向的对象(*pt)的类型为Type或者从Type类直接或者间接派生而来的类型,则pt即可转为Type类型,反之,结果为0。
dynamic_cast<Type *>(pt);
下面演示一下dynamic_cast的用法:
#include <iostream>
#include <cstdlib>
#include <ctime>
using std::cout;
class Grand{
private:
int hold;
public:
Grand(int h = 0):hold(h){}
virtual void Speak() const {cout << "I am a grand class!\n";}
virtual int Value() const {return hold;}
};
class Superb: public Grand{
public:
Superb(int h = 0):Grand(h){}
void Speak() const {cout << "I am a superb class!\n";}
virtual void Say() const{
cout << "I hold the superb value of " << Value() << "!\n";
}
};
class Magnificent : public Superb{
private:
char ch;
public:
Magnificent(int h = 0,char c = 'A'):Superb(h),ch(c){}
void Speak() const{ cout << "I am a Magnificent class!\n";}
void Say() const{
cout << "I hold the character " << ch << " and the integer " << Value() << "!\n";
}
};
Grand * GetOne();
int main(){
std::srand(std::time(0));
Grand *pg;
Superb *ps;
for (int i = 0; i < 5; ++i) {
pg = GetOne();
pg->Speak();
if (ps = dynamic_cast<Superb*>(pg)){
ps->Say();
}
}
return 0;
}
Grand *GetOne(){
Grand *p;
switch (std::rand() % 3) {
case 0:
p = new Grand(std::rand() % 100);
break;
case 1:
p = new Superb(std::rand() % 100);
break;
case 2:
p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);
break;
}
return p;
}
输出:
I am a Magnificent class!
I hold the character V and the integer 56!
I am a Magnificent class!
I hold the character X and the integer 53!
I am a Magnificent class!
I hold the character E and the integer 63!
I am a superb class!
I hold the superb value of 70!
I am a superb class!
I hold the superb value of 71!
下面的图为上述程序的结构图:
即使编译器支持RTTI,但在默认情况下,也可能是关闭该特性。
dynamic_cast
也可以用于引用。因为没有与空指针对应的引用值,所以无法使用特殊的引用值来指示失败。所以失败了,就会引发 bad_cast
的异常。
#include<typeinfo> // for bad_cast
try {
Superb & rs = dynamic_cast<Superb &> (rg);
...
}
catch (bad_cast &) {
...
};
15.4.2 typeid
运算符 和 type_info
类
typeid
运算符使能够确定两个对象
是否为同种类型
。接受两种参数:
- 类名
- 结果为对象的表达式
typeid
运算符返回一个type_info对象的引用
,其中 type_info
在头文件ypeinfo
中定义的一个类。
typ_info类
重载 ==
和 !=
运算符,以便于使用对类型进行比较。它包含一个name()成员,该函数返回一个随实现而异的的字符串:通常是类名。
示例
typeid(Magnificent) == typeid(*pg) // 将pg指向一个 Magnifgicent 对象,表达式返回结果为True,否则为False。
typeid
测试用来选择一种操作
,因为操作不是类的方法,所以 不能通过类指针来调用它。
‼️如果pg是一个空指针,那么程序将引发bad_typei.d异常,该异常类型在exception派生而来的,在头文件typeinfo中声明。
稍微修改一下15.4.1的示例,以让修改后的程序可以使用typeid()和name()
#include <iostream>
#include <cstdlib>
#include <ctime>
using std::cout;
using std::endl;
class Grand{
private:
int hold;
public:
Grand(int h = 0):hold(h){}
virtual void Speak() const {cout << "I am a grand class!\n";}
virtual int Value() const {return hold;}
};
class Superb: public Grand{
public:
Superb(int h = 0):Grand(h){}
void Speak() const {cout << "I am a superb class!\n";}
virtual void Say() const{
cout << "I hold the superb value of " << Value() << "!\n";
}
};
class Magnificent : public Superb{
private:
char ch;
public:
Magnificent(int h = 0,char c = 'A'):Superb(h),ch(c){}
void Speak() const{ cout << "I am a Magnificent class!\n";}
void Say() const{
cout << "I hold the character " << ch << " and the integer " << Value() << "!\n";
}
};
Grand * GetOne();
int main(){
std::srand(std::time(0));
Grand *pg;
Superb *ps;
for (int i = 0; i < 5; ++i) {
pg = GetOne();
cout << "Now processing type " << typeid(*pg).name() << endl;
pg->Speak();
if (ps = dynamic_cast<Superb*>(pg)){
ps->Say();
}
if (typeid(Magnificent) == typeid(*pg)){
cout << "Yes , you're really Magnificent" << endl;
}
}
return 0;
}
Grand *GetOne(){
Grand *p;
switch (std::rand() % 3) {
case 0:
p = new Grand(std::rand() % 100);
break;
case 1:
p = new Superb(std::rand() % 100);
break;
case 2:
p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);
break;
}
return p;
}
输出:
Now processing type 11Magnificent
I am a Magnificent class!
I hold the character Z and the integer 58!
Yes , you're really Magnificent
Now processing type 11Magnificent
I am a Magnificent class!
I hold the character C and the integer 50!
Yes , you're really Magnificent
Now processing type 6Superb
I am a superb class!
I hold the superb value of 69!
Now processing type 5Grand
I am a grand class!
Now processing type 5Grand
I am a grand class!
15.4.3 误用RTTI的例子
对15.4.2的例子放弃dynamic_cast和虚函数则写出如下代码:
Grand *pg;
Superb *ps;
Magnificent *pm;
for (int i = 0; i < 5; ++i) {
pg = GetOne();
if (typeid(Magnificent) == typeid(*pg)){
pm = (Magnificent *)pg;
pm->Say();
pm->Speak();
}else if(typeid(Superb) == typeid(*pg)){
ps = (Superb *)pg;
pg->Say();
pg->Speak;
}else{
pg->Speak();
}
}
放弃上述二者,代码将变得又长又难看,如果当你想要从Magnificent再派生一个类出来的话,那么程序还得多加一个else if,并且新的派生类还需要重新定义Speak()和Say()函数。
✅如果发现程序中if else系列语句中使用typeid,从这个例子应该得到启发:是否需要使用dynamic_cast和虚函数进行优化。
15.5 类型转换运算符
假设 High 和 Low 是两个类。
通过4种类型转换运算符来使得转换过程增加规范。
-
dynamic_cast
-
能够在类层次结构中进行向上转换,而不允许其他转换。
dynamic_cast <type_name> (expression) // 判断expression是否可以转换成为 type_name 类型
-
-
const_cast
-
用于执行只有一种用途的类型转换,即改变值为const或者volatile
。语法和 dynamic_cast相同
// 语法格式 const_cast <type_name> (expression) // 删除const属性,使变成可修改对象 // 示例 High bar; const High *pbar = &bar; High *pb = const_cast <High *> (pbar); // 让*pb成为一个用于修改bar对象值的指针。删除const属性
-
提供该运算符的原因:有时候可能需要这样一个值,它在大多数时候是常量,但有些时候又需要进行修改。在这种情况下,可以将其先声明为const,后面在需要修改它的时候,再使用const_cast。
-
const_cast不是万能的,下面看一个例子:
#include <iostream> using std::endl; using std::cout; using std::cin; void change(const int *pt,int n); int main(){ int pop1 = 38383; const int pop2 = 2000; cout << "pop1、pop2: " << pop1 << ", " << pop2 << endl; change(&pop1,-103); change(&pop2,-103); cout << "pop1、pop2: " << pop1 << ", " << pop2 << endl; return 0; } void change(const int *pt,int n){ int *pc; pc = const_cast<int *>(pt); *pc += n; } 输出: pop1、pop2: 38383, 2000 pop1、pop2: 38280, 2000
程序说明:const_cast运算符可以删除const int *pt中的const,使得编译器能够接受change()函数。指针pt删除了pt的const的特征,因此可以用来修改指向的值,前提是指向的值不是const。因此pop1可修改,pop2不可修改。
我们怎么样才可以同时修改两个值呢?
✅在const后加个volatile关键字,再配合const_cast进行强制转换。
贴个链接:https://blog.csdn.net/qq_38877888/article/details/114190548
上面的链接将编译器如何处理被const修饰的值的。
-
-
static_cast
-
type_name 和 expression互相隐式转换为其所属的类型时,转换才合法,否则将出错。
static_cast <type_name> (expression)
-
假设High类是Low类的基类,而Pond是一个无关的类,则从High到Low(向下转换)、Low到High(向上转换)的转换都是合法的,而从Low到Pond的转换是不允许的。
-
static_cast无需进行类型转换就可以进行另一个方向的转换,所以其向下转换也是合法的,
-
也可以将枚举值转换为整型,将整型转换为枚举值……
-
-
reinterpret_cast
-
转换适用于依赖于实现的底层编程技术,具有不可移植性。
-
不做任何处理,也不能删除const属性
-
不支持所有的类型转换
- 可以将指针类型转换为足以存储指针表示的类型,但不能将指针转换为更小的整型或者浮点型。
- 不能将函数指针转换为数据指针。
reinterpret_cast <type_name> (expression)
下面类型在C语言是合理的,但在C++是不允许的,因为char类型太小了,不能存储指针。
char ch = char (&d); //convert address to a char
-