C语言异常与断言接口与实现

标准库函数setjmp和longjmp

在C语言中,标准库函数setjmp和longjmp形成了结构化异常工具的基础。简单的说就是setjmp实例化处理程序,而longjmp产生异常

setjmp和longjmp是C语言所独有的,它们部分弥补了C语言有限的转移能力。与刺激的abort()和exit()相比,goto语句看
起来是处理异常的更可行方案。不幸的是,goto是本地的:它只能跳到所在函数内部的标号上,而不能将控制权转移到所在程序的任意地点(当然,除非你的所
有代码都在main体中)。

为了解决这个限制,C函数库提供了setjmp()和longjmp()函数,它们分别承担非局部标号和goto作用。头文件<setjmp.h>申明了这些函数及同时所需的jmp_buf数据类型。

函数说明:


int setjmp(jmp_buf env)


建立本地的jmp_buf缓冲区并且初始化,用于将来跳转回此处。这个子程序保存程序的调用环境于env参数所指的缓冲区,env将被longjmp使用。如果是从setjmp直接调用返回,setjmp返回值为0。如果是从longjmp恢复的程序调用环境返回,setjmp返回非零值。


void longjmp(jmp_buf env, int value)


恢复env所指的缓冲区中的程序调用环境上下文,env所指缓冲区的内容是由setjmp子程序调用所保存。value的值从longjmp传递给setjmplongjmp完成后,程序从对应的setjmp调用处继续执行,如同setjmp调用刚刚完成。如果value传递给longjmp零值,setjmp的返回值为1;否则,setjmp的返回值为value

成员类型:

jmp_buf 数组类型,例如:struct int[16]struct __jmp_buf_tag,用于保存恢复调用环境所需的信息。

jmp_buf 的定义:

typedef struct _jmp_buf
{
    int _jp[_JBLEN+1];
} jmp_buf[1];

这个是 setjmp.h 里的一行定义,把一个 struct 定义成一个数组。这样,在声明 jmp_buf 的时候,可以把数据分配到堆栈上。但是作为参数传递的时候则作为一个指针。

原理非常简单:

1.setjmp(j)设置“jump”点,用正确的程序上下文填充jmp_buf对象j。这个上下文包括程序存放位置、栈和框架指针,其它重要的寄存器和内存数据。当初始化完jump的上下文,setjmp()返回0值。

2.
以后调用longjmp(j,r)的效果就是一个非局部的goto或“长跳转”到由j描述的上下文处(也就是到那原来设置j的setjmp()处)。当作
为长跳转的目标而被调用时,setjmp()返回r或1(如果r设为0的话)。(记住,setjmp()不能在这种情况时返回0。)

通过有两类返回值,setjmp()让你知道它正在被怎么使用。当设置j时,setjmp()如你期望地执行;但当作为长跳转的目标时,setjmp()就从外面“唤醒”它的上下文。你可以用longjmp()来终止异常,用setjmp()标记相应的异常处理程序。

本文地址:http://www.cnblogs.com/archimedes/p/c-exception-assert.html,转载请注明源地址。

一个简单的例子:

#include <stdio.h>
#include <setjmp.h>

static jmp_buf buf;
void second(void) {
    printf("second\n");         // 打印
    longjmp(buf,1);             // 跳回setjmp的调用处 - 使得setjmp返回值为1
}
void first(void) {
    second();
    printf("first\n");          // 不可能执行到此行
}
int main() {
    if ( ! setjmp(buf) ) {
        first();                // 进入此行前,setjmp返回0
    } else {                    // 当longjmp跳转回,setjmp返回1,因此进入此行
        printf("main\n");       // 打印
    }
    return 0;
}

运行结果:

second
main

在下例中,setjmp被用于包住一个例外处理,类似trylongjmp调用类似于throw语句,允许一个异常返回给setjmp一个异常值。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>

void first(void);
void second(void);
static jmp_buf exception_env;
static int exception_type;

int main(void) {
    void *volatile mem_buffer;

    mem_buffer = NULL;

    if (setjmp(exception_env)) {
        /* 如果运行到这将产生一个异常*/
        printf("first failed, exception type %d\n", exception_type);
    } else {
        printf("calling first\n");
        first();
        mem_buffer = malloc(300); /* 分配内存 */
        printf("%s",strcpy((char*)mem_buffer, "first succeeded!")); /* ... 不会被执行 */
    }
    if (mem_buffer)
        free((void*) mem_buffer); /* 小心释放内存 */
    return 0;
}

