C++——友元、异常和其他

一、友元

  类并非只能拥有友元函数,也可以将类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相违背;相反,它们提高了公有接口的灵活性。

  1、友元类

  一般来说,如果希望一个类型的对象发生变化时,另一个其他类型的对象也相应地跟着产生变化,可以使用友元类来实现。

  要想使一个类成为另一个类的友元类,只需要在另一个类的公有、私有或保护部分声明如下语句即可:

    friend  class  Classname;

  友元类声明的位置无关紧要。

  下面定义Tv类和Remote类,其中Remote是Tv的友元类,通过Remote对象可以控制TV对象: 

头文件:

 1 #include <stdio.h>
 2 #include <iostream>
 3 
 4 class Tv{
 5 public:
 6     friend class Remote;
 7     enum {Off,On};
 8     enum {MinVal,MaxVal = 20};
 9     enum {Antenna,Cable};
10     enum {TV, DVD};
11     
12     Tv(int s = Off, int mc = 125):state(s),volume(5),maxchannel(mc),channel(2),mode(Cable),input(TV){};
13     void onoff(){state ^= 1;}
14     bool ison(){return state == On;}
15     bool volup();
16     bool voldown();
17     void chanup();
18     void chandown();
19     void set_mode(){mode ^= 1;}
20     void set_input(){input ^= 1;}
21     void settings()const;
22 private:
23     int state;
24     int volume;
25     int maxchannel;
26     int channel;
27     int mode;
28     int input;
29 };
30 class Remote{
31 private:
32     int mode;
33 public:
34     Remote(int m = Tv::TV):mode(m){};
35     bool volup(Tv & t){return t.volup();}
36     bool voldown(Tv & t){return t.voldown();}
37     void onoff(Tv & t){t.onoff();}
38     void chanup(Tv & t){t.chanup();}
39     void chandown(Tv & t){t.chandown();}
40     void set_chan(Tv & t, int c){t.channel = c;};
41     void set_mode(Tv & t){t.set_mode();}
42     void set_input(Tv & t){t.set_input();}
43 };

 

实现文件:

 1 #include "TV.h"
 2 bool Tv::volup(){
 3     if (volume < MaxVal) {
 4         volume ++;
 5         return true;
 6     }
 7     return false;
 8 }
 9 bool Tv::voldown(){
10     if (volume > MinVal) {
11         volume --;
12         return true;
13     }
14     return false;
15 }
16 void Tv:: chanup(){
17     if (channel < maxchannel) {
18         channel++;
19         return;
20     }
21     channel = 1;
22 }
23 void Tv:: chandown(){
24     if (channel > 1) {
25         channel--;
26         return;
27     }
28     channel = maxchannel;
29 }
30 void Tv:: settings()const{
31     using namespace std;
32     cout << "TV is " << (state == Off?"Off":"On") << endl;
33     if (state == On) {
34         cout << "Volume setting = " << volume << endl;
35         cout << "Channel setting = " << channel << endl;
36         cout << "Mode = " << (mode == Antenna ? "antenna" : "cable") << endl;
37         cout << "Input = " << (input == TV? "TV" :"DVD") << endl;
38     }
39 }

 

