C编程
C 语言并没有直接支持错误处理(也叫做异常处理)。根据约定,程序员应当预防错误的发生,并测试函数的返回值。例如,-1 和 NULL 在一些函数中(如 socket()(Unix 套接字编程)或 malloc())被用来指示程序员需要注意的问题。在最坏的情况下,如果出现不可避免的错误且无法恢复,C 程序员通常会尝试记录该错误并“优雅”地终止程序。
C 语言提供了一个外部变量叫做 "errno",通过包含 <errno.h> 头文件,程序可以访问该变量。这个文件定义了在程序请求资源时,操作系统(如 Linux,具体定义在 include/asm-generic/errno.h)可能发生的错误。errno 变量通过 strerror(errno) 函数可以获取错误描述。
以下代码测试 malloc 库函数的返回值,检查动态内存分配是否成功:
#include <stdio.h> /* perror */
#include <errno.h> /* errno */
#include <stdlib.h> /* malloc, free, exit */
int main(void)
{
/* 指向字符的指针,要求动态分配 2,000,000,000 个存储元素
* (作为无符号长整型常量声明)。
* (如果你的系统可用内存小于 2GB,调用 malloc 将失败。)
*/
char *ptr = malloc(2000000000UL);
if (ptr == NULL) {
perror("malloc failed");
/* 在这里,你可能希望退出程序或采取补救措施
来应对没有 2GB 可用内存的情况 */
} else {
/* 此后,代码可以假设已经成功分配了 2,000,000,000 个字符... */
free(ptr);
}
exit(EXIT_SUCCESS); /* 退出程序 */
}
上面的代码段展示了如何使用库函数 malloc 的返回值来检查错误。许多库函数的返回值用于标志错误,因此聪明的程序员应该检查这些返回值。在上面的代码中,如果 malloc 返回 NULL 指针,则表示内存分配出错,程序将退出。在更复杂的实现中,程序可能会尝试处理该错误,并尝试从失败的内存分配中恢复。
防止除零错误
C 程序员常犯的一个错误是没有在除法命令之前检查除数是否为零。以下代码将在运行时产生错误,且在大多数情况下程序会退出:
int dividend = 50;
int divisor = 0;
int quotient;
quotient = (dividend / divisor); /* 这会导致运行时错误! */
在普通的算术运算中,除以零是未定义的。因此,必须检查除数是否为零。或者,在 *nix 系统中,可以通过屏蔽 SIGFPE 信号来阻止操作系统终止进程。
下面的代码通过在除法之前检查除数是否为零来修复此问题:
#include <stdio.h> /* 用于 fprintf 和 stderr */
#include <stdlib.h> /* 用于 exit */
int main(void)
{
int dividend = 50;
int divisor = 0;
int quotient;
if (divisor == 0) {
/* 错误处理示例:将消息写入 stderr,并
* 退出程序,标明失败 */
fprintf(stderr, "Division by zero! Aborting...\n");
exit(EXIT_FAILURE); /* 表示失败 */
}
quotient = dividend / divisor;
exit(EXIT_SUCCESS); /* 表示成功 */
}
信号
在某些情况下,环境可能会通过引发信号来响应 C 程序中的错误。信号是由主机环境或操作系统触发的事件,用于指示发生了特定的错误或关键事件(例如,除以零、硬件中断等)。然而,这些信号并不是用来捕获错误的工具;它们通常表示会干扰正常程序流程的关键事件。
要处理信号,程序需要使用 signal.h 头文件。需要定义一个信号处理程序,然后调用 signal() 函数来处理给定的信号。某些信号(如除零错误)通常不会允许程序恢复。信号处理程序的作用是确保在程序终止之前,某些资源能够正确清理。
C 标准库仅定义了六个信号;Unix 系统定义了更多的信号。每个信号都有一个数字,称为信号编号(signum)。以下是一些常见的信号:
#define SIGHUP 1 /* 挂起进程 */
#define SIGINT 2 /* 中断进程。C 标准 */
#define SIGQUIT 3 /* 退出进程 */
#define SIGILL 4 /* 非法指令。C 标准 */
#define SIGTRAP 5 /* 跟踪陷阱,调试用。C 标准 */
#define SIGABRT 6 /* 中止。C 标准 */
#define SIGFPE 8 /* 浮点错误。C 标准 */
#define SIGSEGV 11 /* 内存错误。C 标准 */
#define SIGTERM 15 /* 终止请求。C 标准 */
信号通过 signal() 函数处理,signal.h 库提供了此函数。其语法为:
void signal(int signal_to_catch, void (*signal_handler)(int));
信号可以通过 raise() 或 kill() 来引发。raise() 会将信号发送到当前进程;kill() 则将信号发送给指定的进程。
需要注意的是,signal() 现在已经被弃用,推荐使用 sigaction(),因为 signal() 在不同 Unix 系统之间的可移植性较差,并且可能会导致意外行为。然而,由于 sigaction() 的使用更为复杂,这里我们依旧使用 signal() 来说明其基本概念。
示例
下面是一个简单的例子,展示如何使用信号:
#include <stdio.h>
#include <unistd.h> // Unix 标准库,用于导入 sleep()
#include <stdlib.h>
#include <signal.h>
void handler(int signum) {
printf("Signal received %d, coming out...\n", signum);
exit(1);
}
int main () {
signal(SIGINT, handler); // 将 handler() 函数附加到 SIGINT 信号上;即 ctrl+c,键盘中断
while(1) {
printf("Sleeping...\n");
sleep(1000); // sleep 会暂停进程一段时间,或者直到接收到信号为止
}
return 0;
}
编译并在机器上测试该代码;当你看到 "Sleeping..." 输出时,按下 ctrl + c 来发送中断信号。
更复杂的示例
下面是一个更复杂的示例,展示了如何创建一个信号处理程序并引发信号:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
static void catch_function(int signal) {
puts("Interactive attention signal caught.");
}
int main(void) {
if (signal(SIGINT, catch_function) == SIG_ERR) {
fputs("An error occurred while setting a signal handler.\n", stderr);
return EXIT_FAILURE;
}
puts("Raising the interactive attention signal.");
if (raise(SIGINT) != 0) {
fputs("Error raising the signal.\n", stderr);
return EXIT_FAILURE;
}
puts("Exiting.");
return 0;
}
在这个例子中,signal(SIGINT, catch_function) 将 SIGINT 信号与 catch_function 处理程序关联。然后通过 raise(SIGINT) 显式触发 SIGINT 信号。程序接收到信号后,catch_function 会被调用,打印一条消息并终止程序。
setjmp
setjmp 函数可用于模拟其他编程语言中的异常处理功能。第一次调用 setjmp 会将当前执行点的引用存储起来,只要包含 setjmp() 的函数没有返回或退出,该引用就有效。调用 longjmp 会使程序的执行返回到关联的 setjmp 调用处。
setjmp 函数接受一个 jmp_buf 类型的参数(用于存储执行上下文),并在第一次调用时返回 0(即设置返回点时)。当第二次运行时(即调用 longjmp 时),它将返回传递给 longjmp 的值。
longjmp 函数接受两个参数:一个已经传递给 setjmp 的 jmp_buf,以及一个值,该值会传递给 setjmp 以作为返回值。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
int main(void) {
int val;
jmp_buf environment;
val = setjmp(environment); // 第一次调用时,val 设置为 0
if (val != 0) {
printf("You returned from a longjmp call, return value is %d", val); // 此时 val 为 1,来自 longjmp()
exit(0);
}
puts("Calling longjmp now");
longjmp(environment, 1); // 触发 longjmp,使程序跳回 setjmp 处,并传递值 1
return 0;
}
尝试在自己的机器上使用编译器运行该代码。
注意事项:
- 在
setjmp返回时,非易失性变量的值可能会被破坏。 - 虽然
setjmp()和longjmp()可以用于错误处理,但如果可能的话,通常更倾向于使用函数的返回值来指示错误。setjmp()和longjmp()在发生错误时最为有用,尤其是在深度嵌套的函数调用中,此时检查每一层的返回值直到返回点可能会非常繁琐。