这是一个非常经典的话题,因为 C 语言本身没有像 Java、C# 或 Python 那样内置的 try...catch...throw 语法结构,当 C 程序中出现错误(比如除以零、内存分配失败、数组越界等)时,它不能直接“抛出”一个异常对象然后被某个 catch 块捕获。

C 语言采用的是一种更底层、更直接的方式来处理错误,主要包括以下几种机制:
错误码
这是最传统、最基础的 C 语言错误处理方式,当一个函数执行时可能会出错,它不会抛出异常,而是返回一个特定的值来表示成功或失败。
- 整数返回值:函数返回
0或1、-1等来表示状态。 - 指针返回值:函数返回
NULL指针来表示失败(malloc)。
示例:
#include <stdio.h>
#include <stdlib.h>
// 一个可能失败的函数
int divide(int a, int b, int *result) {
if (b == 0) {
// 错误情况:返回一个错误码
return -1; // -1 表示除零错误
}
*result = a / b;
return 0; // 0 表示成功
}
int main() {
int res;
if (divide(10, 2, &res) == 0) {
printf("Division successful: %d\n", res); // 输出: Division successful: 5
} else {
printf("Error: Division by zero!\n");
}
if (divide(10, 0, &res) == 0) {
printf("Division successful: %d\n", res);
} else {
printf("Error: Division by zero!\n"); // 输出: Error: Division by zero!
}
return 0;
}
优点:

