C++PrimerPlus学习之友元,异常和其他

友元

  • 友元类

    • 友元声明可以位于公有,私有或保护部分,其所在的位置无关紧要。

    • 注意类的声明的先后顺序

      class Base
      {
      public:
          friend class Point;
          void show()const{cout<<x<<' '<<y<<endl;}
          Base(int tx,int ty):x(tx),y(ty){}
          Base(){x=y=0;}
      private:
          int x,y;
      };
      class Point
      {
      public:
          void showx(Base &a)const{cout<<a.x<<endl;}
          void showy(Base &a)const{cout<<a.y<<endl;}
          void setBase(Base &a,int x,int y){a=Base(x,y);}
      };
      int main()
      {
          Point tmp;
          Base tmp2=Base(1,2);
          tmp.showx(tmp2);
          tmp.showy(tmp2);
          tmp.setBase(tmp2,3,4);
          tmp.showx(tmp2);
          tmp.showy(tmp2);
      }
      
  • 友元成员函数

    • 使用前向声明,注意排列的次序
    class Tv;//前向声明
    class Remote{...};
    class Tv{...};
    
    • 如果Remote类中函数需要使用Tv类中的成员时,我们可以先只在类中声明,再在后面使用内联函数进行定义
    • 内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。故内联定义位于头文件中。也可以将定义放在实现文件中,但必须删除关键字inline,这样函数的链接性将是外部的
    • 将整个Remote类成为友元并不需要前向声明,因为友元语句本身已经指出Remote是一个类:friend class Remote;
  • 其他友元关系

    • 互相是友元的类
      • 注意声明定义的顺序
        class A
        {
        friend class B;
        public:
        	void buzz(B &b);
        	...
        };
        class B
        {
        friend class A;
        public:
        	bool check(A &a);
        	...
        };
        inline void A::buzz(B &b){...}
        inline bool B::check(A &a){...}
        
    • 共同的友元
      • 函数需要访问两个类的私有成员

      • 注意声明和定义的顺序

      • 需要前向声明

        class A;
        class B
        {
        	friend void sync(A &a,const B &b);
        	friend void sync(B &b,const A &a);
        	...
        };
        class A
        {
        	friend void sync(A &a,const B &b);
        	friend void sync(B &b,const A &a);
        	...
        };
        //define the friend functions
        inline void sync(A &a,const B &b){...}
        inline void sync(B &b,const A &a){...}
        

嵌套类

  • 嵌套类的作用域由其声明位置所决定,实际上和普通的成员没有多大不同。
  • 当嵌套类的声明位于公有部分,包含类的外面才能使用嵌套类,而且必须使用作用域解析运算符(旧版本的C++不允许嵌套类或无法完全实现这种概念)
  • 嵌套类,结构和枚举的作用域特征

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

