c++ primer plus chapter15 友元、异常和其他

友元类:既不是is-a关系,也不是has-a关系,比如电视和遥控。遥控可以改变电视的状态,因此应该把遥控作为电视的友元。友元类声明可以在public、protected、private部分,位置没有影响。若B是A的友元类,那么编译器需要先了解A类然后才能处理B类,方法是先定义A类,或者使用前向声明。一个例子如下:

class Tv
{
friend class Remote;
};

class Remote
{
};

Remote类可以访问Tv类的私有成员。有时候Remote只有部分成员需要访问Tv类的私有成员,这时候可以如下编码:

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

这样声明Remote类的set_chan函数是Tv类的友元,但是有一个问题是,编译器需要知道Remote的定义才能在Tv类中处理这个友元函数,因此需要把Remote类定义放在Tv类的前面,然而Remote类的方法提到了Tv类对象,这就意味着需要把Tv类放到前面,解决这个问题的方案是使用前向声明。排列顺序如下:

class Tv;
class Remote{};
class Tv{};

不能按照如下顺序:

class Remote;
class Tv{};
class Remote{};

因为编译器处理到Tv类的友元的时候,除了需要看到Remote类的声明,还要看到该函数的声明,而这个声明是在Remote类定义中提供的。还有一个问题,若Remote类中有如下这样的内联函数:

void func(Tv &t){t.lala();}

此时编译器需要看到Tv类中lala()的声明,但是此声明是在Tv类定义中提供的,这个问题只能通过编码解决,因为即使类定义中只有声明,也可以在定义的时候加inline使其成为内联函数。

还要注意,Remote类中的函数定义必须在Tv类定义后面给出,因为此时需要Tv类中定义的方法。

让Remote类成为Tv类的友元的时候,不需要将Remote类前向声明的原因是,友元语句已经说明了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)
{
}

其中,Tv类的buzz方法使用了Remote对象,可以在Remote类定义之前声明,因为通过友元已经知道Remote是一个类,但是定义必须在Remote的方法定义之后。

共同的友元:这种友元需要使用的场景是,一个函数需要访问两个类的私有数据,这个函数显然不能作为两个类的成员函数。可以作为一个类的成员函数,另一个类的友元,但是一般将其作为两个类的友元。此时按照如下顺序书写:

class B;

class A
{
friend void func(A & a, B & b);
};

class B
{
friend void func(A & a, B & b);
};

inline void func(A & a, B & b)
{
}

嵌套类:和包含的不同是,包含是将另一个类的对象作为自己的数据成员,而嵌套是在本类内部定义另一个类,如下:

class Queue
{
private:
    class Node
    {
    public:
        Item item;
        Node * next;
        Node(const Item & i) : item(i), next(0){}
    };
};

若在方法文件中定义构造函数,需要两次作用域解析运算符,如下:

Queue::Node::Node(const Item &i) : item(i), next(0){}

嵌套类作用域:若嵌套类是在private部分定义的,那么只有后者知道它;若在protected部分定义,那么外部不可见,但是派生类可以直接创建这种类型的对象;若在public部分定义,那么外部可见,但要使用类限定符。

嵌套类访问规则:对嵌套类的访问规则和常规类一样,即使Queue类,也只能显式访问Node类的public成员,因此将Node类的数据成员定义成public的,由于Node在Queue内部,因此对外部世界来说即使是public也是不可见的。

abort():在头文件cstdlib中提供,功能是终止程序,发送标准错误流,是否刷新文件缓冲区因实现而异

exit():和abort()类似,刷新文件缓冲区,但是不显示消息。

异常机制:c++异常机制有3个组成部分:

1.引发异常

2.使用处理程序捕获异常

3.使用try块

一个标准的模板如下:

try {
    throw "error";
}
catch (const char * s)
{
    cout << s << endl;
}

try语句中throw用于抛出异常,catch格式类似函数,用括号接收throw抛出的类型。和label不同,若try中没有引发异常,那么catch语句块中的内容不会被执行。通过throw引发异常之后沿着调用栈后退,找到第一个try块,然后向下找catch语句。可以有catch语句,捕获不同类型的异常,一般参数使用对象实现的,即不同的错误类。

一个使用对象作为异常类型的例子:

class Test1
{
private:
    int i;
public:
    Test1(int _i = 0) : i(_i){}
    void func(){cout << i << endl;}
};

class Test2
{
private:
    string s;
public:
    Test2(string _s = "none") : s(_s){}
    void func(){cout << s << endl;}
};

try {
    if (...)
        throw Test1(10);
    if (...)
        throw Test2("error");
}
catch (Test1 & t)
{
    t.func();
}
catch (Test2 & t)
{
    t.func();
}

栈解退:try中涉及多层函数调用的时候,引发异常,发生栈解退,找到第一个try块,在这个过程中,所有被解退的函数中的类对象都会被自动析构。

关于抛出对象:throw一个对象的时候,编译器总是创造这个对象的副本,即使catch要捕获的是引用,因为若try中的函数被解退了,那么这个对象就不复存在了。catch语句中捕获引用的好处是,可以使用基类对象的引用,当定义一个派生链的异常类的时候,可以在最后的catch语句中使用基类的引用,捕获所有异常,即按照派生链相反的顺序排列catch语句。

