异常处理

函数异常块

函数异常块是函数体的一种替代语法,主要用于应对初始化列表抛出的异常,在析构函数或其他常规函数中极少使用:

struct S {
   std::string m;
   S(const std::string& arg) try : m(arg, 100) {
      std::cout << "constructed, mn = " << m << '\n';
   } catch(const std::exception& e) {
      std::cerr << "arg=" << arg << " failed: " << e.what() << '\n';
   } // 此处隐式 throw;
};

异常规范

C++使用跟在函数名后面的 throw 说明函数会抛出的异常。( C++11已废用

例:

void func1() throw(std::bad_exception, std::bad_alloc);

该异常规范已经在C++11被弃用,但是仍然可能存在于某些代码中。

C++ 还引入了一个新的关键字: noexpect

noexpect 有两种用法:

  • 用于保证函数不会抛出异常。
    void func1() noexcept;
    

    若一个函数被 noexcept 修饰,但是仍然抛出了异常,则编译器会出现警告。

  • 用来判断函数是否被 noexcept 修饰:
    noexcept(func1) == true
    

    noexcept 表达式的返回值是一个右值 bool 类型。

备注

  • noexcept 可以用来帮助编译器优化代码

  • noexcept 与 throw() 相同,尽管都为函数声明的一部分,但是不允许出现在 typedef 中

  • 函数违反了 noexcept 并不会导致调用 unexpected() ,也不会出现 栈解退

栈解退

测试代码如下:

#include <iostream>
#include <exception>

void func1();
void func2();
using namespace std;

int main() {
   try {
      try {
            func1();
            cout<<"第二层try块"<<endl;
      } catch (exception&e) {
            cout<<"第二层catch"<<endl;
      }
      cout<<"第一层try块"<<endl;
   } catch (std::exception&e) {
      cout<<"第一层catch"<<endl;
   }
   return 0;
}

void func1(){
   func2();
   cout<<"func1()"<<endl;
}
void func2(){
   throw std::bad_exception();
}

运行结果如下:

第二层catch

第一层try块

也就是说:当 func2 引发异常后,函数直接跳转到了 第二层try块 的结尾,然后直接进入 第二层catch 块。而若是函数返回的话,则会先返回到 func1() 然后再执行 第二层try块 的剩余语句。

异常处理中以下行为被保证:1

  • 栈解退时函数调用栈的所有栈对象将被析构,而堆对象不会

  • 若类的析构函数抛出异常,则直接调用 std::terminate() 中断程序

  • 若类的构造函数抛出异常,则撤销所有已构造成员

  • catch中允许 非const到const派生类到基类数组和函数到指针 之间的类型转换(前提是使用引用)

  • 异常类型匹配将优先选择第一个可以处理该异常的catch子句

  • throw抛出的异常对象对象将被保留至异常被处理为止

  • catch子句获得的只是异常对象的副本(使用引用类型只是为了多态)

  • 异常对象必须是可复制的

  • 抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型

  • 若被catch的是基类指针,则派生类将被切片。

  • catch可以 throw; 将捕获的异常重新抛出,否则视为已处理

1

C++异常处理知多少(一)

异常中断

异常在两种情况下会引发中断:

  • 若函数引发的异常未被捕获(即 未捕获异常 ),则调用 std::terminate() 中断程序。

  • 若函数引发了不在异常规范列表中的异常(即 意外异常 ),则调用 std::unexpected 中断程序。

默认情况下 std::terminate() 将会调用 std::abort() 中断程序,通过 set_terminate() 可以自定义 std::terminate() 调用的函数。例如:

#include <iostream>
#include <exception>

using namespace std;

void myQuit(){
   cout<<"myQuit()"<<endl;
}

int main() {
   set_terminate(myQuit);
   throw bad_exception();
   return 0;
}

输出结果为:

myQuit()

进程已结束,退出代码 134 (interrupted by signal 6: SIGABRT)

备注

std::terminate()noexception 修饰,若 std::ter11.上篇文章说过,在异常抛出栈展开的时候,编译器会适当撤销函数退出前分配的局部空间,如果局部对象是类类型,则自动调用它的析构函数。但如果在函数内单独地使用new动态的分配了内存,而且在释放资源之前发生了异常,那么栈展开时这个动态空间将不会被释放。而由类类型对象分配的资源不管是静态的还是动态的一般都会适当的被释放,因为栈展开时保证调用它们的析构函数。因此,在可能存在异常的程序以及分配资源的程序最好使用类来管理那些资源,看一个例子:nate() 抛出异常,则会通过 std::abort 终止程序,但是不会再有 异常中断提示

默认情况下 std::unexpected 将会调用 std::terminate() 中断程序,可以通过 set_unexpected 自定义 std::unexpected 调用的函数。例如:

#include <iostream>
#include <exception>

using namespace std;

void myQuit(){
   cout<<"myQuit()"<<endl;
}

void myUnexcepted(){
   cout<<__FUNCTION__ <<endl;
}

int main() throw(bad_alloc) {
   set_terminate(myQuit);
   set_unexpected(myUnexcepted);
   throw bad_exception();
   return 0;
}

输出结果为:

myUnexcepted
myQuit()

进程已结束,退出代码 134 (interrupted by signal 6: SIGABRT)

需要注意的是:若 std::unexpected 中抛出了异常,则:

  • 若新抛出的异常与原来的异常规范匹配,则从抛出异常的位置继续处理异常

  • 若新抛出的异常与原来的异常规范不匹配,且异常规范中没有 std::bad_exception ,则调用 std::terminate 终止程序。

  • 若新抛出的异常与原来的异常规范不匹配,且异常规范中包含了 std::bad_exception ,则将新抛出的异常替换为 std::bad_exception

信号

C++信号处理对应的头文件为 signal.hcsignal,通过其提供的 signal 函数可以设置信号的处理。

signal 函数声明如下:

typedef void (*__sighandler_t) (int)
__sighandler_t signal (int __sig, __sighandler_t __handler)

其中, typedef void (*__sighandler_t) (int) 定义了一个返回类型为void,参数类型为int的函数指针 __sighandler_t2 __sighandler_t signal (int __sig, __sighandler_t __handler) 定义了一个返回类型为函数指针,参数分别为 信号类型信号处理函数signal 函数。该函数被 throw() 修饰,若抛出异常将引起程序中断。

signal 返回旧的 信号处理函数指针 ,参数为 信号信号处理函数指针。当 signal 发生错误时,返回类型为 SIG_ERR

信号类型

其中,信号类型及其默认行为如下:3

信号类型

信号数值

信号说明

说明

SIGUP

1

hangup

许多服务进程在接受到该信号后会重新读取配置文件。但实际功能是通知进程它的控制终端被断开。缺省行为是终止进程。

SIGINT

2

interrupt

Shell的 Ctrl + C 。该信号的正式名字是中断信号。缺省行为是终止进程。

SIGQUIT

3

quit

Shell的 Ctrl + / 。用于通知应用程序从容的(译注:即在结束前执行一些退出动作)关闭。缺省行为是终止进程,并且创建一个核心转储。

SIGILL

4

illegal instr

若执行的进程中包含非法指令,操作系统将向该进程发送SIGILL信号。可以尝试捕获该信号来协助调试。缺省行为是终止进程,并且创建一个核心转储。

SIGTRAP

5

trace trap

用于调试目的。通知被调试进程达到断点。一旦该信号被交付,被调试的进程将停止,并且它的父进程将接到通知。缺省行为是终止进程,并且创建一个核心转储。

SIGABRT

6

abort()

在异常终止(abort)一个进程的同时创建核心转储。然而如果该信号被捕获,并且信号处理句柄没有返回,那么进程不会终止。缺省行为是终止进程,并且创建一个核心转储。

SIGFPE

8

floating point exception

当进程发生一个浮点错误时,SIGFPE信号被发送给该进程。对于那些处理复杂数学运算的程序,一般会建议你捕获该信号。缺省行为是终止进程,并且创建一个核心转储。

SIGKILL

9

kill

该信号不能被捕获或忽略。一旦该信号被交付给一个进程,那么这个进程就会终止。但在处理一个“非中断操作”(比如磁盘I/O)可能不会终止进程。此时会造成进程死锁。缺省行为是终止进程。

SIGBUS

10

bus error

CPU检测到数据总线上的错误时将产生SIGBUS信号。当程序尝试去访问一个没有正确对齐的内存地址时就会产生该信号。缺省行为是终止进程,并且创建一个核心转储。

SIGSEGV

11

segmentation violation

当程序没有权利访问一个受保护的内存地址时,或者访问无效的虚拟内存地址(脏指针,dirty pointers,译注:由于没有和后备存储器中内容进行同步而造成。关于野指针,可以参见http://en.wikipedia.org/wiki /Wild_pointer 的解释。)时,会产生这个信号。缺省行为是终止进程,并且创建一个核心转储。

SIGSYS

12

non-existent system call invoked

进程执行一个不存在的系统调用时操作系统会交付该信号。缺省行为是终止进程,并且创建一个核心转储。

SIGPIPE

13

write on a pipe with no one to read it

若进程尝试对管道执行写操作,然而管道的另一边却没有回应时,操作系统会将SIGPIPE信号交付给这个打算写入的进程。缺省行为是终止进程。

SIGALRM

14

alarm clock

在进程的计时器到期的时候,SIGALRM信号会被交付给进程。缺省行为是终止进程。

SIGTERM

15

software termination signal from kill

通知该进程终止,并且在终止之前做一些清理活动。SIGTERM信号是Unix的kill命令发送的缺省信号,同时也是操作系统关闭时向进程发送的缺省信号。缺省行为是终止进程。

SIGURG

16

urgent condition on IO channel

在进程已打开的套接字上发生某些情况时,SIGURG将被发送给该进程。如果进程不捕获这个信号的话,那么将被丢弃。缺省行为是丢弃这个信号。

SIGSTOP

17

sendable stop signal not from tty

本信号不能被捕获或忽略。一旦进程接收到SIGSTOP信号,它会被暂停,直到接收到另一个SIGCONT信号为止。缺省行为是暂停进程,直到接收到一个SIGCONT信号为止。

SIGTSTP

18

stop signal from tty

暂停进程,Shell的 Ctrl + Z 。缺省行为是暂停进程,直到接收到一个SIGCONT信号为止。

SIGCONT

19

continue a stopped process

当进程停止的时候,这个信号用来告诉进程恢复运行。不能被忽略或阻塞,但可以被捕获。这样做很有意义:因为进程大概不愿意忽略或阻塞SIGCONT信号,否则,如果进程接收到SIGSTOP或SIGSTP的时候该怎么办?缺省行 为是丢弃该信号。

SIGCHLD

20

to parent on child stop or exit

SIGCHLD是由Berkeley Unix引入的,并且比SRV 4 Unix上的实现有更好的接口。(如果信号是一个没有追溯能力的过程(not a retroactive process),那么BSD的SIGCHID信号实现会比较好。在system V Unix的实现中,如果进程要求捕获该信号,操作系统会检查是否存在有任何未完成的子进程(这些子进程是已经退出exit)的子进程,并且在等待调用 wait的父进程收集它们的状态)。如果子进程退出的时候附带有一些终止信息(terminating information),那么信号处理句柄就会被调用。所以,仅仅要求捕获这个信号会导致信号处理句柄被调用(译注:即是上面说的“信号的追溯能 力”),而这是却一种相当混乱的状况。)一旦一个进程的子进程状态发生改变,SIGCHLD信号就会被发送给该进程。就像我在前面章节提到的,父进程虽然 可以fork出子进程,但没有必要等待子进程退出。一般来说这是不太好的,因为这样的话,一旦进程退出就可能会变成一个僵尸进程。可是如果父进程捕获 SIGCHLD信号的话,它就可以使用wait系列调用中的某一个去收集子进程状态,或者判断发生了什么事情。当发送SIGSTOP、SIGSTP或SIGCONF信号给子进程时,SIGCHLD信号也会被发送给父进程。缺省行为是丢弃该信号。

SIGTIIN

21

to readers pgrp upon background tty read

当一个后台进程尝试进行一个读操作时,SIGTTIN信号被发送给该进程。进程将会阻塞直到接收到SIGCONT信号为止。缺省行为是停止进程,直到接收到SIGCONT信号。

SIGTTOU

22

like TTIN if (tp->t_local&LTOSTOP)

SIGTTOU信号与SIGTTIN很相似,不同之处在于SIGTTOU信号是由于后台进程尝试对一个设置了TOSTOP属性的tty执行写操作时才会 产生。然而,如果tty没有设置这个属性,SIGTTOU就不会被发送。缺省行为是停止进程,直到接收到SIGCONT信号。

SIGIO

23

input/output possible signal

如果进程在一个文件描述符上有I/O操作的话,SIGIO信号将被发送给这个进程。进程可以通过fcntl调用来设置。缺省行为是丢弃该信号

SIGCPU

24

exceeded CPU time limit

如果一旦进程超出了它可以使用的CPU限制(CPU limit),SIGXCPU信号就被发送给它。这个限制可以使用setrlimit设置。缺省行为是终止进程。

SIGXFSZ

25

exceeded file size limit

如果一旦进程超出了它可以使用的文件大小限制,SIGXFSZ信号就被发送给它。稍后我们会继续讨论这个信号。缺省行为是终止进程

SIGVTALRM

26

virtual time alarm

如果一旦进程超过了它设定的虚拟计时器计数时,SIGVTALRM信号就被发送给它。缺省行为是终止进程。

SIGPROF

27

profiling time alarm

当设置了计时器时,SIGPROF是另一个将会发送给进程的信号。缺省行为是终止进程。

SIGWINCH

28

window size changes

当进程调整了终端的尺寸时,SIGWINCH信号被发送给该进程。缺省行为是丢弃该信号。

SIGUSR1

29

user defined signal 1

自定义信号,缺省行为是终止进程。

SIGUSR2

30

user defined signal 2

自定义信号,缺省行为是终止进程。

备注

信号 0 被用于 kill(pid, 0) 这个函数。用来在不发送信号的情况下测试进程是否存在

虚拟信号处理函数

除了自定义信号处理函数外,C++还附带了三个虚拟信号处理函数(fake signal functions):4

函数指针

函数定义

函数说明, 解释

SIG_ERR

(__sighandler_t) -1)