异常

  • 使用abort()

    • 其典型实现是向标准错误流(即cerr使用的错误流)发送消息 abnormal program termination(程序异常终止),然后终止程序。

    • 返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程),处理失败。

    • abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。

      #include<bits/stdc++.h>
      using namespace std;
      int main()
      {
          int x,y;
          while(cin>>x>>y)
          {
              if(x+y==0)
              {
                  cout<<"error"<<endl;
                  abort();
              }
              else cout<<2.0*x*y/(x+y)<<endl;
          }
          return 0;
      }
      
  • 异常机制

    • 组成部分

      • 引发异常
      • 使用处理程序捕获异常
      • 使用try
    • 一个例子

      #include<bits/stdc++.h>
      using namespace std;
      double solve(double x,double y)
      {
          if(x==-y)
          {
              throw "bad";
          }
          return 2.0*x*y/(x+y);
      }
      int main()
      {
          double x,y;
          while(cin>>x>>y)
          {
              double z;
              try
              {
                  z=solve(x,y);
              }
              catch (const char *s)
              {
                  cout<<s<<endl;
                  cout<<"error"<<endl;
                  continue;
              }
              cout<<"answer: "<<z<<endl;
          }
          return 0;
      }
      
    • 异常规范

      • 告诉使用的人所有可能会返回的异常,这项工作也可使用注释轻松完成。由于这项工作很难维护,所以C++11已经摒弃了,但该用法仍在标准之中。

      • C++11支持一种特殊的异常规范,使用新增的关键字noexcept指出函数不会引发异常

        double marm() noexcept;
        
    • 栈解退

      • C++通常将信息放在中来处理函数调用。
      • 程序将调用函数的指令的地址(返回地址)放到栈中,但被调用的函数执行完毕时,程序将使用该地址来确定从哪里开始继续执行。
      • 当函数结束时,程序流程跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,以此类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数将被调用。
      • 如果函数出现异常而终止,则程序也将释放栈中的内存,当不会释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块的返回地址。随后,控制权也将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退。
    • 其他异常特性

      • 将引用作为返回值通常可以避免创建副本以提高效率
      • 基类引用可以执行派生类对象,假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配
      • catch块的排列顺序应该与派生顺序相反,即层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放最后面
      • catch(...){//statements}代表捕获任何异常
      • 也可以去掉引用,这时则需要使用虚函数
    • exception

      • C++可以把它当作其他异常类的基类,代码就可以引发exception异常了。如它有一个名为what()的虚函数,我们则可以重新声明一个它的派生类,重新定义what()函数。
      • stdexcept异常类
        • stdexcept定义了logic_errorruntime_error类,它们都是以公有方式从exception派生而来的
        • logic_error
          • 用于描述典型的逻辑错误
          • 几个它的派生类
            • domain_error
              • 当参数不在函数所需参数的定义域中会引起domain_error异常
            • invalid_argument
              • 当给函数传递了一个意料之外的值,如函数希望接受一个只包括0或1的字符串,当传递的字符串包含其他字符,该函数将引发invalid_argument异常
            • length_error
              • 用于指示没有足够的空间来执行所需操作,如string类的append()方法合并的字符串超过最大允许长度时,将引发length_error异常
            • out_of_bounds
              • 用于索引错误
        • runtime_error
          • 用于描述可能在运行期间发生但难以预计和防范的错误
          • 几个它的派生类
            • range_error
              • 范围出错
            • overflow_error
              • 上溢错误
            • underflow_error
              • 下溢错误
  • bad_alloc异常和new

    • 对于使用new导致的内存分配问题,C++最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明
    • 但无法分配请求的内存量时,new返回一个空指针
  • 空指针和new

    • 很多代码都是在new失败时返回空指针,为处理new的变化,有些编译器提供了一个标记开关,让用户选择所需的行为。其用法如下

      int *pi=new (std::nothrow)int[10000];
      if(pi==0)
      {
      	cout << "error"<< endl;
      	exit(EXIT_FAILURE);
      }
      
  • 异常何时会迷失方向

    • 意外异常

      • 它在带异常规范的函数中引发的,但规范列表中没有该异常

      • 默认情况下,这将导致程序异常终止

      • 程序首先调用函数terminate()。默认情况下terminate()调用abort()函数。可以指定terminate()应调用的函数(而不是abort()).为此,可调用set_terminate()函数。它们的声明都在exception头文件中:

        typedef void (*terminate_handler)();//没有参数和返回值的函数指针
        terminate_handler set_terminate(terminate_handler f)throw();//C++98
        terminate_handler set_terminate(terminate_handler f)noexcept;//C++11
        void terminate();//C++98
        void terminate()noexcept;//C++11
        
      • 一个例子

        #include<bits/stdc++.h>
        #include<exception>
        using namespace std;
        
        double solve(double x,double y)throw(const char *)
        {
            if(x+y==0)
            {
                throw out_of_range("error");
            }
            return 2.0*x*y/(x+y);
        }
        void myQuit()
        {
            cout<<"Terminating due to uncaught exception\n";
            exit(5);
        }
        int main()
        {
            set_terminate(myQuit);
            double x,y;
            while(cin>>x>>y)
            {
                double z;
                try
                {
                    z=solve(x,y);
                }
                catch(const char *ch)
                {
                    cout<<ch<<endl;
                    continue;
                }
                cout<<z<<endl;
            }
            return 0;
        }
        
        
    • 未捕获异常

      • 如果异常不是在函数中引发的(或者函数没有异常规范),且没被捕获
      • 默认情况下,这将导致程序异常终止
      • 如果发生意外异常,程序将调用unexpected()函数,这个函数将调用terminate()。我们可以使用set_unexpected()函数。与上面类似。这些新函数也是在头文件exception中声明的。
      typedef void (*unexpected_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();//C++11
      
  • 有关异常的注意事先
    异常和动态内存分配并非总能协同工作,当进行栈解退时,动态申请的内存很可能并没有释放掉,所以我们需要在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(运行阶段类型识别)

  • dynamic_cast运算符

    • 使用方法

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

  • typeid运算符

    • 能够确定两个对象是否是同种类型。它和sizeof有些相像。可以接受两种参数。

      • 类名
      • 结果为对象的表达式
    • 使用方法

      typeid(Base)==typeid(*p);//p为指针
      
    • 如果p是一个空指针,程序将引发bad_typeid异常,该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。

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

类型转换运算符

  • dynamic_cast运算符
    • 之前已经介绍过,该运算符的用途是,使得能够在类层次结构中进行向上转换。
  • const_cast运算符
    • 该运算符用于执行只有一种用途的类型转换,即改变值为constvolatile

    • 语法

      const_cast< type-name> (expression)
      
    • type-nameexpression的类型必须相同

    • 一个例子

      #include<bits/stdc++.h>
      using namespace std;
      int main()
      {
          int a=3;
          const int *p=&a;
          int *p2=const_cast<int *>(p);
          *p2+=4;
          cout<<a<<endl;
          //
          const int a2=3;
          const int *p3=&a2;
          int *p4=const_cast<int*>(p3);
          p4+=4;
          cout<<a2<<endl;
      }
      /*output:
      7 
      3
      */
      

      当指针被声明为const int*时,不能修改指向的int,指针p2和p4删除了const特征,因此可用来修改指向的值,但仅当指向的值不是const才行,因此p2可用于修改a,但p4不能用来修改a2。

  • static_cast运算符
    • 使用语法

      static_cast<type-name>(expression)
      
    • 仅当type_name可被隐式转换expression所属的类型或expression可被隐式转换type_name所属的类型时,上述转换才合法。

    • 一个例子

      #include<bits/stdc++.h>
      using namespace std;
      class A{};
      class B:public A{};
      class C{};
      int main()
      {
          A a;
          B b;
          C c;
          A *tmp1=static_cast<A*>(&b);//valid
          A *tmp2=static_cast<C*>(&c);//invalid
      }
      
  • reinterpret_cast运算符
    • 用于天生危险的类型转换。它不允许删除const,但会执行其他令人生厌的操作。

    • 使用语法与另外3个相同。

    • 一个例子

      #include<bits/stdc++.h>
      using namespace std;
      
      struct dat
      {
          short a,b;//short 2个字节
      };
      
      int main()
      {
          long val=0xA224B118;//4个字节
          dat *pd=reinterpret_cast<dat *>(&val);
          cout<<hex;
          cout<<pd->a<<' '<<pd->b<<endl;
      }
      
      
      • reinterpret_cast运算符并不支持所有的类型转换。例如可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是,不能将函数指针转换为数据指针,反之亦然。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值