C++ Primer Plus学习笔记-第十五章:友元,异常和其它

第十五章:友元,异常和其它


前言:本章有三个主要内容,分别是友元,异常和其它;本章除了进一步深入剖析友元的使用(不仅仅是友元函数,还包括友元类),还会提供关于异常的处理方式(这对大型程序的稳定性十分重要),另外还会额外讨论一些其它知识;

1.友元

友元函数可用于扩展类的接口,而友元类的所有方法则都可以使用被友元类的数据成员,包括公有私有保护等所有成员;

在一个类中将另一个类声明为友元,只需在一个类中添加如下语句:

friend class Remote;
//友元声明对位置不敏感,在公有,私有,保护部分使用的效果是一样的

实际上,可能只需要将类中的某个成员函数解析为友元,那么可以借助成员运算符:

class Tv
{
	friend void Remote::set_chan(Tv & t, int c);
};

另外,将类声明为友元面临一个问题:需要将类声明本体放置在包含类的前面;当不想这么做或者两个类互相为对方的友元时,可以借鉴处理未定义但先使用的函数的方法:在使用它的代码前加一个声明性质的语句:

class Tv;
//这种技术有它自己的名字:前向声明

2.嵌套类

在C++中,可以将一个类声明放在另一个类中,这样的处理方式称为嵌套类;然而这和包含一个类的性质是不同的,前者只是包含了类的定义,并不是包含某个具体的类;并且前者的成员方法还可以自己创建嵌套类的实例;

如果想要在程序的其它地方使用嵌套类来创建实例,则需要使用作用域解析运算符:

Team::Coach forhire;
//Coach是嵌套在Team中的

下面的表格总结了嵌套类,结构和枚举的作用域结构特征:

声明位置包含它的类是否可以使用它从它派生而来的类是否可以使用它在外部是否可以使用
私有部分
保护部分
公有部分是,通过类限定符来使用

另外嵌套类还可以出现在模板中,不会导致额外的问题;

3.异常

异常是相对较新的C++功能,这意味着在某些较老的编译器中不得到支持,在某些较新的编译器中此选项是默认关闭的;

处理方法之一是:调用abort()函数;这个函数位于头文件或<stdlib.h>中;作用是向标准错误流发送abnormal program termination(程序异常终止)消息,然后终止程序,还返回一个随实现而异的值(如果程序是由另一个程序调用的,则告诉父进程);通知操作系统处理失败;是否刷新文件缓冲区也取决于实现;如果愿意,也可以使用exit(),该函数刷新文件缓冲区但不显示信息;

也可以使用一个全局变量来存储可能发生异常时的特征值,但务必确保这个值没有被挪作他用;

对异常的处理一般分三个部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用try块

throw语句是向catch块抛出错误信息,并匹配合适的处理块;该关键字后的值指出了异常的特征;该异常特征会尝试和catch块括号中的变量匹配,将进入合适的处理块进行错误处理;处理完成后继续到处理块序列下第一条常规语句中开始执行;

try块后面有一个花括号,表示其中特定的异常可能激活的代码,后面跟有一个或多个catch块;

下面让我们看一个简单的例子:

#include<iostream>
double hmean(double a, double b);

int main()
{
	double x, y;
	
	std::cout<<"Enter teo numbers";
	while (std::cin>> x >> y)
	{
		try {
			z = hmean(x, y);
		}
        //可能发生异常的代码
        
		catch (const char * s)
		{
			std::cout << s <<std::endl;
			std::cout<< "Enter a new pair of numbers:";
			continue;
		}
        //用于处理异常的代码
        
		std::cout<< "Harmonic mean of" << x << "and" << y << "is" << z << std::endl;
		std::cout<<"Enter newt set if numbers <q to quit>:";
	}
	std::cout << "Bye!\n";
	return 0;
}

double hmean(double a,double b)
{
    if (a == -b)
        throw "bad hmean() arguments:a = -b not allowed";//抛出一个异常等待和某个catch块匹配
    return 2.0 * a * b / (a + b);
}

我们可以看到try块中包含了一条语句,如果块中的语句抛出了异常,那么后面的catch块会对异常进行处理;但如果程序在try块外面调用hmean(),抛出的异常是无法被处理的;throw()函数和return类似,也中止函数的执行,但前者会沿着调用的路径回溯,直到遇到try块为止,若始终无法回溯到try块中,则这个异常不会得到处理;

