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 函数接受两个参数:一个已经传递给 setjmpjmp_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() 在发生错误时最为有用,尤其是在深度嵌套的函数调用中,此时检查每一层的返回值直到返回点可能会非常繁琐。
最后修改: 2025年01月12日 星期日 13:28