Error return

signal() 发生错误,则返回该值

SIG_DFL

(((__sighandler_t) 0))

Default action

调用信号的默认行为

SIG_IGN

((__sighandler_t) 1)

Ignore signal

忽视信号

SIG_HOLD

((__sighandler_t) 2)

Add signal to hold mask

Add sig to the process’s signal mask

but leave the disposition of sig unchanged.

触发信号 5

使用 raise 可以向自己的程序发送信号,其声明如下:

int raise (int __sig) throw()

例如:

#include <iostream>
#include <csignal>
int main() {
   signal(SIGINT, sig2);
   raise(SIGINT);
   return 0;
}

输出如下:

信号中断!

进程已结束,退出代码 2
2

关于typedef void (*sighandler_t)(int)的理解

3

C++ signal的使用

4

sighold,sigset,sigrelse

5

C++信号处理

零成本异常并不是零成本

C++ 中有两种常见的异常处理模型。一种是在异常方式时通过做一系列工作来更新程序状态。例如,进入异常处理作用域或者退出作用域。另一种模型是使用元数据来描述发生异常是应该做什么,运行时没有对状态的显式管理。相反,异常机制通过查看程序计数器和查询元数据来推断状态

基于元数据的异常处理通常被 误导性地称为零成本异常 ,这听起来像是异常没有成本。事实上完全相反,基于元素的异常应当被称为 超级昂贵的异常

