友元类、嵌套类、异常、运行阶段类型识别(RTTI)、类型转换运算符dynamic_cast, static_cast, const_cast, reiterpret_cast

1.友元类

1)格式 p489

friend class ClassName;

友元类的所有方法都可以访问原始类的私有成员和保护成员。

2)例子:Tv 类和 Remote (遥控器)类

将 Remote 类声明为 Tv 类的友元类;这同时意味着 Remote 类中有关于 Tv 类的代码,因此编译器必须先了解 Tv 类后,才能处理 Remote 类,因此先定义 Tv 类:

class Tv
{
public:
    friend class Remote; // Remote can access Tv private parts
    ...
private:
    int channel;
    ...
};

class Remote
{
public:
    void set_chan(Tv & t, int c) {t.channel = c;} // Remote 类中可以直接访问 Tv 类的私有成员
    ...
};

3)可以选择让特定的类成员成为另一个类的友元,而不必让整个类成为友元 -- 使用前向声明(forward declaration)p492

在上述例子中,唯一直接访问 Tv 成员的 Remote 方法是 Remote::set_chan(),因此它是唯一需要作为友元的方法;可以将该函数设置为 Tv 类的友元,但需要注意各种声明和定义的顺序:

让 Remote::set_chan() 成为 Tv 类的友元,就需要在 Tv 类中将其声明为友元:

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

要是编译器能够处理这条语句,编译器必须知道 Remote 类的定义,因此 Remote 类的定义需要放在 Tv 类的前面;

但是 Remote 类中的 set_chan() 方法用到了 Tv 类中的成员 channel,因此 Tv 类也应在 Remote 类之前定义;

避开这种循环依赖的方法是使用前向声明,在 Remote 定义的前面插入下面的语句:

class Tv; //froware declaration
class Remote {...};
class Tv {...};

注意,不能使用这样的顺序:

class Remote; //forward declaration
class Tv {...};
class Remote {...};

因为编译器在 Tv 类的声明中看到 Remote 的 set_chan() 方法被声明为 Tv 类的友元之前,应该先看到 Remote 类的声明和 set_chan() 方法的声明

使用正确的顺序后,还需要注意:

如果此时 Remote 类中有 内联方法代码:

class Remote
{
...
public:
    void onoff(Tv & t) {t.onoff();}
    void set_chan(Tv & t, int c) {t.channel = c;}
...
};

 这将调用 Tv 的 onoff() 方法以及使用 Tv 的私有成员 channel,所以此时编译器必须已经看到了 Tv 类的声明,这样才能知道 Tv 有哪些方法和成员,但是 Tv 类的声明在 Remote 类的声明的后面;

解决该问题的方法是,使 Remote 声明中只包含方法声明,并将实际的定义放在 Tv 类之后;这样,排列顺序如下:

class Tv;
class Remote //仅包含方法的声明
{
...
public:
    void onoff(Tv & t);
    void set_channel(Tv & t, int c);
...
};
class Tv {...};
//将 Remote 类中方法的定义放在此处,通过在方法定义中使用 inline 关键字,仍然可以使其成为内联方法

注意,让整个 Remote 类成员友元并不需要前向声明,因为友元语句本身已经指出 Remote 是一个类:p493

friend class Remote;

4)两个类互为友元类 p494

5)某个成员为两个类共同的友元 p495

 

2.嵌套类 p495

C++ 中可以将类声明放在另一个类中;在另一个类中声明的类被称为嵌套类。

包含类 的成员函数可以创建和使用被嵌套类的对象;仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

对类进行嵌套不创建类成员,仅仅是定义了一种类型,该类型(如果不是在共有部分声明)仅在包含嵌套类声明的类总有效。

 

3.异常

1)若出现了除以 0 等异常情况,编译器的处理方法:p500

  • 调用 abort() 函数。abort() 函数原型位于头文件 csdlib(或 stdlib.h)中,其典型实现是向标准错误流(即 cerr 使用的错误流)发送消息 abnormal program termination,然后终止程序;调用 abort() 函数时将直接终止程序,而不是先返回到 main()。
  • 返回错误码。
  • 异常机制

2)异常机制

异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常的处理有 3 个组成部分:

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

其中:

try 块标识后可跟一个多个 catch 块。 p502

throw 关键字表示引发异常,紧跟随其后的值(如字符串或对象)指出了异常的特征,如 p502

int main()
{
    ...
    try
    {
        z = hmean(x, y);
    }
    catch (const char * s) // 将 throw 后的字符串的地址赋给指针 s
    {
        cout << s <<endl;
        ...
    }
    ...
    double hmean(double a, double b)
    {
        if (a == -b)
            throw "bad hmean() arguments: a = -b not allowed"; 
        return 2.0 * a * b / (a + b);
    }
    ...
}

异常类型可以是字符串,或者是其他 C++ 类型;通常为类类型。 p503

注意,throw 不是将控制权返回给调用函数,而是导致程序沿函数调用序列后退,直到找到包含 try 块的函数。在上述列子中,throw 将程序控制权返回给 main();程序将在 main() 中寻找与引发的异常类型匹配的异常处理程序(位于 try 块后面的 catch 块)。p503

如果函数引发了异常,但没有 try 块或没有匹配的处理程序时,程序最终将调用 abort() 函数。

3)栈解退 p506

在处理函数调用时,程序将调用函数的指令的地址(返回地址)放在栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。

如果函数由于出现了异常而终止,程序将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,知道找到一个位于 try 块中的返回地址;随后,控制权将转到块尾的异常处理程序。这个过程被成为栈解退。

栈解退机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用

