c++异常

c++异常

  • c++需要异常吗?

    1. 在c语言中常见的错误处理方式分为
    • 返回值

      我们常用函数的返回值来标志成功或者失败,甚至是失败的原因。但是这种做法的最大问题是如果调用者不主动检查返回值也是可以被编译器接收的。这在c++中还导致另外一个问题,就是重载函数不能只有不同的返回值,而有相同的参数。如果需要不同返回值的来返回错误信息的话。(除非c++支持多值返回或者使用结构体返回或者使用输出型参数)

    • 全局状态标志

      例如系统调用使用errno。与返回值不同的是,全局状态标志可以让函数的接口(返回值、参数表)被充分利用。函数在退出前应该设置这个全局变量的值为成功或者失败(包含原因), 而与返回值一样,它要求调用者要在调用后检查这个标志,这种约束全靠个人行为。全局变量还导致另外一个问题,就是多线程不安全,如果多个线程同时为一个全局变量赋值,那么调用者在检查这个标志的时候一定非常疑惑。当然errno是线程安全的。

    1. c++引入异常解决了什么问题

      对于返回值和errno遇到的尴尬,对异常来说基本上不存在,如果你不捕获程序中抛出的异常,默认行为是导致abort()被调用,程序被终止(coredump)。因此你的函数抛出了异常,这个函数的调用者或调用者的调用者,也就是在当前的callstack上,一定有一个地方捕获这个异常,有了异常之后可以放心的只书写正确的逻辑,而将所有的错误处理归结到一个地方。

  • 异常的语法

    • try

      try总是与catch一同出现,伴随一个try语句,至少应该有一个catch()语句, try随后的block是可能抛出异常的地方

    • catch

      catch带有一个参数,参数类型以及参数名称都由程序员指定,如果catch随后的block中并不打算引用这个异常对象那么变量名可以忽略。参数类型可以是基础类型,也可以是一个对象,或者一个对象指针/引用。如果希望捕获任意类型的异常,可以使用...作为catch的参数

    • throw

      throw后面带一个类型的实例,它和catch的关系就像函数调用,catch指定形参,throw给出实参。编译器按照catch出现的顺序以及catch指定的参数类型确定一个异常应该由哪个catch处理。当然throw不一定非要出现在try随后的block中,它可以出现在任何需要的地方,只要最终会有catch可以捕获它即可。即使在catch随后的block中,仍然可以继续throw。这时候有两种情况,一种是throw一个新的类型异常,这与普通的throw一样。二是要rethrow当前的这个异常,在这种情况下,throw不带参数即可表达

      try
      {
          // ...
      }
      catch(int)
      {
          throw myException("Exception");
      }
      catch(float)
      {
          throw;
      }
      
    • 函数声明

    void foo() throw (int); // 只能抛出int异常

    void bar() throw () // 不抛出任何异常

    void bar(); // 可以抛出任何类型的异常或者不抛出

    ​ 如果一个函数的声明中带有throw限定符,则在函数体中也必须同样出现。

  • 异常使用技巧

    • 异常处理流程

      函数调用发生时,会执行保护现场寄存器、参数压栈、为被调用的函数创建堆栈。这几个操作都会使堆栈增长。每次函数返回则会恢复现场、使堆栈减小。函数返回过程中恢复现场的过程称为unwindingstack

      异常处理中的throw语句产生的效果与函数返回相同,它也引发unwindingstack.如果catch不是在throw的直接上层函数中,那么这个unwinding的过程会一直持续,直到找到合适的catch。如果没有合适的catch,则最后std::unexcepted()函数被调用,说明发现了一个没想到的异常,这个函数会调用std::terminate(),这个terminate()调用abort(),程序终止(coredump)

    • guard模式

      异常处理在unwindingstack的时候,会析构所有栈上的对象,但是却不会自动删除堆上的对象,甚至代码中虽然写了delete语句,但是却被throw跳过,导致内存泄露,或者其他资源的泄露

      void foo()
      {
         	MyClass *p = new MyClass;
          bar(p);
          delete p;
      }
      void bar(MyClass*& p)
      {
          throw MyException();
      }
      

      对于这种情况,c++提供了智能指针来解决,当发生异常unwindingstack的时候,智能指针会析构,而在它的析构函数中,指针将被自动delete

      class MyClass
      {
      };
      
      void bar(const MyClass* p) throw (const char*)
      {
          throw "hh";
      }
      
      void foo()
      {
          // MyClass* ptr = new MyClass;
          // bar(ptr);
          // delete ptr;
          
          std::unique_ptr ptr(new MyClass);
          bar(ptr.get());
      }
      
    • 构造函数和析构函数

      构造函数没有返回值,很多地方都推荐通过抛出异常来通知调用者构造失败。这是个好办法但是不完美。因为在构造函数中抛出异常并不会引发析构函数的调用

      class Foo
      {
      public:
          ~Foo() {}
      };
      
      class Bar
      {
      public:
          Bar()
          {
              c_ = new char[100];
              throw - 1;
          }
          ~Bar()
          {
              cout <<"destroy" << endl; // 不会被调用
              delete c_;
          }
      
      private:
          char *c_;
          Foo f_;
      };
      

      解决办法使用智能指针

      class Foo
      {
      public:
          ~Foo() {}
      };
      
      class Bar
      {
      public:
          Bar()
          {
              c_.reset(new char[100]);
              throw - 1;
          }
          ~Bar()
          {
              cout <<"destroy" << endl; // 不会被调用
          }
      
      private:
          std::unique_ptr c_;
          Foo f_;
      };
      
  • 标准异常

c++异常_第1张图片

  1. bad_typeid

    使用typeid运算符时,如果其操作数是一个多态类的指针,而该指针的值为NULL,则会抛出异常

  2. bad_cast

    在使用dynamci_cast进行从多态基类对象(或引用)到派生类的引用强制类型转换时,如果类型是不安全的,则会抛出该异常

  3. bad_alloc

    在使用new运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常

  4. out_of_range

    下标越界,则会抛出此异常

  5. runtime_error

    exception类派生.报告运行时错误,只有在程序运行时,这类错误才可能被检测到.

  6. ios::failure

    exception类派生,没有子类从logic_error派生的异常类:

  7. domian_error

    报告违反了前置条件

  8. invalid_argument

    表明抛出这个异常的函数接收到了一个无效的参数

  9. length_error

    表明程序试图产生一个长度大于npos的对象

你可能感兴趣的:(c++)