基于元数据的异常处理的要点是:在主线(非异常)代码路径中没有用于异常支持的代码,异常发生的次数很少,因此您最终会得到更好的性能。

模式

运行时管理

基于元数据

主线代码

在运行时更新状态

发生异常

查询状态以找到正确的处理程序

获取程序计数器并找到适用它的元数据,查询元数据以找到正确的处理程序

请注意:使用基于元数据的所谓”零成本“异常实际上会导致抛出异常的成本显著增加,因为抛出异常的机器必须找到元数据以便查找要运行的处理程序。此元数据通常以 针对大小而非速度的格式 储存,因此必须在抛出异常时进行额外的工作以解码数据来找到正确的处理程序

“零成本异常”的名字意指没有生成额外的代码来防止异常发生

但即使是这样,也并不是零成本异常就和没有异常一样

异常的存在意味着代码生成受制于隐式约束:在执行任何可能引发异常的操作之前,如果对象对于一个异常处理是可见的,那么编译器必须将对象状态储存到内存中。(任何带有遇析构函数的对象都是可见的,因为异常处理程序可能必须运行析构函数)

简单来讲,潜在的可抛出异常操作限制了编译器优化可观察对象的能力,因为异常流和主线代码是相互独立的

这些成本是肉眼不可见的。他们会导致失去优化的机会

零成本例外很好(尽管用词不当),但请注意,成本实际上并不为零。