函数返回仅仅处理该函数放在栈中的对象,throw 语句处理 try 块和 throw 之间整个函数调用序列放在栈中的对象

4)throw 语句返回控制权 p510

throw 语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的 try-catch 组合

此外

try
{
    hm = hmean(a, b);
    ...
}
catch (bad_hmean & bg)
{
    bg.mesg();
    cout << "Cayght in means() \n";
    throw; //rethrows the exception in hmean(a, b)
}

catch 中的 throw; 语句用来将捕获到的 hmean(a, b) 中的异常再次向上传递。p509

5)

引发异常时编译器总是创建一个临时拷贝。p510

class problem {...};
...
void super() throw (problem) //异常规范 p506
{
    ...
    if (oh_no)
    {
        problem oops;
        throw oops; //引发异常,编译器创建一个临时拷贝
    ...
    }
...
try
{
    super();
}
catch (problem & p) // p 指向 oops 的副本而不是 oop 本身
{
    ...
}

catch 后面使用 problem & p 而不是 problem p 的原因是,基类引用可以指向派生类对象。因此使用基类引用能够捕获任何异常对象。

6)exception 类 p511

exception 头文件定义了 exception 类,可以把它用作其他异常类的基类。exception 类中有一个名为 what() 的虚函数,它返回一个字符串,该字符串的特征随实现而异;因为它是一个虚函数,因此可以在从 exception 类派生而来的类中重新定义它:

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

基于 exception 的异常类型:

a. stdexcept 异常类 p512

头文件 stdexcept 定义了一些异常类;首先,该文件定义了 logic_error 和 runtime_error 类,它们都是以公有方式从 exception 派生而来的

class logic_error : public exception
{
public:
    explicit logic_error(const string & what_arg);
...
};

class domain_error : public logic_error 
{
public:
    expolicit domain_error(const string & what_arg);
...
};

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

异常类 logic_error 描述了典型的逻辑错误。logic_error 派生的每个类的名称指出了它们用于报告的错误类型

  • domain_error
  • invalid_argument
  • length_error
  • out_of_bounds

每个类都有一个类似于 logic_error 的构造函数,使得我们能够提供一个供方法 what() 返回的字符串。

runtime_error 类派生的每个类描述了运行期间发生的错误

  • range_error
  • overflow_error
  • underflow_error

b. bad_alloc 异常和 new p513

对于使用 new 导致的内存分配问题,C++ 的最新处理方式是让 new 引发 bad_alloc 异常。

c. 空指针 和 new p514

C++ 标准也提供了一种在 new 导致的内存分配问题 失败时返回空指针的 new。

7)异常、类和继承 p514

8)意外异常(带异常规范的函数产生的异常与规范列表中的异常不匹配)和未捕获异常 p517

9)动态分配和异常 p519

void test2(int n)
{
    double * ar = new double(n);
    ...
    if(oh_no)
        throw exception();
    ...
    delete [] ar;
    return;
}

上述代码中,进行栈解退时,将删除栈中的变量 ar;但函数过早地终止意味着函数末尾的 delete[] 语句不会被执行,因此 ar 指向的内存块并未被释放,产生了内存泄漏。可以进行如下改进:

void test3(int n)
{
    double * ar = new double(n);
   ...
    try
    {
        if (oh_no)
            throw exception();
    }
    catch (exception & ex)
    {
    delete [] ar;
    throw; //重新 throw 原异常
    }
    ...
    delete [] ar;
    return;

另一种方法是使用 智能指针模板(ch16)。

 

4.运行阶段类型识别(Runtime Type Identification)p520

C++ 中支持 RTTI 的 3 个元素:

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

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

1)dynamic_cast 运算符

dynamic_cast 运算符能够回答“是否可以安全地将对象的地址赋给特定类型的指针”。

语法:

Superb * bm = dynamic_cast<Superb *>(pg);

其中 pg 指向一个对象。

上述语句表明:指针 pg 的类型是否可以安全地被转换为 Superb *? 如果可以,运算符将返回对象的地址,否则返回一个空指针。

例如,对于该语句:

dynamic_cast<Type *>(pt);

通常,如果指向的对象(*pt)的类型为 Type 或者是从 Type 直接或间接派生而来的类型,则上面的表达式将指针pt转换为 Tpye 类型的指针;否则,结果为0,即空指针。

也可以将 dynamic_cast 用于引用,例如:

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

但没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast 将引发类型为 bad_cast 的异常;该异常是从 exception 类派生而来的,它是在头文件 typeinfo 中被定义的。 p523

2)typeid 运算符和 type_info 类 p524

typeid 运算符能够确定两个对象是否为同种类型,它可以接受两种参数:

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

typeid 运算符返回一个对 type_info 对象的引用,而 type_info 类重载了 == 和 != 运算符,因此可以使用这些运算符来对类型进行比较。

例如,如果 pg 指向的是一个 Magnificent 对象,则下述表达式的结果为 bool 值 true,否则为 false:

typeid(Magnificent) == typeid(*pg);

如果 pg 指向的是一个空指针,程序将引发 bad_typeid 异常。

type_info 类包含一个 name() 成员,该函数返回一个随实现而异的字符串:通常是类的名称。如,下面的语句显式指针 pg 指向的对象所属的类定义的字符串

cout << "Now processing type " << typeid(*pg).name() << ".\n";

 

5.类型转换运算符 p526

  • dynamic_cast
  • const_cast
  • static_cast
  • reinterpret_cast

 

posted @ 2022-05-08 20:17  SanFranciscoo  阅读(56)  评论(0编辑  收藏  举报