- 简单、直观,开销极小。
- 不需要特殊的语言支持。
缺点:
- 调用者必须检查返回值:如果调用者忘记检查,错误就会被忽略,可能导致程序进入未知状态。
- 错误码可能被误解:
malloc返回NULL既是错误码,也是一个有效的返回值(表示“没有内存”)。 - 错误信息有限:只能返回一个整数,无法传递详细的错误描述(如错误类型、发生位置等)。
setjmp 和 longjmp
这是一种更强大的“非局部跳转”(Non-local Jump)机制,可以模拟 try...catch 的行为,但它非常危险,通常只用于处理非常严重的错误(如栈溢出、内存耗尽等)。
setjmp(jmp_buf env): 在一个“正常”的代码点调用,会保存当前的程序执行上下文(包括栈指针、程序计数器等),它返回0。longjmp(jmp_buf env, int val): 在程序的任何地方调用,它会“跳回”到之前setjmp的地方,并恢复保存的上下文,程序会从setjmp的地方重新执行,但这次setjmp的返回值是longjmp传入的val。
示例:
#include <stdio.h>
#include <setjmp.h>
jmp_buf jump_buffer; // 全局的跳转缓冲区
void function_that_might_fail() {
printf("Function called. About to jump back.\n");
// 模拟一个严重错误
longjmp(jump_buffer, 42); // 跳转到 setjmp 的地方,并返回值 42
}
int main() {
int ret_val = setjmp(jump_buffer); // 设置跳转点
if (ret_val == 0) {
// 第一次执行,正常流程
printf("First call to setjmp. Returned 0.\n");
function_that_might_fail(); // 调用可能出错的函数
printf("This line will never be reached.\n");
} else {
// longjmp 跳转回来后,从这里开始执行
printf("Jumped back! setjmp returned %d.\n", ret_val);
}
return 0;
}
输出:
First call to setjmp. Returned 0.
Function called. About to jump back.
Jumped back! setjmp returned 42.
优点:
- 可以从一个深层嵌套的函数中直接跳出,逐层返回检查的麻烦。
- 可以传递一个整数值作为“错误码”。
缺点:
- 极度危险:它会直接绕过正常的函数调用栈和析构函数(在 C++ 中),如果你在
setjmp和longjmp之间分配了内存,这个内存永远不会被释放,导致内存泄漏。 - 破坏程序结构:使得代码流程变得难以追踪,破坏了结构化编程的原则。
- 难以调试:跳转行为会让调试器非常困惑。
除非是处理像 signal 处理程序这样无法使用 return 退出的极端情况,否则强烈不推荐在日常代码中使用 setjmp/longjmp。
errno 和标准库错误
这是 C 标准库提供的一种全局错误报告机制,许多标准库函数在执行失败时,并不会通过返回值告知具体原因,而是会将一个全局变量 errno 设置为一个特定的错误码(宏定义在 <errno.h> 中)。
调用者需要检查函数的返回值(通常是 -1 或 NULL),然后主动去检查 errno 的值来了解错误详情。
示例:
#include <stdio.h>
#include <errno.h> // 必须包含
#include <string.h> // 用于 strerror
#include <fcntl.h> // 用于 O_RDONLY
int main() {
FILE *fp;
fp = fopen("a_non_existent_file.txt", "r");
if (fp == NULL) {
// fopen 失败,errno 会被设置
printf("Error opening file.\n");
printf("errno value: %d\n", errno); // 输出具体的错误码,2
// 使用 perror 或 strerror 打印可读的错误信息
perror("perror output"); // 会自动打印 "perror output: " + 错误描述
printf("strerror output: %s\n", strerror(errno)); // 直接获取错误描述字符串
return 1;
}
// ... 正常处理文件 ...
fclose(fp);
return 0;
}
常见 errno 值:
ENOENT(No such file or directory) - 文件或目录不存在ENOMEM(Out of memory) - 内存不足EINVAL(Invalid argument) - 无效参数EAGAIN(Resource temporarily unavailable) - 资源暂时不可用
优点:
- 提供了标准化的、可读的错误信息。
- 不会干扰正常的返回值。
缺点:
- 仍然是“被动检查”:调用者必须记得检查。
errno是全局变量:在多线程环境下,errno可能是线程安全的(POSIX 要求),但它仍然是共享状态,可能导致竞态条件,如果函数 A 设置了errno但还没来得及处理,函数 B 又设置了它。- 不能区分来源:如果多个函数都可能失败,你无法知道是哪个函数设置的
errno。
断言 (assert)
assert 不是一个运行时错误处理机制,而是一个调试辅助工具,它用于检查那些在程序逻辑上“绝不应该发生”的条件。
assert的条件为真,程序继续运行。- 如果为假,程序会立即终止,并打印一条包含断言失败、文件名和行号的消息。
assert 宏定义在 <assert.h> 中,在发布版本(Release)中,可以通过定义 NDEBUG 宏来禁用所有 assert,使其不产生任何代码。
示例:
#include <stdio.h>
#include <assert.h>
int compute_something(int x) {
assert(x >= 0 && "Input must be non-negative");
return x * x;
}
int main() {
printf("compute_something(5) = %d\n", compute_something(5)); // 正常
printf("compute_something(-1) = %d\n", compute_something(-1)); // 触发断言
// 程序在这里终止,输出类似:
// a.out: main.c:5: compute_something: Assertion `x >= 0 && "Input must be non-negative"' failed.
// Aborted (core dumped)
return 0;
}
优点:
- 快速定位逻辑错误。
- 在开发阶段提供强大的保护。
缺点:
- 不是运行时错误处理:它假设错误是“不应该发生的”,所以直接终止程序,对于可以预见的、可能发生的错误(如文件不存在、网络中断),
assert不适用。 - 会终止程序,没有恢复的机会。
总结与现代 C++ 的对比
| 机制 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 错误码 | 简单函数,调用者明确知道如何处理。 | 简单、高效、无开销。 | 调用者必须检查,易忽略;信息有限。 |
setjmp/longjmp |
极端情况(如信号处理、内存耗尽)。 | 能跳出深层嵌套。 | 危险,破坏结构,可能导致内存泄漏。 |
errno |
标准库函数(I/O, 内存分配等)的错误报告。 | 标准化,提供可读错误信息。 | 被动检查,全局变量,多线程需小心。 |
assert |
调试阶段,检查“绝不应该发生”的逻辑错误。 | 快速定位问题,无运行时开销。 | 不是运行时处理,会直接终止程序。 |
为什么 C 语言没有 try...catch?
- 哲学:C 语言追求简单和高效,异常处理机制(特别是栈展开)会带来显著的运行时开销,需要维护额外的异常对象表和栈信息,这与 C 的设计哲学相悖。
- 历史:C 语言在 20 世纪 70 年代诞生,而结构化异常处理是后来才在更高级的语言中普及。
- 性能:异常处理通常被认为比简单的错误检查慢,在 C 中,开发者被期望对性能有完全的控制权,因此选择让开发者自己决定如何处理错误。
现代 C 的替代方案:
在 C++ 中,try...catch 是标准且推荐的做法,如果你在写 C 代码,但又希望有更健壮的错误处理,可以考虑以下几种现代方法:
- 使用 C++:如果项目允许,直接使用 C++ 的异常机制是最佳选择。
- 错误码 + 回调函数:对于异步操作或需要复杂错误处理的场景,可以通过回调函数传递错误对象或结构体。
- 使用第三方库:有一些 C 语言的库(如
libunwind)提供了更安全的栈展开机制,但它们非常复杂,且不属于标准 C。 - Golang 的
error风格:虽然源自 Go,但这种思想(返回一个error类型的指针)在 C 社区也越来越受欢迎,通过一个typedef定义一个错误结构体,让返回值携带更多信息。
C 语言没有内置的异常,而是提供了一套灵活但需要开发者高度自律的错误处理工具集,选择哪种方式取决于你的具体需求、代码复杂度和性能要求,对于绝大多数情况,错误码 + errno 的组合已经足够了。
