岳阳网站开发培训,外贸网站平台有几个,大连模板建站系统,企业信息系统官网文章目录 第24章 错误处理24.1 assert.h: 诊断24.2 errno.h: 错误24.2.1 perror函数和strerror函数 24.3 signal.h: 信号处理24.3.1 信号宏24.3.2 signal函数24.3.3 预定义的信号处理函数24.3.4 raise函数 24.4 setjmp.h: 非局部跳转问与答写在… 文章目录 第24章 错误处理24.1 assert.h: 诊断24.2 errno.h: 错误24.2.1 perror函数和strerror函数 24.3 signal.h: 信号处理24.3.1 信号宏24.3.2 signal函数24.3.3 预定义的信号处理函数24.3.4 raise函数 24.4 setjmp.h: 非局部跳转问与答写在最后 第24章 错误处理
——编写无错程序的方法有两种但只有第三种写程序的方法才行得通。
学习C语言的学生所编写的程序在遇到异常输入时经常无法正常运行但真正商业用途的程序却必须“非常稳健”即能够从错误中恢复正常而不至于崩溃。为了使程序非常稳健我们需要能够预见程序执行时可能遇到的错误包括对每个错误进行检测并提供错误发生时的合适行为。 本章讲述两种在程序中检测错误的方法调用assert宏以及测试errno变量。24.1节介绍了assert.h头assert宏就是在这里定义的。24.2节讨论了errno.h头其中定义了errno变量。这一节还包含perror函数与strerror函数这两个函数分别来自stdio.h和string.h它们与errno变量紧密相关。 24.3节讲解如何检测并处理称为信号的条件一些信号用于表示错误。处理信号的函数在signal.h头中声明。 最后24.4节探讨setjmp/longjmp机制它们经常用于响应错误。setjmp和longjmp都属于setjmp.h头。 错误的检测和处理并不是C语言的强项。C语言对运行时错误以多种形式表示而没有提供一种统一的方式。而且在C程序中需要由程序员编写检测错误的代码。因此很容易忽略一些可能发生的错误。一旦发生某个被忽略的错误程序经常可以继续运行虽然这样也不是很好。C、Java和C#等较新的语言具有“异常处理”特性可以更容易地检测和响应错误。 24.1 assert.h: 诊断
void assert(scalar expression);assert定义在assert.h中。它使程序可以监控自己的行为并尽早发现可能会发生的错误。
虽然assert实际上是一个宏但它是按照函数的使用方式设计的。assert有一个参数这个参数必须是一种“断言”——一个我们认为在正常情况下一定为真的表达式。每次执行assert时它都会检查其参数的值。如果参数的值不为0assert什么也不做如果参数的值为0assert会向stderr标准误差流22.1节写一条消息并调用abort函数26.2节终止程序执行。 例如假定文件demo.c声明了一个长度为10的数组a我们关心的是demo.c程序中的语句 a[i] 0; 可能会由于i不在0~9之间而导致程序失败。可以在给a[i]赋值前使用assert宏检查这种情况
assert(0 i i 10) ; /* checks subscript first */
a[i] 0; /* now does the assignment */ 如果i的值小于0或者大于等于10程序在显出类似下面的消息后会终止
Assertion failed: 0 i i 10, file demo.c, line 109 C99对assert做了两处小修改。C89标准指出assert的参数必须是int类型的。C99放宽了要求允许参数为任意标量类型因此在assert的原型中出现了单词scalar。例如现在参数可以为浮点数或指针。此外C99要求失败的assert显示其所在的函数名。C89只要求assert以文本格式显示参数、源文件及源文件中的行号。C99建议的消息格式为 Assertion failed: expression, function abc, file xyz, line nnn. 根据编译器的不同assert生成的消息格式也不尽相同但它们都应包含标准要求的信息。例如GCC在上述情况下给出如下的消息 a.out: demo.c:109: main: Assertion 0 i i 10 failed. assert有一个缺点因为它引入了额外的检查所以会增加程序的运行时间。偶尔使用一次assert可能对程序的运行速度没有很大影响但在实时程序中这么小的运行时间增加可能也是无法接受的。因此许多程序员在测试过程中会使用assert但当程序最终完成时就会禁止assert。要禁止assert很容易只需要在包含assert.h之前定义宏NDEBUG即可
#define NDEBUG
#include assert.hNDEBUG宏的值不重要只要定义了NDEBUG宏即可。一旦之后程序又有错误发生就可以去掉NDEBUG宏的定义来重新启用assert。 请注意!!不要在assert中使用有副作用的表达式包括函数调用。万一某天禁止了assert这些表达式将不会再被求值。考虑下面的例子 assert((p malloc(n)) ! NULL);一旦定义了NDEBUGassert就会被忽略并且malloc不会被调用。 函数assert是在程序运行期间做诊断工作从C11开始引入的静态断言_Static_assert可以把检查和诊断工作放在程序编译期间进行18.7节。 24.2 errno.h: 错误 标准库中的一些函数通过向errno.h中声明的int类型errno变量存储一个错误码正整数来表示有错误发生。errno可能实际上是个宏。如果确实是宏C标准要求它表示左值4.2节以便像变量一样使用。大部分使用errno变量的函数集中在math.h但也有一些在标准库的其他部分中。 假设我们需要使用一个库函数该库函数通过给errno赋值来产生程序运行出错的信号。在调用这个函数之后我们可以检查errno的值是否为零。如果不为零则表示在函数调用过程中有错误发生。举例来说假如需要检查sqrt函数23.3节的调用是否出错可以使用类似下面的代码
errno 0;
y sqrt(x);
if (errno ! 0 ) { fprintf(stderr, sqrt error; program terminated.\n); exit(EXIT_FAILURE);
} 当使用errno来检测库函数调用中的错误时在函数调用前将errno置零非常重要。虽然在程序刚开始运行时errno的值为零但有可能在随后的函数调用中已经被改动了。库函数不会将errno清零这是程序需要做的事情。 当错误发生时向errno中存储的值通常是EDOM或ERANGE。这两个宏都定义在errno.h中。这两个值代表调用数学函数时可能发生的两种错误: 定义域错误EDOM传递给函数的一个参数超出了函数的定义域。例如用负数作为sqrt的参数就会导致定义域错误。取值范围错误ERANGE函数的返回值太大无法用返回类型表示。例如用1000作为exp函数23.3节的参数就经常会导致取值范围错误因为 e 1000 {e^{1000}} e1000太大导致无法在大多数计算机上用double类型表示。
一些函数可能会同时导致这两种错误。可以用errno分别与EDOM和ERANGE比较然后确定究竟发生了哪种错误。 C99在errno.h中增加了EILSEQ宏。特定头尤其是wchar.h头25.5节中的库函数在发生编码错误22.3节时把EILSEQ的值存储到errno中。 24.2.1 perror函数和strerror函数
void perror(const char *s); //来自stdio.h
char *strerror(int errnum); //来自string.h 下面看两个与变量errno有关的函数不过这两个函数都不属于errno.h。 当库函数向errno存储了一个非零值时可能会希望显示一条描述这种错误的消息。一种实现方式是调用perror函数在stdio.h中声明它会按顺序显示以下信息(1)调用perror的参数(2)一个冒号(3)一个空格(4)一条出错消息消息的内容根据errno的值决定(5)一个换行符。perror函数会输出到stderr流22.1节而不是标准输出。下面是一个使用perror的例子 errno 0;
y sqrt(x);
if (errno ! 0) { perror(sqrt error); exit(EXIT_FAILURE);
} 如果sqrt调用因定义域错误而失败perror会产生如下输出
sqrt error:Numerical argument out of domainperror函数在sqrt error后所显示的出错消息是由实现定义的。在这个例子中Numerical argument out of domain是与EDOM错误相对应的消息。ERANGE错误通常会对应于不同的消息例如Numerical result out of range。 strerror函数属于string.h。当以错误码为参数调用strerror时函数会返回一个指针它指向一个描述这个错误的字符串。例如调用 puts(strerror(EDOM));可能会显示
Numerical argument out of domainstrerror函数的参数通常是errno的值但以任意整数作为参数时strerror都能返回一个字符串。
strerror与perror函数密切相关。如果strerror的参数为errno那么perror所显示的出错消息与strerror所返回的消息是相同的。 24.3 signal.h: 信号处理 signal.h提供了处理异常情况称为信号的工具。信号有两种类型运行时错误例如除以0和发生在程序以外的事件。例如许多操作系统都允许用户中断或终止正在运行的程序C语言把这些事件视为信号。当有错误或外部事件发生时我们称产生了一个信号。大多数信号是异步的它们可以在程序执行过程中的任意时刻发生而不仅是在程序员所知道的特定时刻发生。由于信号可能会在任何意想不到的时刻发生因此必须用一种独特的方式来处理它们。 本节按C标准中的描述来介绍信号。这里对信号谈得很有限但实际上信号在UNIX中的作用很大。这里不作详细讨论。 24.3.1 信号宏 signal.h定义了一系列的宏用于表示不同的信号。表24-1中列出了这些宏以及它们的含义。每个宏的值都是一个正整型常量。C语言的实现可以提供更多的信号宏只要宏的名字以SIG和一个大写字母开头就行。特别地UNIX实现提供许多额外的信号宏。 表24-1 信号
宏名含义SIGABRT异常终止可能由于调用abort导致SIGFPE在算术运算中发生错误可能是除以0或溢出SIGILL无效指令SIGINT中断SIGSEGV无效存储访问SIGTERM终止请求
C标准并不要求表24-1中列出的信号都自动产生因为对于某个特定的计算机或操作系统不是所有的信号都有意义。大多数C语言的实现都至少支持其中的一部分。 24.3.2 signal函数
void (*signal(int sig, void (*func)(int)))(int);signal.h提供了两个函数raise和signal。这里先讨论signal函数它会安装一个信号处理函数以便将来给定的信号发生时使用。signal函数的使用比它的原型看起来要简单得多。它的第一个参数是特定信号的编码第二个参数是一个指向会在信号发信生时处理这一号的函数的指针。例如下面的signal函数调用为SIGINT信号安装了一个处理函数 signal(SIGINT, handler); handler就是信号处理函数的函数名。一旦随后在程序执行过程中出现了SIGINT信号handler函数就会自动被调用。
每个信号处理函数都必须有一个int类型的参数且返回类型为void。当一个特定的信号产生并调用相应的处理函数时信号的编码会作为参数传递给处理函数。知道是哪种信号导致了处理函数被调用是十分有用的尤其是它允许我们对多个信号使用同一处理函数。 信号处理函数可以做许多事。这可能包含忽略该信号、执行一些错误恢复或终止程序。然而除非信号是由调用abort函数26.2节或raise函数引发的否则信号处理函数不应该调用库函数或试图使用具有静态存储期18.2节的变量。但这些规则也有例外。 一旦信号处理函数返回程序就会从信号发生点恢复并继续执行但有2种例外情况
如果信号是SIGABRT当处理函数返回时程序会异常地终止如果处理的信号是SIGFPE那么处理函数返回的结果是未定义的。也就是说不要处理它。 虽然signal函数有返回值但经常被丢弃。返回值是指向指定信号的前一个处理函数的指针。如果需要可以将它保存在变量中。特别是如果打算恢复原来的处理函数那么就需要保留signal函数的返回值 void (*orig_handler)(int); /* function pointer variable */
...
orig_handler signal(SIGINT, handler);这条语句将handler函数安装为SIGINT的处理函数并将指向原来的处理函数的指针保存在变量orig_handler中。如果要恢复原来的处理函数我们需要使用下面的代码
signal(SIGINT, orig_handler); /* restores original handler */24.3.3 预定义的信号处理函数 除了编写自己的信号处理函数还可以选择使用signal.h提供的预定义的处理函数。有两个这样的函数每个都是用宏表示的。 SIG_DFL。SIG_DFL按“默认”方式处理信号。可以使用下面的调用安装SIG_DFL signal(SIGINT, SIG_DFL); /* use default handler */ 调用SIG_DFL的结果是由实现定义的但大多数情况下会导致程序终止。 SIG_IGN。调用 signal(SIGINT, SIG_IGN); /* ignore SIGINT signal */ 指明随后当信号SIGINT产生时忽略该信号。
除了SIG_DFL和SIG_IGNsignal.h可能还会提供其他的信号处理函数其函数名必须是以SIG_和一个大写字母开头。当程序刚开始执行时根据不同的实现每个信号的处理函数都会被初始化为SIG_DFL或SIG_IGN。 signal.h还定义了另一个宏SIG_ERR它看起来像是个信号处理函数。实际上SIG_ERR是用来在安装处理函数时检测是否发生错误的。如果一个signal调用失败即不能对所指定的信号安装处理函数就会返回SIG_ERR并在errno中存入一个正值。因此为了测试signal调用是否失败可以使用如下代码 if (signal(SIGINT, handler) SIG_ERR) { perror(signal(SIGINT, handler) failed); ...
}在整个信号处理机制中有一个棘手的问题如果信号是由处理这个信号的函数引发的那会怎样呢为了避免无限递归C89标准为程序员安装的信号处理函数引发信号的情况规定了一个两步的过程。首先要么把该信号对应的处理函数重置为SIG_DFL默认处理函数要么在处理函数执行的时候阻塞该信号。SIGILL是一个特殊情况当SIGILL发生时这两种行为都不需要。然后再调用程序员提供的处理函数。 请注意!!信号处理完之后处理函数是否需要重新安装是由实现定义的。UNIX实现通常会在使用处理函数之后保持其安装状态但其他实现可能会把处理函数重置为SIG_DFL。在后一种情况下处理函数可以通过在其返回前调用signal函数来实现自身的重新安装。 C99对信号处理过程做了一些小的改动。当信号发生时实现不仅可以禁用该信号还可以禁用别的信号。对于处理SIGILL或SIGSEGV信号以及SIGFPE信号的信号处理函数函数返回的结果是未定义的。C99还增加了一条限制如果信号是因为调用abort函数或raise函数而产生的信号处理函数本身一定不能调用raise函数。(我的理解是raise不能连续发生) 24.3.4 raise函数
int raise(int sig); 通常信号是由于运行时错误或外部事件而产生的但有时候如果程序可以触发信号会非常方便。raise函数就可以实现这一目的。raise函数的参数指定所需信号的编码
raise(SIGABRT); /* raises the SIGABRT signal */ raise函数的返回值可以用来测试调用是否成功0代表成功非0则代表失败。 下面的程序说明了如何使用信号。首先给SIGINT信号安装了一个惯用的处理函数并小心地保存了原先的处理函数然后调用raise_sig产生该信号接下来程序将SIG_IGN设置为SIGINT的处理函数并再次调用raise_sig最后它重新安装信号SIGINT原先的处理函数并最后调用一次raise_sig。 /*
tsignal.c
--Tests signals
*/
#include signal.h
#include stdio.h
void handler(int sig);
void raise_sig(void);
int main(void)
{ void (*orig_handler)(int); printf(Installing handler for signal %d\n, SIGINT); orig_handler signal(SIGINT, handler); raise_sig(); printf(Changing handler to SIG_IGN\n); signal(SIGINT, SIG_IGN); raise_sig(); printf(Restoring original handler\n); signal(SIGINT, orig_handler); raise_sig(); printf(Program terminates normally\n); return 0;
}
void handler(int sig)
{ printf(Handler called for signal %d\n, sig);
}
void raise_sig(void)
{ raise(SIGINT);
}当然调用raise并不需要在单独的函数中。这里定义raise_sig函数只是为了说明一点无论信号是从哪里产生的无论是在main函数中还是在其他函数中它都会被最近安装的该信号的处理函数捕获。
这段程序的输出可能会有多种。下面是一种可能的输出形式
Installing handler for signal 2
Handler called for signal 2
Changing handler to SIG_IGN
Restoring original handler这个输出结果表明我们的实现把SIGINT的值定义为2而且SIGINT原先的处理函数一定是SIG_DFL。如果是SIG_IGN应该会看到信息Program terminates normally。最后我们注意到SIG_DFL会导致程序终止但不会显示出错消息。 24.4 setjmp.h: 非局部跳转
int setjmp(jmp_buf env);
_Noreturn void longjmp(jmp_buf env, int val); 通常情况下函数会返回到它被调用的位置。我们无法使用goto语句6.4节使它转到其他地方因为goto只能跳转到同一函数内的某个标号处。但是setjmp.h可以使一个函数直接跳转到另一个函数不需要返回。 在setjmp.h中最重要的内容就是setjmp宏和longjmp函数。setjmp宏“标记”程序中的一个位置随后可以使用longjmp跳转到该位置。虽然这一强大的机制可以有多种潜在的用途但它主要被用于错误处理。 如果要为将来的跳转标记一个位置可以调用setjmp宏调用的参数是一个jmp_buf类型在setjmp.h中声明的变量。setjmp宏会将当前“环境”包括一个指向setjmp宏自身位置的指针保存到该变量中以便将来可以在调用longjmp函数时使用然后返回0。 要返回setjmp宏所标记的位置可以调用longjmp函数调用的参数是调用setjmp宏时使用的同一个jmp_buf类型的变量。longjmp函数会首先根据jmp_buf变量的内容恢复当前环境然后从setjmp宏调用中返回——这是最难以理解的。这次setjmp宏的返回值是val就是调用longjmp函数时的第二个参数。如果val的值为0那么setjmp宏会返回1。 请注意!!一定要确保作为longjmp函数的参数之前已经被setjmp调用初始化了。还有一点很重要包含setjmp最初调用的函数一定不能在调用longjmp之前返回。如果两个条件都不满足调用longjmp会导致未定义的行为。程序很可能会崩溃。 总而言之setjmp会在第一次调用时返回0随后longjmp将控制权重新转给最初的setjmp宏调用而setjmp在这次调用时会返回一个非零值。明白了吗我们可能需要一个例子。 下面的程序使用setjmp宏在main函数中标记一个位置然后函数f2通过调用longjmp函数返回到这个位置。 /*
tsetjmp.c
--Tests setjmp/longjmp
*/
#include setjmp.h
#include stdio.h
jmp_buf env;
void f1(void);
void f2(void);
int main(void)
{ if (setjmp(env) 0) printf(setjmp returned 0\n); else { printf(Program terminates: longjmp called\n); return 0; } f1(); printf(Program terminates normally\n); return 0;
} void f1(void)
{ printf(f1 begins\n); f2(); printf(f1 returns\n);
}void f2(void)
{ printf(f2 begins\n); longjmp(env, 1); printf(f2 returns\n);
} 这段程序的输出如下
setjmp returned 0
f1 begins
f2 begins
Program terminates: longjmp called setjmp宏的最初调用返回0因此main函数会调用f1。接着f1调用f2f2使用longjmp函数将控制权重新转给main函数而不是返回到f1。当longjmp函数被执行时控制权重新回到setjmp宏调用。这一次setjmp宏返回1就是在longjmp函数调用时所指定的值。 问与答 问1书上说在调用可能修改errno的库函数之前把errno设置为0是很重要的。但是我见过一些UNIX程序在没有把errno设置为0的情况下就对其进行测试。这是什么缘故呢? 答UNIX程序通常包含对操作系统函数的调用。这些系统调用需要用到errno但使用方法与本节提到的方法略有不同。当这样的调用失败时除了在errno中存储一个值之外还会返回一个特殊的值例如-1或空指针。程序不需要在这些调用之前往errno中存储0因为函数的返回值本身就可以表明发生了错误。C标准库中的一些函数也是这样的errno更多地用于指明错误类型而不是用于发出出错信号。 问2我使用的errno.h版本中除了EDOM和ERANGE以外还定义了其他的宏。这是合法的吗? 答是合法的。C标准允许使用宏表示其他错误条件只要宏的名字以字母E开头并且其后有一个数字或大写字母。UNIX实现中通常会定义许多这样的宏。 问3一些表示信号的宏的名字含义比较模糊比如SIGFPE和SIGSEGV。这些名字是如何得来的呢 答信号的名字可以追溯到早期的C编译器这些编译器运行在DECPDP-11计算机上。PDP-11的硬件可以检测一些错误诸如“Floating Point Exception”和“Segmentation Violation”。 问4我很好奇。书上说除非信号是由abort函数或raise函数引发的否则信号处理函数不应该调用库函数。但你又说有例外情况是什么例外呢 答信号处理函数可以调用singal函数只要第一个参数是当前正在处理的信号就可以。这一限定条件很重要因为它允许信号处理函数自身进行重新安装。在C99中信号处理函数还可以调用abort函数或_Exit函数26.2节。 问5接着上一个问题信号处理函数通常不能访问具有静态存储期的变量。这个规则的例外是什么 答这个问题要难回答一些。答案涉及signal.h头中声明的一个名为sig_atomic_t的类型。根据C标准sig_atomic_t是一个可以作为一个“原子实体”访问的整型。换句话说CPU可以用一条指令从内存中取出sig_atomic_t的值或将其存放到内存中而不需要用两条或更多条指令。通常把sig_atomic_t定义为int因为大多数CPU可以用一条指令存取int类型的值。
下面谈谈信号处理函数不可以访问静态变量这一规则的例外情况。C标准允许信号处理函数在sig_atomic_t类型的变量中存储值即使该变量具有静态存储期也可以前提是该变量声明为volatile。为了了解这一不可思议的规则产生的原因考虑信号处理函数要修改一个类型比sig_atomic_t宽一些的静态变量的情况。如果程序在信号发生之前从内存中取出了该变量的一部分并在信号处理完毕后取完该变量那么这个值就没有价值了。sig_atomic_t类型的变量可以一步取出所以不会出现这种问题。把变量声明为volatile会警告编译器变量的值随时可能改变。信号可能突然产生并调用信号处理函数来修改该变量。 问6程序tsignal.c在信号处理函数内调用了printf函数。这不是非法的吗 答如果信号处理函数是由raise或abort调用的那么就可以调用库函数。tsignal.c使用raise来调用信号处理函数。 问7setjmp会如何修改传递给它的参数呢C语言不是始终以值的形式传递参数吗 答C标准要求jmp_buf必须是一个数组类型因此传递给setjmp的实际上是一个指针。 问8我在使用setjmp时遇到一些问题。使用setjmp有什么限制吗 答按照C标准只有2种使用setjmp的方式是合法的。 作为表达式语句中的表达式可能会强制转换成void。 作为if、swtich、while、do或for语句中控制表达式的一部分。整个控制表达式必须符合下面的形式之一其中constexp是一个整型常量表达式而op是关系或判等运算符。 setjmp(...)!setjmp(...)constexpr op setjmp(...)setjmp(...) op constexpr
其他的用法会导致未定义的行为。 问9调用longjmp函数后程序中变量的值是什么 答大部分变量的值保留了longjmp函数被调用时的值。然而包含setjmp宏的函数中自动变量的值是不确定的除非该变量被声明为volatile或者在执行setjmp时没有被修改过。 问10在信号处理函数里调用longjmp函数合法吗 答是合法的只要该信号处理函数的调用不是由某个信号处理函数执行过程中触发的信号引发的。C99删除了这一限制。 写在最后 本文是博主阅读《C语言程序设计现代方法第2版·修订版》时所作笔记日后会持续更新后续章节笔记。欢迎各位大佬阅读学习如有疑问请及时联系指正希望对各位有所帮助Thank you very much!