c++ primer plus 第十五章笔记 友元,异常和其他

友元类:
        两个类不存在继承和包含的关系,但是我想通过一个类的成员函数来修改另一个类的私有成员和保护成员的时候,可以使用友元类。

class A
{
    private:
        int num;//私有成员
    //...
    public: 
    //...
        friend class B;//声明一个友元类
}

class B
{
    private:
    //...
    public:
    void set(A &t, int c){t.num = c;}//友元类的成员函数可以直接访问其他类的私有成员和保护成员

}

如何将一个类中的成员函数设置为另一个类的友元函数?

class B
{
    private:
    //...
    public:
    //...
    void set();
}

class A
{
    private:
        //...
        int num;
    public:
        friend void B::set();//声明另一个类中的方法是友元函数,B中对应的友元函数直接可以访问A中私有和 保护成员,而不用通过A对象的方法

}

如何解决循环引用,循环依赖的问题?

什么是循环引用和循环依赖?比方说类A中的函数对类B的对象进行了操作,类B中又对类A的对象进行了操作,如果类A声明在类B前面,则会导致编译器不认识类A中的B,如果类B在类A前面,则会导致编译器不认识类B中的A,这就是循环引用。

如:

class A
{
    public:
        int num1;
    private:
        void Output() {cout << num1};
        void Show(const B & b){ b.Output();}
};

class B
{
    public:
        int num2;
    private:
        void Output() {cout << num2};
        void Show(const A &a){ a.Test();}
};
//显然A中的Show()不认识其参数类型const B &
//如果AB交换位置,又会导致B的Show不认识类A
//这就是循环引用,循环依赖

此时,编译器既不认识B也不认识b.Output()

解决办法:
使用"向前声明"

class B;//声明一下B是一个类,这种方法叫做向前声明
class A
{
    public:
        int num1;
    private:
        void Output();
        void Show(const B & b);
};

class B
{
    public:
        int num2;
    private:
        void Output() {cout << num2};
        void Show(const A &a){ a.Test();}
};

但是还有一个问题,那就是编译器不认识Show中b的方法Output,因为现在编译器只是知道B是个类,并不知道B中的成员函数是什么

即此时知道B是个类,但是不知道B的成员函数Output()

解决方法:

将A中的内联函数写在类B的声明之下

class B;//声明一下B是一个类,这种方法叫做向前声明
class A
{
    public:
        int num1;
    private:
        void Output();
        void Show(const B & b);
}

class B
{
    public:
        int num2;
    private:
        void Output() {cout << num2};
        void Show(const A &a){ a.Test();}
}
//在类外面定义内联函数,要用inline,并声明属于哪个类
inline void A::Show(const B &b)
{
     b.Output();
}

注意:在头文件下定义的成员函数都是内联函数,因此要在函数前面加上inline

模板类里面的成员函数一点都是放在.h文件下进行定义的

嵌套类:

即在一个类里面再声明一个了类作为成员变量:

template<class T>
class Queue
{
	private:
		class Node
		{
			T data;
			Node *next;
		} 
}

15.3 异常

1.调用#include<cstdlib>里面的abort()函数,遇到异常时,直接终止程序,并输出核心已转存(core dumped)的错误提示

#include<cstdlib>

//计算调和平均值,调和平均值 = 2.0 * x * y / x + y,注意x和y不能是相反数
/*
在一些新式编译器中,当除数为0时,并不会出现错误信息,程序仍然可以正常运行,此时算出来的调和平均值
是INF,表示一个无穷大的特殊浮点数,而有的编译器可能会运行到生成除数为0就崩溃的程序
*/

double hmean(double a, double b)
{
    if(a == -b)
    {
        cout << "invalid argument to hmean()" << endl;
        abort();//程序直接终止运行; 
    }
    else
        return 2.0 * x * y / x + y;

}

2.

返回错误码

bool hmean(double a, double b, double *c)//传入指针,指针直接操作内存
{
    if(a == -b)
    {
        cout << "invalid argument to hmean()" << endl;
        return false;
    }
    else
    {
        *c = 2.0 * x * y / x + y;
        return true;//此处的true和false可作为错误码
    }
}

3.

使用c++的异常处理机制

1)使用try块

2)异常发生时抛出异常(throw)

3)程序捕抓异常(catch)

double hmean(double a, double b, double *c)//传入指针,指针直接操作内存
{
    if(a == -b)
    {
        throw "bad hmean() argument, a = -b not allowed. ";//抛出异常,throw后面可以跟字符串,对象或其他c++类型,此处的异常类型是const char *类型
        //throw抛出异常后hmean函数立即终止,返回main函数中调用hmean的下一行,程序找到能接收抛出异常类的异常处理程序catch(const char *s)
    }
    else
        return 2.0 * a * b / a + b;
}

int main(void)