main.cpp

 1 #include <iostream>
 2 #include "TV.h"
 3 
 4 using namespace std;
 5 
 6 int main(int argc, const char * argv[]) {
 7     Tv s42;
 8     cout << "Initial setting for 42\" TV:\n";
 9     s42.settings();
10     s42.onoff();
11     s42.chanup();
12     cout << "\nAdjusted settings for 42\" TV:\n";
13     s42.chanup();
14     cout << "\nAdjusted setting for 42\" TV:\n";
15     s42.settings();
16     
17     Remote grey;
18     grey.set_chan(s42, 10);
19     grey.volup(s42);
20     grey.volup(s42);
21     cout << "\n42\"settings after using remote :\n";
22     s42.settings();
23     
24     Tv s58(Tv::On);
25     s58.set_mode();
26     grey.set_chan(s58, 28);
27     cout << "\n58\" settings:\n";
28     s58.settings();
29     return 0;
30 }
31 
32 
33 输出结果:
34 Initial setting for 42" TV:
35 TV is Off
36 
37 Adjusted settings for 42" TV:
38 
39 Adjusted setting for 42" TV:
40 TV is On
41 Volume setting = 5
42 Channel setting = 4
43 Mode = cable
44 Input = TV
45 
46 42"settings after using remote :
47 TV is On
48 Volume setting = 7
49 Channel setting = 10
50 Mode = cable
51 Input = TV
52 
53 58" settings:
54 TV is On
55 Volume setting = 5
56 Channel setting = 28
57 Mode = antenna
58 Input = TV

 2、友元成员函数

 可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元。

 在一个类中将另一个类的成员函数声明为友元的通用格式如下:

    class ClassA{

      friend returntype ClassB::funcname(arguments);

    }

   要使编译器能处理上面的语句,它必须知道ClassB的定义。否则,它无法知道ClassB是一个类,而funcname()是这个类的方法。这意味着应将ClassB的定义放到ClassA的前面。一般来说,类ClassA的友元函数funcname()应含有一个ClassA对象的引用;funcname()又作为ClassB的成员函数,在ClassB中声明和定义,并且会提到ClassA对象,而这意味着ClassA的定义应当位于ClassB定义之前。避开这种循环依赖的方法是,使用前向声明。因此,需要在ClassB定义的前面插入下面的语句:

  class ClassA;//前向声明

  这样,排列次序应如下:

  class ClassA;

  class ClassB{....};

  class ClassA{....};

  注意:上面的次序不能交换。

  同时,友元成员函数应只在ClassB中声明,而在ClassA声明的后面定义。友元成员函数有可能会调用ClassA的成员,如果在ClassB中对其进行定义的话,将无法知道ClassA有哪些方法,因为ClassA的声明位于ClassB的后面。因此,为了解决这个问题,使ClassB声明中只包含方法声明,并将实际的定义放在ClassA类的后面。

  示例:

 1 #include <stdio.h>
 2 #include <iostream>
 3 
 4 class TV;
 5 class Remote{
 6 private:
 7     int volume;
 8 public:
 9     Remote(int vo = 0):volume(vo){}
10     void setVolume(TV & t,int vo);
11     void showVolume(){std::cout << "Remote's volume:" << volume << std::endl;};
12 };
13 class TV{
14 private:
15     int volume;
16 public:
17     TV(int vo = 0):volume(vo){}
18     void showVolume(){std::cout << "TV's volume:" << volume << std::endl;}
19     friend void Remote::setVolume(TV & t, int vo);
20 };
21 inline void Remote::setVolume(TV & t, int vo){
22     volume = vo;
23     t.volume = vo;
24 }

 

main.cpp

 1 #include <iostream>
 2 #include "Friend.h"
 3 
 4 using namespace std;
 5 
 6 int main(int argc, const char * argv[]) {
 7     TV tv;
 8     Remote remote;
 9     remote.setVolume(tv, 12);
10     tv.showVolume();
11     remote.showVolume();
12     return 0;
13 }
14 
15 输出结果:
16 TV's volume:12
17 Remote's volume:12

  3、其他友元关系

  我们可以让两个类成为彼此的友元。

  class ClassA{

  friend class ClassB;

  public:

    void funcA(ClassB & b);

    ...

  };

  class ClassB{

  friend class ClassA;

  public:

    void funcB(ClassA & a);

  };

  inline void ClassA::funcA(ClassB & b){

    ...

  }

  需要的注意的是,对于使用ClassB对象的ClassA方法,其原型可以在ClassB类声明之前声明,但是必须在ClassB类声明之后定义。

  4、共同的友元

  使用友元的另一种情况是,函数需要访问两个类的私有数据。从逻辑上看,这样的函数应是每个类的成员函数,但这是不可能的。它可以是一个类的成员,同时是另一个类的友元,但有时将函数作为两个类的友元更为合理。

  class ClassB;

  class ClassA{

    friend void sync(ClassA &, ClassB &);

    friend void sync(ClassB &, ClassA &);

  };

  class ClassB{

    friend void sync(ClassA &, ClassB &);

    friend void sync(ClassB &, ClassA &);

  };

  inline void sync(ClassA & a, ClassB & b){

    .....

  }

  inline void sync(ClassB & b, ClassA & a){

    .....

  }

  向前声明使编译器看到ClassA类声明中的友元声明时,知道ClassB是一种类型。

 

二、嵌套类

  在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类,它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

  对类进行嵌套与包含并不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

  对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。

  1、嵌套类和访问权限

  有两种访问权限适合于嵌套类。首先,嵌套类的声明位置决定了嵌套类的作用域,即它决定了程序的哪些部分可以创建这种类的对象。其次,和其他类一样,嵌套类的公有部分、保护部分和私有部分控制了对类成员的访问。在哪些地方可以使嵌套类以及如何使用嵌套类,取决于作用域和访问控制。

  (1)作用域

  如果嵌套类是在另一个类的私有部分声明的,则只有后者知道它。

  如果嵌套类是在另一个类的保护部分声明的,则它对于后者来说是可见的,但是对于外部世界则是不可见的。然而,在这种情况中,派生类知道嵌套类,并可以直接创建这种类型的对象。

  如果嵌套类是在另一个类的公有部分声明的,则允许后者、后者的派生类以及外部世界使用它,因为它是公有的。然而,由于嵌套类的作用域为包含他的类,因此在外部世界使用它时,必须使用类限定符。

  嵌套结构和枚举的作用域与嵌套类相同。其实,很多程序员都适用公有枚举来提供可供客户程序员使用的类常数。下面的表总结了嵌套类、结构和枚举的作用域特征。

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

  (2)访问控制

  类可见后,其决定作用的将是访问控制。对嵌套类访问权的控制规则与对常规类相同。

  总之,类声明的位置决定了类的作用域或可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套类的访问权限。

 