exception类:在头文件<exception>中提供,是所有异常类的基类,有一个what()的虚方法,返回一个字符串,这个方法可以在派生类中重写。c++库提供了一些从exception类中派生而来的常用异常类:

1.stdexcept类,在头文件stdexcept中提供,定义了logic_error和runtime_error类,这两个类直接从exception类public继承,是一系列派生类的基类,logic_error描述了典型逻辑错误,其子类包括:

domain_error:数学函数定义域异常,传入了非法值

invalid_argument:给函数传递了意料之外的值

length_error:没有足够的空间完成操作,比如string.append有长度限制,若超长抛出此异常

out_of_bounds:数组索引无效

runtime_error描述了可能在运行期间发生但是难以预计和防范的错误,其子类有:

range_error:计算结果不在允许的范围内,但是不是上溢或者下溢

overflow_error:上溢错误

underflow_error:下溢错误,浮点数计算时会发生,存在一个浮点数能表示的非零最小值,比这个值还小抛出此异常

2.bad_alloc异常:使用new分配内存出现问题抛出此异常,老的实现是返回一个NULL指针。

3.空指针和new:c++允许调用者显式控制是否开启bad_alloc异常,如下:

int * p = new (nothrow) int;

这样若内存分配失败,那么返回空指针,而不是抛出异常。

异常和动态申请内存:在栈解退的时候,对象会被自动调用析构函数,但是用new动态分配的内存不会被释放,导致内存泄漏,这个问题只能靠编程解决,在catch块中增加清理机制。

RTTI:运行阶段类型识别,主要用来处理派生链中不知道具体对象类型的问题,c++中3个为RTTI使用的元素:

1.dynamic_cast,使用一个指向基类的指针生成一个指向派生类的指针,否则返回空指针

2.typeid:返回一个指出对象的类型的值

3.type_info:存储有关特定类型的信息

RTTI只适用于包含虚函数的类,否则不会有同名函数,没必要使用。

1.dynamic_cast

一个dynamic_cast使用的例子如下:

class Test1
{
public:
    virtual ~Test1(){}
};

class Test2 : public Test1
{
};

Test1 * p1 = new Test1;
Test2 * p2 = dynamic_cast<Test2 *>(p1);

这里p2会是一个空指针,因为p1指向的是Test1的内容,而Test2中可能新增内容,导致不安全,此运算符用法如下:

dynamic_cast<Type *>(p)

若p指向的类型是Type,或者使用Type派生而来的类型,那么将p转换成Type *类型,否则返回空指针,这个运算符就是用来检查指针转换安全性的,也可以用于引用类型转换,但是没有空指针对应的引用值,因此无法使用类似空指针这样的返回值表示失败。当请求不正确时,dynamic_case引发类型为bad_cast的异常,使用exception派生出来的,在头文件typeinfo中提供,使用方法如下:

#include <typeinfo>

try {
    Superb & rs = dynamic_cast<Superb &>(rg);
}
catch (bad_cast &)
{
}

2.typeid运算符和type_info类

typeid运算符用来确定两个对象是否是相同的类型。可以接受两种参数:

类名

结果为对象的表达式

返回值是对一个type_info对象的引用,type_info是在头文件typeinfo中提供的一个类,重载了==和!=运算符,可以对类型进行比较。使用方法如下:

typeid(Magnificent) == typeid(*pg)

若pg是Magnificent类型的指针,那么表达式的值是true,否则是false。若pg是空指针,引发bad_typeid异常

type_info类提供了一个name(),用来返回类型名,这个实现随厂商而异,比如int类型可能是i或者int,使用方法如下:

cout << typeid(*pg).name() << endl;

类型转换运算符:除了dynamic_cast,还有几种,所有类型转换运算符如下:

dynamic_cast,const_cast,static_cast,reinterpret_cast

const_cast:只允许类型改变值为const或者volatile,语法和dynamic_case相同,如下:

High bar;
const High * pber = &bar;
High * pb = const_cast<High *>pber;        //valid
const Low * pl = const_cast<Low *>pber;    //invalid

static_cast:语法和前面两个相同,只有两边有一边可以进行隐式转换的时候,才可以进行,如下,其中Pond是无关类,所以最后一个语句是非法的:

High bar;
Low blow;
High * pb = static_cast<High *>(&blow);        //valid
Low * pl = static_cast<Low *>(&bar);           //valid
Pond * pmer = static_cast<Pond *>(&blow);      //invalid

reinterpret_cast:语法一致,不允许删除const,会执行依赖于实现的操作,如下:

struct dat
{
    short a;
    short b;
};
long value = 0xA224B118;
dat * pd = reinterpret_cast<dat *>(&value);
cout << hex << pd->a << endl;

这种操作和硬件强相关,不可移植。reinterpret_cast有限制,比如不能将指针转换成更小的整型或者浮点型,不能将函数指针转换成数据指针。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值