{
    int a, b;
   while( cin >> a >> b && a != 0 && b != 0)
    {
        try//try块,用于注明可能发生异常的代码,在try块外调用的hmean函数,发生时异常不会处理该异常,而是会终止程序的运行
        {
            hmean(a, b);
        }
        catch(const char *s)//捕抓异常,异常处理程序
        {
            cout << s << endl;
            cout << "Enter a new pair of arguments." << endl;
            continue;
        }
    }
    hmean(10, -10);//在try块外发生异常,异常处理程序不会捕获,而是直接终止程序运行
    return 0;
}

通常是类作为异常类型来抛出,因为类可以携带更多的信息,抛出异常捕抓异常try块标注可能发生异常的代码的方式和字符串类型相同,此处不在过多赘述。

栈解退

两种抛出异常

1.直接抛出异常

try块中的函数中,直接有抛出异常的语句,此时抛出异常后函数结束,抛出的异常类型返回main函数找到匹配的异常处理函数捕捉异常并处理,如上面的例子,都是直接抛出异常

2.间接抛出异常

比方说函数a调用函数b,函数b调用函数c,函数c中抛出异常

我们知道函数的调用是一个压栈的过程,此时函数c抛出异常后,将会直接结束函数c运行,退回到上一个函数的调用,即出栈,到函数b,在函数b中寻找能够处理该异常类型的catch异常处理函数,找不到直接退栈不会执行该函数的内容,再往前找,直到找到能够处理该异常类型的catch异常处理函数为止,如果退到最后main函数都没有能处理这个异常类型的catch异常处理函数,则结束程序运行,这个就是栈解退,即异常抛出后就会往回倒,找到能满足条件的函数,找不到就程序运行结束。

一些需要注意的点:

1)如果抛出的异常类型是某个了的对象的话,将会调用该类的构造函数,生成一个该对象的副本,然后找到对应的catch来处理该异常

注意点:

为什么要创建副本?

原因是抛出对象后,函数结束,该对象被销毁,不能抛出对象本身,应该抛出该对象的副本

catch接收的异常类型是什么类型?

是该对象类型的引用

为什么是引用?而不是按值传参?

因为基类的引用既可以指向基类,还可以指向派生类,使用引用的话,该异常类型的派生类也可以被这个catch捕获

{
    Class test;
    throw test;//将会生成test的副本,p指向test的副本而不是test本身是一件好事
}
//...

catch(Class & t)//对应的异常类型是Class类型的引用
{

}

2)在catch中,如果throw后面没有跟异常类型,那么抛出来的异常类型是什么?

是,和catch处理的异常类型是同一异常类型

catch<const char *>
{
    throw;//此时抛出的异常是const char *的异常
}

3)如果a派生出b,b派生出c,c派生出d,这种叫做层次化派生

//假设a是Base1类型的对象, c是Base2, c是Base3, d是Base4,这种叫做层次化派生
{
 throw a;
 throw b;
 throw c;
 throw d;
}

//那么catch接收应该是
catch(Base4 &d)
{
}

catch(Base3 &c)
{
}

catch(Base2 &b)
{
}

catch(Base1 &a)
{
}
//catch的顺序应该和throw的顺序相反,原因是基类的引用可以指向基类的对象也可以指向派生类的对象
//如果把Base1 &a放在最前面,那么Base1的这一个catch就能接收Base2, Base3, Base4的异常了,导致
//其他catch无法执行

exception类

1.exception类是c++的异常类,这个异常类里面有一个重要的虚函数what,它返回一个字符串,我们可以使用exception这个异常类来派生出其他的异常类供我们使用。

//exception里面虚函数what的声明
virtual const char* what() const noexcept;
//返回一个c风格的指针, noexcept的意思是说该函数不会抛出任何异常

2.c++库还定义了很多基于exception的异常类,比如在stdexcep头文件下的logic_error和runtime_error.

3.bad_alloc和new

bad_alloc是一个异常类,在头文件new中,其作用是当你用new开辟内存的时候,开辟失败了,库里面的某个函数就会抛出异常(不需要自己抛出),就会返回一个bad_alloc类的对象(在包含头文件new的情况下)

可以使用

catch(bad_alloc &ba)//捕获该异常 
{

}

对开辟内存失败后做出一些操作。

我们熟悉的new开辟内存空间一般是返回NULL指针,如果包含了new头文件,就可以有两种选择

int *pi = new (std::nothrow) int;//关闭bad_alloc,返回NULL
int *pa = new (std::nowthrow) int[500];//开辟bad_alloc,抛出bad_alloc异常

类的成员函数声明中,参数列表后面加一个throw是什么意思?

class A
{

    public:
    virtual ~A() throw(){}//表示该析构函数不抛出任何异常
    //virtual ~A() throw(...){}//表示该析构函数抛出任何异常
    //virtual ~A() throw(int ){}//表示该析构函数抛出int异常
}

  RTTI

