OOP Lec12: Exception

Exception

Badly formed code will not be run.

Exception 翻译为“异常”。

C++ 的目的之一:比 C 更严格的类型检查

Python 在 21 年开始使用自带的类型检查

TypeScript:有类型的 JavaScript

引入

如何处理文件:

  1. 用指令打开文件
  2. 计算文件大小
  3. 分配内存
  4. 将文件读入内存
  5. 关闭文件

各个环节都可能发生异常。

异常的三个特征:

  1. 可预见
  2. 不可避免
  3. 不一定发生

C 语言中的异常处理方式:返回一个固定的错误值

如果全部直接处理,造成的后果:

  1. 业务逻辑淹没在异常处理中,难以阅读
  2. 修改内容、增减内容非常困难
  3. 有时未必能通过程序本身解决异常,需要停止运行

异常处理

语法

异常使用 trycatchthrow 三个关键字处理。

try

用于声明某个代码段内可能抛出异常。

其后应该跟随若干个 catch 来处理异常。

try{
    ...
}

catch

处理异常的条件语句。

catch 后的括号内,表示接收的异常的类型。捕获不同类型的异常可以由不同的代码处理。

花括号内的代码称为 handler。

在捕获时,类型的匹配按照代码的顺序,而 ... 表示捕捉任何异常。

推荐捕获一个引用 &

catch(ErrorType& e){
    ...
}
catch(...){
    ...
}

throw

抛出异常:

template<typename T>
T& Vector<T>::operator[](int indx){
    if(indx < 0 || indx >= m_size){
        throw VectorIndexError(indx);
    }
    return m_elements[indx];
}

可以在捕捉异常后再抛出:

catch(VectorIndexError& e){
    throw;
}

异常规范

在函数原型中写出异常规范,声明函数可能返回何种异常:

void print(Document& p) throw(PrintOffLine, BadDocument);
void goodguy() throw();// throw no exceptions, until C++11
void alloc() throw(...);// can throw any exception
void abc() noexcept;// throw no exceptions, since C++11

如果在函数中返回了规范之外的异常,系统会调用 std::unexpected() 来处理。

std::unexpected() 默认调用 std::terminate() 来终止程序。

可以用 std::set_unexpected(func)std::unexpected() 重载为 func()

也可以用 std::set_terminate(func)std::terminate() 重载为 func()

如果 std::unexpected() 被调用后,抛出的异常仍然不符合异常规范,则会抛出 std::bad_exception 异常。

在 C++17 之后,异常规范说明已被弃用。使用 noexcept 说明函数不会抛出任何异常时,若抛出了异常,则会直接调用 std::terminate()

机制

任何一句话抛出了异常,其下面的语句都不能执行。

异常由 catch 转接到对应的代码。

执行完 catch 中的代码后,将跳过 try 中剩余语句。

总的来说,当

  1. 检查是否被 try 包围。

    • 是:跳到第 2 步
    • 否:跳到第 3 步
  2. 检查是否有条件匹配的 catch

    • 是:执行 catch 下的语句
    • 否:跳回第 1 步
  3. 检查是否在函数内

    • 是:回溯到调用该函数的代码,跳回第 1 步
    • 否:退出

优点

分离了业务代码和异常处理。使得修改异常的处理方式更加简洁易读。

在异常接收中还可以利用造型(up-casting),来简化处理。

示例

读取文件

try{
    open the file;
    determine its size;
    allocate that much memory;
    read the file into memory;
    close the file;
}catch(fileOpenFailed){
    dosomething
}catch(sizeDeterminationFailed){
    dosomething
}catch(...){
}

Vector 访问

template<typename T>
T& Vector<T>::operator[](int indx){
    return m_elements[indx];
}

indx 可能是非法下标。如何通知调用的函数此处的异常?

  1. C 语言的做法:返回特殊值,这个特殊值不在正常返回值的值域中
  2. C 语言的做法:保存有一个全局变量,发生异常时修改全局变量来通知。
  3. C++ 的做法:使用异常。
class VectorIndexError{
public:
    VectorIndexError(int v): bad_value(v)
private:
    int bad_value;
};

template<typename T>
T& Vector<T>::operator[](int indx){
    if(indx < 0 || indx >= m_size){
        throw VectorIndexError(indx);
    }
    return m_elements[indx];
}

如果发生了异常,则返回到上一个处理异常的函数处。

void func(){
    int i = vec[4];
    return i * 5;
}

void outer(){
    try{
        func();
        func2();
    }catch(VectorIndexError& e){
        e.diagnostic();
    }
    cout << "Control is here!" << endl;
}
void outer2(){
    string err("exception caught");
    try{
        func();
    }catch(VectorIndexError& e){
        throw;
    }
}
void outer3(){
    try{
        outer2();
    }catch(...){// catch any exception
    }
}

new 的异常

在 C 中,malloc 在未成功分配空间时会返回 NULL,作为判断依据。

new 在未成功分配空间时不会返回 nullptr,而是会抛出 std::bad_alloc 异常。

void func(){
    try{
        while(true){
            int *p = new int[100000000ul];
        }
    }catch(const std::bad_alloc& e){
        std::cerr << "fail to new the array! " << e.what() << '\n';
    }
}

STL 异常定义在 <exception> 中。

exception:所有异常的公共基类

  • bad 系列
    • bad_allocnew 无法分配空间抛出的异常
    • bad_castdynamic_cast 对引用的类型检查出错,抛出的异常
    • bad_typeid:对多态类型的空指针使用 typeid 抛出的异常
    • bad_exception:当前抛出异常的拷贝构造出错时,抛出的异常
  • runtime_error:事件超出程序范围抛出的异常
    • overflow_error:算数上溢抛出的异常(STL 中仅 std::bitset::to_ulong
    • range_error:算数超界抛出的异常
  • logic_error:程序逻辑错误引发的异常,并且可能是可以预防的
    • domain_error:当输入超出了其类型的定义域时抛出的异常
    • length_error:当对容器的操作使其超出了预定义的长度上限时抛出的异常
    • out_of_range:当对容器的操作超出了其当前范围时抛出的异常
    • invalid_argument:当传入参数不合法时抛出的异常

空间安全

构造函数

try-catch 中,所有的本地变量都能正确地调用自己的析构函数,不需要自己释放空间;

但如果结合了 new,可能在分配好空间,执行构造函数的过程中抛出异常,这时候申请的空间需要另外的 delete

这时候会发生很麻烦的问题,指针并没有正确地指向申请的空间。

解决方法:使用两步构造

  • 在构造函数内对基本变量赋值
  • 任何需要申请资源和空间的操作,在显式的 init() 函数内执行

这样保证构造函数不会抛出异常,在 init() 抛出异常时可以正常地析构。

析构函数

在析构函数中抛出异常,我们应该全部解决,这样才能正确地释放空间。

在抛出异常导致的析构中,如果再抛出异常,就会调用 std::terminate()

开发策略

异常处理的各种手段:

  1. 返回特殊值(首选)
  2. 抛出异常
  3. 函数入口的参数检查

更多学习:Exceptions - cppreference.com

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