void first(void) {
    jmp_buf my_env;

    printf("calling second\n");
    memcpy(my_env, exception_env, sizeof(jmp_buf));
    switch (setjmp(exception_env)) {
        case 3:
            /* 如果运行到这,表示有异常 */
            printf("second failed with type 3 exception; remapping to type 1.\n");
            exception_type = 1;

        default:
            memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
            longjmp(exception_env, exception_type); /* continue handling the exception */

        case 0:
            /* normal, desired operation */
            second();
            printf("second succeeded\n");  /* not reached */
    }
    memcpy(exception_env, my_env, sizeof(jmp_buf)); /* restore exception stack */
}

void second(void) {
    printf("entering second\n" );
    exception_type = 3;
    longjmp(exception_env, exception_type);
    printf("leaving second\n");
}

运行结果:

calling first
calling second
entering second
second failed with type 3 exception; remapping to type 1.
first failed, exception type 1

接口

Except接口在一系列宏指令和函数中包装了setjmp和longjmp,它们一起提供了一个结构化异常处理工具

异常是Except_T类型的一个全局或静态变量:

#ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED
#include <setjmp.h>
#define T Except_T
typedef struct T {
    char *reason;
} T;

Except_T结构只有一个字段,它可以初始化为一个描述异常的字符串,当发生一个未处理的异常时,才把字符串打印出来

异常处理程序处理的是异常的地址。异常必须是全局的或静态的变量,因此它们的地址唯一地标志了它们,异常e由宏指令引发或由函数引发:

#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)
void Except_raise(const T *e, const char *file,int line);

处理程序是由TRY-EXCEPT和TRY-FINALLY语句来实例化的,这两个语句用宏指令实现,这两个语句可以处理嵌套异常,也可以管理异常状态的数据

TRY-EXCEPT的语法是:

TRY

  S

EXCEPT(e1)

  S1

EXCEPT(e2)

  S2

……

EXCEPT(en)

  Sn

ELSE

  S0

END_TRY

看下面的代码:

int Allocation_handle = 0;
jmp_buf Allocate_Failed;

void *allocate(unsigned n)
{
    void *new = malloc(n);
    if(new)
        return new;
    if(Allocation_handle)
        longjmp(Allocate_Failed, 1);
    assert(0);
}

char *buf;
Allocation_handle = 1;
if(setjmp(Allocate_Failed)) {
    fprintf(stderr, "cound't allocate the  buff\n");
    exit(EXIT_FAILURE);
}
buf = allocate(4096);
Allocation_handle = 0;

上面的代码没有提供嵌套的处理程序,Allocation_handle标志的使用也很麻烦。

把Allocation_Failed变成一个异常,该异常是在malloc返回一个空指针时由allocate引发:

Except_T Allocate_Failed = {"Allocation failed"};
void *allocate(unsigned n)
{
    void *new = malloc(n);
    if(new)
        return new;
    RAISE(Allocation_Failed);
    assert(0);
}

如果客户调用程序代码想处理这个异常,那么它需要在TRY-EXCEPT语句内调用allocate:

extern Except_T Allocate_Failed;
char *buf;
TRY
    buf = allocate(4096);
EXCEPT(Allocate_Failed)
    fprintf(stderr, "could't allocate the buff\n");
    exit(EXIT_FAILURE);
END_TRY;

TRY-EXCEPT语句是用setjmp和longjmp来实现的

TRY-FINALLY语句的语法是:

TRY

  S

FINALLY

  S1

END_TRY

如果S没有产生任何异常,那么执行S1,然后继续执行END_TRY,如果S产生了异常,那么S的执行被中断,控制立即转给S1。S1执行完后,引
起S1执行的异常重新产生,使得它可以由前一个实例化的处理程序来处理。注意:S1是在两种情况中都必须执行的,处理程序可以用RERAISE宏指令显示
地重新产生异常

#define RERAISE Except_raise(Except_frame.exception, \
    Except_frame.file, Except_frame.line)

接口中的最后一个宏指令是:

#define RETURN switch (Except_stack = Except_stack->prev,0) default: return

RETURN宏指令用在TRY语句的内部,用来代替return语句