三、异常

  程序有时会遇到运行阶段错误,导致程序无法正常地运行下去。例如,程序可能试图打开一个不可用的文件,请求过多的内存,或者遭遇不能容忍的值。通常,程序员都会试图预防这种意外情况。C++异常为处理这种情况提供了一种功能强大而灵活的工具。异常是相对较新的C++功能,有些老式编译器可能没有实现。另外,有些编译器默认关闭这种特性,可能需要编译器选项来启用它。

  1、调用abort()

  abort()函数的原型位于头文件cstdlib(stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它返回一个随实现而异的值,告诉操作系统(如果程序是另一个程序调用的,则告诉父进程),处理失败。abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。如果愿意,也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

  2、返回错误码

  一种比异常终止更灵活的方法是,使用函数的返回值来指出问题。可以使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。通过告知调用程序成功了还是失败了,使得程序可以采取除异常终止程序之外的其他措施。例如:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 bool hmean(double x, double y, double & z);
 6 int main(int argc, const char * argv[]) {
 7     double z;
 8     if (hmean(12.4, 33.6, z)) {
 9         cout << z;
10     }
11     else
12         cout << "错误发生了";
13     return 0;
14 }
15 bool hmean(double x, double y, double & z){
16     if (x == -y) {
17         return false;
18     }
19     z =  2*x*y/(x+y);
20     return true;
21 }

  3、异常机制

  接下来介绍如何使用异常机制来处理错误。C++异常是对程序运行过程中发生的异常情况的一种响应。异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常的处理有三个组成部分:

  *引发异常;

  *使用处理程序捕获异常;

  *使用try块;

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 double hmean(double,double);
 6 int main(int argc, const char * argv[]) {
 7     double x,y,z;
 8     cout << "请输入两个数字:\n";
 9     while (cin >> x >> y) {
10         try {
11             z = hmean(x, y);
12         } catch (const char * s) {
13             cout << s << endl;
14             cout << "请重新输入数字:\n";
15            continue;
16         }
17         cout << "x和y的调和平均数:" << z << endl;
18         cout << "请继续输入:\n";
19     }
20     return 0;
21 }
22 double hmean(double x, double y){
23     if (x == -y) {
24         throw "在函数hmean()中x = -y是不允许的";
25     }
26     return 2.0*x*y/(x+y);
27 }

输出结果:

请输入两个数字:

12

32

xy的调和平均数:17.4545

请继续输入:

11

-11

在函数hmean()x y是不允许的

请重新输入数字:

12

55

xy的调和平均数:19.7015

请继续输入:

q

 

  程序在出现问题时将引发异常。throw语句实际上是跳转,即命令程序跳转到另一条语句。throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

  程序使用异常处理程序来捕获异常,异常处理程序位于要处理问题的程序中。catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起来的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳转到这个位置执行。异常处理程序也被称为catch块。

  try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起来的代码块,表示需要注意这些代码引发的异常。

  4、将对象用作异常类型

  通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch块可以根据这些信息来决定采取什么样的措施。

 1 #include <stdio.h>
 2 #include <iostream>
 3 
 4 class bad_hmean{
 5 private:
 6     double v1;
 7     double v2;
 8 public:
 9     bad_hmean(int a = 0, int b = 0):v1(a),v2(b){};
10     void mesg();
11 };
12 inline void bad_hmean::mesg(){
13     std::cout << "hmean("<< v1 <<", "<< v2 << "):"<<"invalid arguments: a = -b\n";
14 }
15 class bad_gmean{
16 public:
17     double v1;
18     double v2;
19     bad_gmean(double a = 0, double b = 0):v1(a),v2(b){}
20     const char * mesg();
21 };
22 inline const char * bad_gmean::mesg(){
23     return "gmean() arguments should be >= 0\n";
24 }
#include <iostream>
#include <cmath>
#include "Bad_hmean.h"

using namespace std;
double hmean(double a, double b);
double gmean(double a, double b);
int main(int argc, const char * argv[]) {
    double x,y,z;
    cout << "请输入两个数字:\n";
    while (cin >> x >> y) {
        try {
            z = hmean(x, y);
            cout << "x和y的调和平均数:" << z << endl;
            cout << "x和y的几何平均数:" << gmean(x,y) << endl;
        } catch (bad_hmean & h) {
            h.mesg();
            cout << "重新输入.\n";
            continue;
        }
        catch (bad_gmean & g){
            g.mesg();
            cout << "输入的值:" << g.v1 << "," << g.v2 << endl;
            cout <<"你输入的数字有误;\n";
            break;
        }
    }
    return 0;
}
double hmean(double a, double b){
    if (a == -b) {
        throw bad_hmean(a,b);
    }
    return 2*a*b/(a+b);
}
double gmean(double a, double b){
    if (a < 0 || b < 0) {
        throw bad_gmean(a,b);
    }
    return sqrt(a*b);
}

输出结果:
请输入两个数字:
1212
hmean(12, -12):invalid arguments: a = -b
重新输入.
12
12
x和y的调和平均数:12
x和y的几何平均数:12
1245
x和y的调和平均数:32.7273
x和y的几何平均数:输入的值:12,-45
你输入的数字有误;

  

  5、异常规范和C++11

  有时候,一种理念看似有前途,但实际的使用效果并不好。一个这样的例子是异常规范,这是C++98新增的一项功能,但C++11却将其摒弃了。这意味着C++11仍然处于标准之中,但以后可能会从标准中剔除,因此不建议使用它。

  然而,忽略异常规范前,至少应该知道它是什么样的,如下所示:

    double harm(double a) throw(bad_thing);//可能会抛出bad_thing异常

    double marm(double) throw();//不会抛出异常

  其中的throw()部分就是异常规范,它可能出现在函数原型和函数定义中,可包含类型列表,也可不包含。

  异常规范的作用之一是,告诉用户可能需要try块。然而,这项工作也可使用注释轻松完成。异常规范的另一个作用是,让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。这很难检查。例如,marm()可能不会引发异常,但它可能调用一个函数,而这个函数调用的另一个函数引发了异常。另外,给函数编写代码时它不会引发异常,但库更新后他却会引发异常。总之,最好不要使用这项功能。而C++11建议最好忽略异常规范。

  然而,C++11确实支持一中特殊的异常规范:可使用新增的关键字noexcept指出函数不会引发异常:

    double marm() noexcept;//marm()不会抛出异常

  有关这种异常规范是否必要和有用存在一些争议,有些人认为最好不要使用它(至少在大多数情况下如此);而有些人认为引入这个新关键字很有必要,理由是知道函数不会引发异常有助于编译器优化代码。通过使用这个关键字,编写函数的程序员相当于作出了承诺。

  还有运算符noexcept(),它判断操作数是否会引发异常。

  6、栈解退

  假设try块并没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这涉及到栈解退,下面进行讨论。

  首先来看一看C++通常是如何处理函数调用和返回的。C++通常通过将信息放在栈中来处理函数调用。具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到栈中。在栈中,这些函数参数被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,以此类推。当函数结束时,程序流将跳转到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,以此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数(如果有的话)将被调用。

  现在假设函数由于出现异常(而不是由于返回)终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

使用return:

  

使用throw:

  7、其他异常特性

  虽然throw-catch机制类似于函数参数和函数返回机制,但还是有些不同之处。其中之一是函数fun()中的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函:包含能够捕获相应异常的try-catch组合。

  另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

  提示:如果有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

  通过正确地排列catch块的顺序,使能够在如何处理异常方面有选择的余地。然而,有时候可能不知道会发生哪些异常;在这种情况下,仍能够捕获异常,即使不知道异常的类型。方法是使用省略号来表示异常类型,从而捕获任何异常:

    catch(...){//statements}

  如果知道一些可能会引发的异常,可以将上述捕获所有异常的catch块放在最后面,这有点类似于switch语句中的default。

  可以创建捕获对象而不是引用的处理程序。在catch语句中使用基类对象时,将捕获所有的派生类对象,但派生类特性将被剥去,因此将使用虚方法的基类版本。

  8、exception类

  较新的C++编译器将异常合并到语言中。例如,为支持该语言,exception头文件定义了exception类,C++可以把它用作其他异常类的基类。代码可以引发exception异常,也可以将exception类用作基类。有一个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而已。然而,由于这是一个虚方法,因此可以在从exception派生而来的类中从心定义它:

    #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 argument to gmean"; }

      ....

    };

  如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们:

    try{

    ...

    }

    catch(std::exception & e){

      std::cout << e.what() << endl;

    ...

    }

  否则,可以分别捕获它们。

  C++库定义了很多基于exception的异常类型。

  (1)stdexcept异常类

  头文件stdxecept定义了其他几个派生类。首先,该文件定义了logic_error和runtime_error类,它们都是以公有方式从exception派生而来的:

    class logic_error:public exception{

    public:
      explicit logic_error(const string & what_arg);

      ....

    };

    class domain_error:public exceptriong{

    public:
      explicit domain_error(const string& what_arg);

    ......

    };

  注意,这些类的构造函数接受一个string对象作为参数,该参数提供了方法what()以C-风格字符串方式返回的字符数据。

  这两个新类被用作两个派生系列的基类。异常系列logic_error描述了典型的逻辑错误。总体而言,通过合理的编程可以避免这种错误,但实际上这些错误还是可能发生的。每个类的名称指出了它用于报告的错误类型:

  * domain_error;

  * invalid_argument;

  * length_error;

  * out_of_bounds;

  每个类有一个类似于logic_error的构造函数,可以提供一个供方法what()返回的字符串。  

  异常domain_error用于返回参数不在定义域内的错误。

  异常invalid_argument指出给函数传递了一个意料外的值。

  异常length_error用于指出没有足够的空间来执行所需的操作。

  异常out_of_bounds通常用于指示索引错误。

  

  接下来,rntime_error异常系列描述了可能在运行期间发生但难以预计和防范的错误。每个类的名称指出了它用于报告的错误类型:
  * range_error;

  * overflow_error;

  * underflow_error。

  每个类独有一个类似于runtime_error的构造函数,让您能够提供一个供方法what()返回的字符串。

  下溢(underflow)错误在浮点数计算中。一般而言,存在浮点数类型可以表示的最小非零值,计算结果比这个值还小时将导致下溢错误。整型和浮点型都可能发生上溢错误,当计算结果超出了某种类型能够表示的最大数量级时,将发生上溢错误。计算结果可能不在函数允许的范围之内,但没有发生上溢或下溢错误,在这种情况下,可以使用range_error异常。

  一般而言,logic_error系列错误表明存在可以通过编程修复的错误,而runtime_error系列错误表明存在无法避免的问题。所有这些错误类有相同的常规特征,它们之间的主要区别在于:不同的类名能够分别处理每种异常。另一方面,继承关系使我们可以一起处理它们。

  (2)bad_alloc异常和new

  对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含了bad_alloc类的声明,它是从exception类公有派生而来的。

  

  9、异常、类和继承

  异常、类和继承以三种方式相互关联。首先,可以像标准C++库所做的那样,从一个异常类派生出另一个;其次,可以在类定义中嵌套异常类声明来组合异常;第三,这种嵌套声明本身可以被继承,还可以用作基类。

  10、异常何时会迷失方向

  异常被引发后,有两种情况下会导致问题。首先,如果他是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及派生类的对象匹配),否则称为意外异常。在默认情况下,这将导致程序异常终止(虽然C++11摒弃了异常规范,但仍支持它,且有些现有的代码使用了它)。如果异常不是在函数中引发的(或者函数没有异常规范),则必须捕获它。如果没有捕获(在没有try块或没有匹配的catch块时,将出现这种情况),则异常被称为未捕获异常。在默认情况下,这将导致程序异常终止。然而,可以修改程序对意外异常和未捕获异常的反应。下面来看如何修改,先从未捕获异常开始。

  未捕获的异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。在默认情况下,terminate()调用abort()函数。可以指定terminate()应调用的函数(而不是abort())来修改terminate()的这种行为。为此,可调用set_terminate()函数。set_terminate()和terminate()是在头文件exception中声明的:

    typedef void (*terminate_handler)();

    terminate_handler set_terminate(terminate_handler f) throw();//C++98

    terminate_handler set_terminate(terminate_hander f) noexcept;//C++11

    void terminate();//C++98

    void terminate() noxecept;//C++11

  其中typedef使terminate_handler成为这样一种类型的名称:指向没有参数和返回值的函数的指针。set_terminate()函数将不带任何参数且返回类型为void的函数的名称(地址)作为参数,并返回该函数的地址。如果调用了set_terminate()函数多次,则terminate()将调用最后一次set_terminate()调用设置的函数。

  来看一个例子。假设希望未捕获的异常导致程序打印一条消息,然后调用exit()函数,将退出状态设置为5 。首先,请包含头文件exception。可以使用using编译指令、适当的using声明或std::限定符,来使其声明可用。

    #include <exception>

    using namespace std;

    然后,设计一个完成上述两种操作所需的函数,其原型如下:

    void myQuit(){

      cout << "Terminating due to uncaught exception\n";

      exit(5);

    }

    最后,在程序的开头,将终止操作指定为调用该函数。

    set_terminate(myQuit);

  现在,如果引发了一个异常且没有被捕获,程序将调用terminate(),而后者将调用myQuit()。

  接下来看意外异常。通过给函数指定异常规范,可以让函数的用户知道要捕获那些异常。假设函数的原型如下:  

    double Argh(double,double)  throw(out_of_bounds);

    则可以这样使用函数:

    try{

      x = Argh(a,b);

    }

    catch(out_of_bounds & ex){

      ....

    }

    知道应捕获哪些异常很有帮助的,因为默认情况下,未捕获的异常将导致程序异常终止。

  原则上,异常规范应包含调用的其他函数引发的异常。例如,如果Argh()调用了Duh()函数,而后者可能引发retort对象异常,则Argh()和Duh()的异常规范中都应包含retort。除非自己编写所有的函数,并且特别仔细,否则无法保证上述工作都已经正确完成。

  如果函数引发了其异常规范中没有的异常,行为与未捕获的异常及其类似。如果发生意外异常,程序将调用unexpected()函数。这个函数将调用terminate()函数,后者在默认情况下将调用abort()函数。正如有一个可用于修改terminate()的行为的set_terminate()函数一样,也有一个可用于修改unexcepected()的行为的set_unexpected()函数。这些新函数也是在头文件exception中声明的:

  typedef void (*unexpexted_handler)();

  unexpected_handler set_unexpected(unexpected_handler f) throw();//C++98

  unexpected_handler set_unexpected(unexpected_handler f) noexcept;//C++11

  void unexpected();//C++98

  void unexpected() noexcept;//C++11

  然而,与提供给set_terminate()的函数的行为相比,提供给set_unexpected()的函数的行为受到更严格的限制。具体地说,unexpected_handler函数可以:
    *通过调用terminate()(默认行为)、abort()或exit()来终止程序;

    *引发异常。

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

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

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

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

  总之,如果要捕获所有的异常(不管是预期的异常还是意外异常),则可以这样做:

  首先,确保异常头文件的声明可用:

    #include<exception>

    using namespace std;

  然后,设计一个替代函数,将意外异常转换为bad_exception异常,该函数的原型如下:

    void myUnexpected(){

      throw std::bad_exceprion();//或者只是throw

    }

  仅使用throw,而不指定异常将导致重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被bad_exception对象取代。

  接下来则程序开始的位置,将意外异常操作指定为调用该函数:

    set_unexpected(myUnexpectd);

  最后,将bad_exception类型包含在异常规范中,并添加如下catch块序列:

    double Argh(double,double) throw(out_of_bounds, bad_exception);

    ...

    try{

      x = Argh(a,b);

    }

    catch(out_of_bouds & ex){

      ...

    }

    catch(bad_exception & ex){

       ....

    }

  11、有关异常的注意事项

  从前面关于如何使用异常的讨论可知,应在设计程序时就加入异常处理功能,而不是以后再添加。这样做有些缺点。例如,使用异常会增加程序代码,降低程序的运行速度。异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。异常和动态内存分配并非总能协同工作。

  下面进一步讨论动态内存分配和异常。首先,请看下面的函数:

  void test1(int n){

    string mesg("你好!");

    ...

    if(oh_no)

      throw exception();

    ....

    return;

  }  

  string类采用动态内存分配。通常,当函数结束时,将为mesg调用string的析构函数。虽然throw语句过早地终止了函数,但它仍然使得析构函数被调用,这要归功于栈解退。因此在这里,内存被正确地管理。

  接下来看下面这个函数:

  void test2(int n){

    double * ar = new double[n];

    ...

    if(oh_no)

      throw exception();

    ....

    delete [] ar;

    return; 

  }

  这里有个问题。解退栈时,将删除栈中的变量ar。但函数过早的终止意味着函数末尾的delete[]语句被忽略。指针消失了,但它指向的内存块未被释放,并且不可访问。总之,这些内存被泄漏了。

  这种泄漏是可以避免的。例如,可以在引发异常的函数中捕获该异常,在catch块中包含一些清理代码,然后重新引发异常:

  void test3(int n){

    double * ar = new double[n];

    ....

    try{

      if(oh_no)

         throw exception();

    }

    catch(exception & ex){

      delete [] ar;

      throw;

    }

    ....

    delete [] ar;

    return;

  }

  然而,这将增加疏忽和产生其他错误的机会。

  总之,虽然异常处理对于某些项目极为重要,但它也会增加编程的工作量、增大程序、降低程序的速度。另一方面,不进行错误检查的代价可能非常高。

 