例子:
        在层次化派生中,a派生出b,b派生出c,我们知道基类的指针和引用既可以指向基类的对象还可以指向派生类的对象,假如a中有一个定义虚方法Show,那么我们b和c都可以使用该方法(不管你是重写了Show还是就是用继承而来的a的Show)。

        此时,如果在b的成员函数中定义了一个虚方法Bfuction后派生的c,我们知道c也可以使用从b继承而来的Bfuction,但是,这个时候问题来是,如果我们在代码中使用a的指针指向了a的对象,然后调用了a的Bfuction方法(有这种可能,毕竟基类的指针可以指向基类也可以指向派生类),那就会报错,因为a中并没有该Bfuction方法。

        总的来说就是基类的指针和引用可能会指向基类的对象也可能指向派生类的对象,当你使用基类的指针的时候,却不考虑是不是现在可能指向基类的对象,自以为是指向派生类的对象,然后用派生类的方法来操作基类的对象。

         基类的指针指向基类的对象,却用了派生类的方法。

       c++新特性RTTI帮助解决这种问题,RTTI中有一个运算符叫做dynamic_cast,该运算符的用法如下:
 

//...
class A
{
    public:
        virtual void Show() {cout << "I'm A" << endl;}

};
class B : public A
{

    public:
        virtual void Show() {cout << "I'm B" << endl;
        virtual void Bfuction() {cout << "Bfuction" << endl;}

};
class C : public B
{


    public:
        virtual void Show() {cout << "I'm C" << endl;
        virtual void Bfuction() {cout << "Bfuction" << endl;}

};
//typedef A Class
//typedef B Class
//typedef C Class

int main(void)
{
    A *pr1 = new Class;//Class可以是A类可以是B类,可以是C类,当Class是A类的时候,pr1就不能使用方法Bfuction
    A *pr2;
    if(pr2 = dynamic_cast<B *>(pr1))//意思就是基类A的指针能不能当作派生类B的指针来用,如果可以则返回pr1所指向的对象的地址,否则则返回NULL,相当于就是帮你看看你这个指针行为是否安全
    pr2->Bfuction
    return 0;
}

即dynamic_cast可以帮助避免这种基类指针指向基类对象却使用派生类方法的情况。

type_id和type_info类

        在头文件typeinfo中,有一个type_id运算符和type_info类。

        type_id是一个运算符,该运算符需要传入一个对象作为参数,该运算符可以返回一个type_info对象的引用,type_info对象中有一个name的方法,该方法可以返回该对象类的名字,即可以通过这个运算符来查某个对象的类名。

例子:

#include<typeinfo>
class A
{
};
int main(void)
{
    A test;
    cout << typeid(test).name() << endl;
    return 0;
}

旁边多了个数字不用管他,就是这样的

复习一下如何产生随机数

1.引入头文件#include<cstdlib> #include<ctime>

2.产生随机数种子srand(time(0))

3.调用产生随机数的函数rand()

#include<iostream>
#include <cstdlin>
#include <ctime>

using namespace std;

srand(time(0));
int main(void)
{
    cout << rand() << endl;
    return 0;
}

类型的转换运算符

在类的派生中,向上转换指的是由派生类转换成基类,向下转换是指由基类转换成派生类

介绍了4中类型转换的运算符

*dynamic_cast<>

上面已经讲过了,我理解的是可以用来判断向下转换是否安全(即基类转派生类),但是不知道为什么定义里面说dynamic_cast是只用于向上转换的

double b = (double)10/3;//c语言风格的类型转换
double b = static_cast<double >(10)/3;//c++风格的类型转换
//不类型转换b就是3,类型转换后b就是3.33...

全,基类的指针能否当作派生类的指针来用?能就返回指向对象的地址,否则就返回NIULL

*const_cast<>

生成一个去const的类型的变种

int main(void)
{
    int a = 10;
    const int *pa = &a;//按理说pa指针是无法改变a变量的值
    int *pb = const_cast<int *>(pa);
    *pb = 20;//此时pb是pa去掉const的变种,即int *
    cout << a << endl;//此时a等于20;
    return 0;

}

*static_cast<>

1.用于数据类型转换

double b = (double)10/3;//c风格的类型转换
//关于为什么上面那句话得到的不是3而是3.33333,原因是从左往右执行的,先将10转换成了double
//然后再除的3
double b = static_cast<double > (10)/3;//c++风格的类型转换

2.既可以在类中进行向上转换还可以向下转换

class A
{

};
class B : public A
{

};

int main(void)
{
    A a;
    B b;
    A *pa = static_cast<A *>(&b);//向上转换,派生类往基类转
    B *pb = static_cast<B *>(&a);//向下转换,基类往派生类转
    return 0;
}

*reinterpret_cast<>

一种很危险的转换,比如将两种不相关的类型进行强制类型转换,不推荐使用

struct Node
{
    int data;
    Node *next;
}


void main(void)
{
    Node *pn = new Node;
    double *pb = reinterpret_cast<Node *>(pn)
    return 0;
}

复习题:

1.throw和return的区别何在?

throw抛出异常后,使用的是栈解退的方式,直到找到能够处理该异常的catch异常处理程序,在栈解退的过程中,并不会继续运行栈中函数的内容,找不到就直接退出,而return返回到上一个调用函数后,会继续运行上一个函数的内容而不会直接退栈。

2.假设有一个从异常基类派生来的异常类层次结构,则应按什么样的顺序放置catch块?

上面有说过,应该将异常基类放在最下面,最新的派生类放在最上面。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值