如果try块中的语句没有引发异常,那么程序会跳过后面catch块的内容直接向后执行;另外,在引发异常但没有提供解决方案时,程序会默认调用abort()函数;

通常,引发异常的函数会传递一个保存异常信息的对象,并尝试和catch块匹配;另外,基类的引用默认可以指向继承类,这也就是说当我们使用引用(实际指针也有相同的性质)匹配异常时,可能会发生某个保存信息的类和它的基类引用匹配的情况,解决方法时按照继承的顺序,恰好反过来排列处理不同对象的catch块,这样就能保证每一种位于继承链中的异常类都能恰好匹配到自己的catch块;

C++11中有一种称为异常规范的语法,虽然饱受争议但我们仍应该了解:

double harm(double a) throw(bad_thing);//表示这个函数将可能主动抛出bad_thing异常
double marm(double b) throw();//表示目前没有发现这个函数可能抛出哪些异常
//throw关键字只表明可能抛出哪些异常,不声明不可能抛出异常

另外,还有另一种关键字:

double marm() noexept;//表示这个函数不可能抛出异常

当一个函数本身没有引发异常,但它调用的函数可能引发异常,或者下游调用链的某个函数可能引发了异常,而该函数又位于try块中时,就会引发栈解退,也就是说函数调用链会回溯,直到遇到第一个拥有try操作的语句块,然后向下面的catch块匹配,试图进行异常处理;不同的是,对于相关标记异常信息的对象,虽然理论上讲应当从栈中弹出,但为了保存异常信息实际上其析构函数并不会被调用;

在catch块中可能异常处理后还会再抛出异常,这样的异常会由下一个try-catch块捕获并进行处理;如果没有找到这样的处理程序,则默认情况下程序将异常终止;

有一个比较新奇的特性,那就是引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用;

有时候我们不知道所有的异常情况,这时可以使用省略号来概括表示所有异常情况:

catch (...) {//异常处理语句}

可以像这样写一个异常匹配的catch块序列:

try{
    duper();
    //这个函数可能抛出下面catch块将处理的异常
}
catch(bad_3 &be)
{//某个处理方案}
catch(bad_2 &be)
{//某个处理方案}
catch(bad_1 &be)
{//某个处理方案}
catch(bad_hmean & h)
{//某个处理方案}
catch(...)
{//某个处理方案}

C++有一个exception头文件,其中定义了一个名为exception的类,这个类有有一个名为what()的虚函数,返回一个字符串。该字符串的特征随实现而异;在exception的派生类中,可以对what()函数进行重载,来让它显示不同的错误信息(这就是刚刚提到的“特征随具体实现而异”);

这里让我们实际使用一下这个类:

#include <exception>
class bad_hmean : public std::exception
{
public:
    const char * what() {return"bad arguments to hmean()";}
....
};
class bad_gmean : ublic std::exception
{
public:
    const char * what() {return"bad argument to gmean()";}
};

try{
    ...
}
catch(std::exception & e)//两种异常在同一个catch块中处理
{
    cout << e.what() << endl;
...
}

头文件stdexcept定义了其它几个异常类,首先该文件定义了logic_error和runtime_error类,它们都是公有方式从exception派生得来的;这两个类的构造函数都接收一个string类作为参数,提供了方法what()以C风格字符串方式返回的字符数据类型;

logic_error派生出了这些派生类:

  • domain_error:传递给函数的参数不在定义域中
  • invalid_argument:给函数传递了一个意料外的值
  • length_error:没有足够的空间来执行所需要的操作
  • out_of_bounds:索引错误

runtime_error派生出了这些派生类:

  • range_error:计算结果不在函数允许范围之内,虽然没有发生上溢或下溢
  • overflow_error:发生上溢
  • underflow_error:发生下溢

这些类中,每个都有一个自己的what()函数,可以供返回字符串;另外补充一点,new申请内存空间失败时将引发bad_alloc异常;

但在曾经的实现中,申请内存空间失败时会返回一个空指针(实际上我认为这种处理方法也不赖);它(bad_alloc)也是从exception类公有派生来的;

当然,现在的实现也并不是完全取消了申请内存空间失败时的new,这种new在申请失败时返回空指针:

int * pi = new (std::nothrow) int;
int * pa = new (std::nothrow) int[500];

异常被引发后,有两种可能会遗失方向;在带异常规范的函数中引发但没有和处理块匹配称为意外异常,在捕获区域外抛出的异常称为未捕获异常;这两种情况都会导致程序异常终止;但我们可以定制程序面对这两种情况时的反应:

程序在遇到这两种意外情况时会调用函数terminate(),而该函数默认调用abort(),但我们可以使用函数set_terminate()来定制terminate()函数的行为,直接将函数的内存地址作为参数传入set_terminate()函数即可;

下面让我们来动手实现一次对默认处理函数的修改:

#include <exception>
using namespace std;

void myQuit()
{
    cout << "Terminating due to uncaught excetion\n";
    exit(5);
}

set_terminate(myQuit);
...//下面的程序将在遇到未被合适处理的异常时,调用我们自定义的处理函数

上面的情况是针对函数合理的抛出了异常,但没有被捕获导致的,下面的情况是由函数抛出了预料之外的异常导致的:程序调用unexpected()函数,默认调用abort()函数终止程序;当然类似的是该函数也可以使用set_unexpected()函数来修改默认调用的函数,这里就不再赘述了;

但这种情况是特殊的并且限制更严格,具体而言该情况的异常处理函数可以:

  • 通过调用terminate(),abort(),exit()来终止程序
  • 引发异常

引发异常(第二种选择)的结果取决于unexpected_handler函数所引发的异常以及引发意外异常的函数的异常规范:

  • 如果新引发的异常和原来的异常规范匹配,则程序将从那里开始进行正常处理即寻找与新引发异常匹配的catch块;基本上这种方法将用预期的异常取代意外的异常;

  • 如果新引发的异常和原来的异常规范不匹配,且异常规范中没有包括std::bad_expection类型,则程序将调用terminate();bad_exception是从exception派生而来的,其声明头文件是exception;

  • 如果新引发的异常与原来的异常规范不匹配,且原来的异常规范中包含了std::bad_exception类型,则不匹配的异常将被std::bad_exception异常所取代

下面让我们将所有捕获的意外异常都替换为预期异常:

#include<exception>
using namespace std;

void myUnexpected()
{
    throw std::bad::exception;
}

set_unexpected(myUnexpected);

另外要注意内存管理和异常并不是总能协调工作;要正确释放那些存储在堆中的数据,应当在异常处理中添加释放内存的代码;

4.RTTI

RTTI指的是运行过程类型识别,这虽然是一种包含争议的C++特性,但我们不去研究谁对谁错只讨论这项技术本身的特性;另外虽然某些不同厂家提供的库中有这项功能,但往往各自的实现机制不同;

RTTI元素一共有三个:

  • 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个派生类的指针,否则该运算符返回空指针;
  • typeid运算符返回一个指出对象类型的值
  • type_info结构存储了有关特定类型的信息

dynamic_cast的使用方法:

Superb * pm = dynamic_cast<Superb *>(pg);
//若成功将完成赋值,失败则得到空指针

注意:即使编译器支持RTTI,它也可能是默认关闭的;另外,还可以将dynamic_cast运算符用于引用:

Superb & pm = dynamic_cast<Superb &>(pg);

typeid和typeof有些相似,它接收这两种参数:

  • 类名
  • 结果为对象的表达式

该运算符返回一个type_info对象的引用,后者是在头文件typeinfo中定义的;另外type_info还重载了运算符==和!=,可以用于进行比较;如果使用typeid()对一个空指针进行运算则会抛出一个bad_typeid异常,该异常从exception类中派生而来,在头文件typeinfo中声明;

5.类型转换运算符

在C++中有这样四种类型转换运算符:

  • dynamic_cast
  • const_cast
  • static_cast
  • reinterpret_cast

其中,dynamic_cast的使用方法是:

pl = dynamic_cast< type_name * >(expression);
//只能用于将基类或指向基类的引用或指针转换为指向派生类的

另外,const_cast的用法相似,但是只能用于const和volatile的转化;而static_cast则相对自由可以支持更多的类型;reinterpret_cast则用于危险的转换,这类转换往往由底层提供支持,不可移植也并不通用;

以上为本章所有内容!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值