四、RTTI

  RTTI是运行阶段类型识别(Runtime Type Identification)的简称。这是新添加到C++中的特性之一,很多老式实现不支持。另一些实现可能包含开关RTTI的编译器设置。RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。很多类库已经为其提供了实现这种功能的方式,但由于C++内部并不支持,因此各个厂商的机制通常互不兼容。创建一种RTTI语言标准将使得未来的库能够彼此兼容。

  1、RTTI的用途

  假设有一个类层次结构,其中的类都是从同一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。这样便可以调用这样的函数:在处理一些信息后,选择一个类,并创建这种类型的对象,然后返回它的地址,而该地址可以被赋给基类指针。如何知道指针指向的是哪种对象呢?

  在回答这个问题之前,先考虑为何要知道类型。可能希望调用类方法的正确版本,在这种情况下,只要知道该函数是类层次结构中所有成员都拥有的虚函数,则并不需要真正知道对象的类型。但派生对象可能包含不是继承而来的方法,在这种情况下,只有某些类型的对象可以使用该方法。也可能是出于调试目的,想跟踪生成的对象的类型。对于后两者情况,RTTI提供解决方案。

  2、RTTI的工作原理

  C++有三个支持RTTI的元素。

  *如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0——空指针。

  *typeid运算符返回一个指出对象的类型的值。

  *type_info结构存储了有关特定类型的信息。

  只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生类对象的地址赋给基类指针。

  警告:RTTI只适用于包含虚函数的类。

  (1)dynamic_cast运算符

  dynamic_cast运算符是最常用的RTTI组件,它不能回答“指针指向的是哪类对象”这样的问题,但能回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。假设有下面的类层次结构:
    class A{//拥有虚方法};

    class B:public A{....};

    class C:public B{....};

  接下来假设有下面的指针:

    A * pa = new A;

    A * pb = new B;

    A * pc = new C;

  最后对于下面的类型转换:

    C * p1 = (C * ) pc;//#1

    C * p2 = (C *) pa;//#2

    B * p3 = (C *) pc;//#3

  哪些是安全的?根据类声明,它们可能全都是安全的,但只有那些指针类型与对象的类型(或对象的直接或间接基类的类型)相同的类型转换才一定是安全的。像上面的转换,只有#1和#3是安全的,#2是不安全的。因为#2把指向基类对象的地址赋给派生类指针,因此程序将期望基类对象用派生类的特征,而通常这是不可能的。

  注意,与问题“指针指向的是那种类型的对象”相比,问题“类型转换是否安全”更通用,也更有用。通常想知道类型的原因在于:知道类型后,就可以知道调用特定的方法是否安全。要调用方法,类型并不一定要完全匹配,而可以是定义了虚拟版本的基类类型。

  下面来看一下dynamic_cast的语法。该运算符的用法如下,其中pb指向一个对象:

    B * ptr = dynamic_cast<B*>(pb);

  这提出了这样的问题:指针pb的类型能否被安全地转换为B*?如果可以,运算符将返回对象的地址,否则返回一个空指针。

  注意:通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Typr类型的指针:

    dynamic_cast<Type *>(pt)

  否则,结果为0,即空指针。

  也可以将dynamic_cast用于引用,其用法稍微有点不同:没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生来的,他实在头文件typeinfo中定义的。因此可以像下面这样使用该运算符,其中rg是对A的引用:

    #include<typeinfo>

    ...

    try{

      B & rs = dynamic_cast<B &>(rg);

      ...

    }

    catch(bad_cast &){

      ....

    };

  (2)typeid运算符和type_info类

  typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参数:

  *类名;

  *结果为对象的表达式

  typeid运算符返回一个对type_info对象的引用,其中,type_info是在头文件typeinfo中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。例如,如果pg指向的是一个Type类对象的地址,则下面的表达式的结果为bool值true,否则为false:

    typeid(Type) == typeid(*pg)

  如果pg是一个空指针,程序将会引发bad_typeid异常。该异常是从exception类派生来的,是在头文件typeinfo中声明的。

  type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类的名称。

 

五、类型转换运算符

  下面的4个类型转换运算符,使得转换过程更规范:  

  *dynamic_cast;

  *const_cast;

  *static_cast;

  *reinterpret_cast.

  可以根据目的选择一个合适的运算符,而不是使用通用的类型转换。这指出了进行类型转换的原因,并让编译器能够检查程序的行为是否与设计者想法吻合。

  (1)dynamic_cast

  dynamic_cast运算符已经在前面介绍过了。总之,假设High和Low是两个类,而ph和pl的类型分别为High*和Low*,则仅当Low是High的可访问基类(直接或间接)时,下面的语句才将一个Low*指针赋给pl:

    pl = dynamic<Low *>ph;

  否则,该语句将空指针赋给pl。通常,该运算符的语法如下:

    dynamic_cast<type_name>(expression)

    该运算符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换。

  (2)const_cast

  const_cast运算符用于执行只有一种用途的类型转换,即改变值为const或volatile,其语法与dynamic_cast运算符相同:

    const_cast<type_name>(expression)

  如果类型的其他方面也被修改,则上述类型转换将出错。也就是说,除了const或volatile特征(有或无)可以不同外,type_name和expression的类型必须相同。再次假设High和Low是两个类:

    High bar;

    const High *pbar = &bar;

      ...

    High * pb = const_cast<High *> (pbar);//允许

    const Low * pl = const_cast<const Low *> (pbar);//不允许

  提供该运算符的原因是,有时候可能需要这样一个值,它在大多数时候是常量,而有时又是可以修改的。在这种情况下,可以将这个值声明为const,并在需要修改它的时候,使用const_cast。这也可以通过类型转换来实现,但通用转换也可能同时改变类型:

    High bar;

    const High * pbar = &bar;

      ...

    High *pb = (High *) (pbar);//允许

    Low *pl = (Low *) (pbar);//同样允许

   由于编程时可能无意间同时修改类型和常量特征,因此使用const_cast运算符更安全。

  const_cast不是万能的。它可以修改指向一个值得指针,但修改const值的结果是不确定的。

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void change(const int * pt, int n);
 6 
 7 int main(int argc, const char * argv[]) {
 8     int num1 = 1000;
 9     const int num2 = 123;
10     cout << "num1,num2 : " << num1 << "," << num2 << endl;
11     change(&num1, -10);
12     change(&num2, -10);
13     cout << "num1,num2 : "<< num1 << "," << num2 << endl;
14     return 0;
15 }
16 void change(const int * pt, int n){
17     int * pc;
18     
19     pc = const_cast<int *>(pt);
20     *pc += n;
21 }
22 
23 输出结果:
24 num1,num2 : 1000,123
25 num1,num2 : 990,123

  上面的代码说明,如果想通过删除const特性的指针来修改指针指向的值,当且仅当指向的值不是const时才可行。

  (3)static_cast

  static_cast运算符的语法与其他类型运算符相同:

    static_cast<type_name>(expression)

  仅当type_name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,上述转换才是合法的,否则将出错。假设High是Low的基类,而Pond是一个无关的类,则从High到Low的转换、从Low到High的转换都是合法的,而从Low到Pond的转换是不允许的:

    High bar;

    Low blow;

    ...

    High * pb = static_cast<High *>(& blow);//允许,向上转换

    Low * pl =  static_cast<Low *> (& bar);//允许,向下转换

    Pond *pmer = static_cast<Pond *>(& blow);//不允许

  第一种转换是合法的,因为向上转换可以隐式地进行。第二种转换是从基类指针到派生类指针,在不进行显示类型转换的情况下,将无法进行。但由于无需进行类型转换,便可以进行另一个方向的类型转换,因此使用static_cast来进行向下转换是合法的。

  同理,由于无需进行类型转换,枚举值就可以转换为整型,所以可以使用static_cast将整型转换为枚举值。同样,可以使用static_cast将double转换为int、将float转换为long以及其他各种数值转换。

  (4)reinterpret_cast

  reinterpret_cast运算符用于天生危险的类型转换。它不允许删除const,但会执行其他令人生厌的操作。有时程序员必须做一些依赖于实现的、令人生厌的操作,使用reinterpret_cast运算符可以简化对这种行为的跟踪工作。该运算符的语法与另外3个相同:

    reinterpret_cast <type_name> (expression)

  下面是使用示例:

    struct dat{short a;short b;};

    long value = 0xA224B118;

    dat * pd = reinterpret_cast <dat *>(&value);

    cout << hex << pd->a;//展示value的前面两个字节的内容

  通常,这样的转换适用于依赖于实现的底层编程技术,是不可移植的。

  然而,reinterpret_cast运算符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是,不能将函数指针转换为数据指针,反之亦然。

  在C++中,普通类型转换也受到限制。基本上,可以执行其他类型转换可执行的操作,加上一些组合,如static_cast或reinterpret_cast后跟const_cast,但不能执行其他转换。 

 

 

 

  

 

posted @ 2015-07-25 11:22  木缥缈  阅读(523)  评论(0编辑  收藏  举报