实现

Except接口中的宏指令和函数一起维护了一个记录异常状态以及实例化处理结构的堆栈。结构中的字段env就是setjmp和longjmp使用的某个jmp_buf,这个堆栈可以处理嵌套的异常

typedef struct Except_Frame Except_Frame;
struct Except_Frame {
    Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const T *exception;
};
extern Except_Frame *Except_stack;

Except_stack指向异常栈顶的异常帧,每个帧的prev字段指向它的前一帧,产生一个异常就是将异常的地址存在exception字段中,并分别在file和line字段中保存异常的附属信息--异常产生的文件以及行号

TRY从句将一个新的Except_Frame压入异常栈,并调用setjmp,由RAISE和RERAISE调用Except_raise填充栈
顶帧的字段exception、file和line,从异常栈中弹出栈顶Exception_Frame,然后调用longjmp,EXCEPT从句检查
该帧中的exception字段,决定应该用哪个处理程序。FINALLY从句执行清除代码,并重新产生已弹出的异常帧中存储的异常。

宏指令TRY、EXCEPT、ELSE、FINALLY_TRY一起将TRY-EXCEPT语句转化成如下形式的语句:

do {

  creat and push an Except_Frame

  if(first return from setjmp) {

    S

  } else if (exception is e1) {

    S1

  ……

  } else if (exception is en) {

    Sn

  } else {

    S0

  }

  if(an exception occurrend and wasn't handled)

    RERAISE;

} while(0)

Exception_Frame的空间分配很简单,在由TRY开始的do-while主体中的复合语句内部声明一个该类型的局部变量即可:

#define TRY do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {

在TRY语句内有四种状态,由下面的枚举标识符给出

enum { Except_entered=0, Except_raised,
       Except_handled,   Except_finalized };

setjmp的第一个返回值将Except_flag设置为Except_entered,表示进入TRY语句,并且将某个异常帧压入异常
栈,Except_entered必须为0,因为setjmp首次调用的返回值为0,随后,setjmp的返回值将被设为Except_raised,表
示发生了异常,处理程序将Except_flag的值设成Except_handled,表示处理程序已经对异常进行了处理。

#define TRY do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {
#define EXCEPT(e) \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;
#define ELSE \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } else { \
        Except_flag = Except_handled;
#define FINALLY \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
    } { \
        if (Except_flag == Except_entered) \
            Except_flag = Except_finalized;
#define END_TRY \
        if (Except_flag == Except_entered) Except_stack = Except_stack->prev; \
        } if (Except_flag == Except_raised) RERAISE; \
} while (0)

最后实现代码如下:

#include <stdlib.h>
#include <stdio.h>
#include "assert.h"
#include "except.h"
#define T Except_T
#ifdef WIN32
__declspec(thread)
#endif
Except_Frame *Except_stack = NULL;
void Except_raise(const T *e, const char *file,
    int line) {
    Except_Frame *p = Except_stack;
    assert(e);
    if (p == NULL) {
        fprintf(stderr, "Uncaught exception");
        if (e->reason)
            fprintf(stderr, " %s", e->reason);
        else
            fprintf(stderr, " at 0x%p", e);
        if (file && line > 0)
            fprintf(stderr, " raised at %s:%d\n", file, line);
        fprintf(stderr, "aborting...\n");
        fflush(stderr);
        abort();
    }
    p->exception = e;
    p->file = file;
    p->line = line;
    Except_stack = Except_stack->prev;
    longjmp(p->env, Except_raised);
}
时间: 2024-10-05 02:24:44

C语言异常与断言接口与实现的相关文章

Matlab与C语言程序应用编程接口

MATLAB作为世界顶尖的数学应用软件,以其强大的工程计算.算法研究.工程绘图.应用程序开发.数据分析和动态仿真等功能,在航空航天.机械制造和工程建筑等领域发挥着越来越重要的作用.而C语言功能丰富,使用灵活方便,目标程序效率高.既有高级语言的优点,又有低级语言的特点.因此,C语言是目前应用最广的编程语言.虽然MATLAB是一个完整的.功能齐全的编程环境,但在某些情况下,与外部环境的数据和程序的交互是非常必须而且有益的. 众所周知,MATLAB是用M语言编程,不能在M文件中直接调用C语言程序.可以

java的异常和断言学习笔记

异常 在java中,一个异常对象总是Throwable子类的实例. Error类体系描述了Java运行系统中的内部错误以及资源耗尽的情况. 由编程导致的错误,会导致RuntimeException异常.而其他错误原因导致的异常----例如,因为I/O错 误导致的曾经运行正确的程序出错,都不会导致RuntimeException异常. 从RuntimeException衍生出来的异常包括下面的问题:1〉错误的类型转换:2〉数组越界访问:3〉试 图访问一个空指针. 不是从RuntimeExcepti

Go 语言编译期断言

这篇文章是关于一个鲜为人知的让 Go 在编译期断言的方法.你可能不会使用它,但是了解一下也很有趣. 作为一个热身,来看一个在 Go 中熟知的编译期断言:接口满意度检查. 在这段代码(playground)中,var _ = 行确保类型 W 是一个 stringWriter,其由 io.WriteString检查. package main import "io" type W struct{} func (w W) Write(b []byte) (int, error) { retu

异常-anroid studio接口调用方法出错了,求帮忙

问题描述 anroid studio接口调用方法出错了,求帮忙 源码如下 运行程序后程序自动退出且报出以下异常 是哪里出错了啊? 解决方案 thread=1 有可能是强制转换类型错误. 解决方案二: 解决方案三: 空指针报错了你的callback可能没取到值,检查一下代码. 参考:http://www.cnblogs.com/top5/archive/2012/03/23/2414286.html 解决方案四: public void onAttach(Activity activity) {

C# 语言规范--1.9 接口

规范|接口     一个接口定义一个协定.实现接口的类或结构必须遵守其协定.接口可以包含方法.属性.索引器和事件作为成员.     示例 interface IExample{   string this[int index] { get; set; }   event EventHandler E;   void F(int value);   string P { get; set; }}public delegate void EventHandler(object sender, Eve

《C语言接口与实现:创建可重用软件的技术》一导读

前言 C语言接口与实现:创建可重用软件的技术 如今的程序员忙于应付大量关于API(Application Programming Interface)的信息.但是,大多数程序员都会在其所写的几乎每一个应用程序中使用API并实现API的库,只有少数程序员会创建或发布新的能广泛应用的API.事实上,程序员似乎更喜欢使用自己搞的东西,而不愿意查找能满足他们要求的程序库,这或许是因为写特定应用程序的代码要比设计可广泛使用的API容易. 不好意思,我也未能免俗:lcc(我和Chris Fraser为ANS

详解C++编程中断言static_assert的使用_C 语言

断言和用户提供的消息 C++ 语言支持可帮助您调试应用程序的三个错误处理机制:#error 指令.static_assert 关键字和 assert (CRT) 宏.所有的三种机制都会发出错误消息,其中两个还会测试软件断言.软件断言指定在程序的某个特定点应满足的条件.如果编译时断言失败,编译器将发出诊断消息和编译错误.如果运行时断言失败,操作系统将发出诊断消息并关闭应用程序. 备注 应用程序的生存期由预处理.编译和运行时阶段组成.每个错误处理机制都会访问在这三个阶段之一中可用的调试信息.若要有效

Go语言中的方法、接口和嵌入类型详解_Golang

概述 在 Go 语言中,如果一个结构体和一个嵌入字段同时实现了相同的接口会发生什么呢?我们猜一下,可能有两个问题: 1.编译器会因为我们同时有两个接口实现而报错吗? 2.如果编译器接受这样的定义,那么当接口调用时编译器要怎么确定该使用哪个实现? 在写了一些测试代码并认真深入的读了一下标准之后,我发现了一些有意思的东西,而且觉得很有必要分享出来,那么让我们先从 Go 语言中的方法开始说起. 方法 Go 语言中同时有函数和方法.一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的

HBase Thrift接口的常见使用问题和注意事项

HBase对于非Java语言提供了Thrift接口支持,这里结合对HBase Thrift接口(HBase版本为0.92.1)的使用经验,总结其中遇到的一些问题及其相关注意事项. 1. 字节的存放顺序 HBase中,由于row(row key和column family.column qualifier.time stamp)是按照字典序进行排序的,因此,对于short.int.long等类型的数据,通过Bytes.toBytes(-)转换成byte数组后,必须按照大端模式(高字节在低地址,低字