在程序运行过程中,错误是不可避免的:文件不存在、内存分配失败、输入无效等等。 良好的错误处理机制能够使程序在遇到异常时优雅地恢复或给出清晰的提示,而不是崩溃或产生错误结果。 C语言没有像C++或Java那样的异常处理机制,而是通过返回值、全局变量errno、断言(assert)等方式来处理错误。 本章将详细介绍C语言中常见的错误处理方法及断言的使用技巧。
C语言中的错误处理通常采用以下几种策略:
errno,可以通过 perror 或 strerror 获取错误信息。
errno 是 <errno.h> 中定义的全局整数变量,当标准库函数(如文件操作、内存分配等)发生错误时,会设置 errno 为一个正值,表示具体的错误类型。
程序可以通过 perror() 打印错误信息,或使用 strerror() 获取错误描述的字符串。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
// 方法1:使用 perror 直接打印错误信息(自动添加前缀)
perror("打开文件失败");
// 方法2:使用 strerror 获取错误描述字符串
printf("错误码: %d, 错误信息: %s\n", errno, strerror(errno));
return 1;
}
fclose(fp);
return 0;
}
errno 只在函数出错时被设置,成功时不会清零。因此,在调用可能出错的函数前,最好将 errno 设置为0,然后再检查。
| 错误码 | 宏 | 含义 |
|---|---|---|
| 1 | EPERM | 操作不允许 |
| 2 | ENOENT | 文件或目录不存在 |
| 13 | EACCES | 权限不足 |
| 22 | EINVAL | 无效参数 |
| 24 | EMFILE | 打开文件过多 |
| 28 | ENOSPC | 设备无剩余空间 |
在实际项目中,可以定义自己的错误码和错误处理函数,使错误处理更统一。
#include <stdio.h>
// 定义错误码枚举
typedef enum {
ERR_SUCCESS = 0,
ERR_FILE_OPEN,
ERR_MEMORY_ALLOC,
ERR_INVALID_ARG,
ERR_DIVIDE_BY_ZERO
} ErrorCode;
// 错误信息映射
const char *errorMessages[] = {
"成功",
"文件打开失败",
"内存分配失败",
"无效参数",
"除零错误"
};
// 自定义错误处理函数
void handleError(ErrorCode code) {
fprintf(stderr, "错误: %s\n", errorMessages[code]);
}
// 示例:除法函数,返回错误码
ErrorCode divide(int a, int b, int *result) {
if (b == 0) return ERR_DIVIDE_BY_ZERO;
*result = a / b;
return ERR_SUCCESS;
}
int main() {
int res;
ErrorCode err = divide(10, 0, &res);
if (err != ERR_SUCCESS) {
handleError(err);
return 1;
}
printf("结果: %d\n", res);
return 0;
}
assert 是 <assert.h> 中定义的宏,用于在调试阶段检测程序中不应出现的条件。
如果断言的条件为假,程序会输出错误信息并终止(调用 abort())。
在发布版本中,可以通过定义 NDEBUG 宏来禁用所有断言,以提高性能。
#include <stdio.h>
#include <assert.h>
int factorial(int n) {
assert(n >= 0); // 确保 n 非负
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
int x = -5;
int fact = factorial(x); // 触发断言失败,程序终止
printf("%d! = %d\n", x, fact);
return 0;
}
#define NDEBUG // 放在 #include <assert.h> 之前
#include <assert.h>
// 之后的所有 assert 都不会产生任何效果
也可以在编译时定义:gcc -DNDEBUG program.c
C11 标准引入了 _Static_assert(也可通过 static_assert 宏使用,需要包含 <assert.h>),用于在编译时检查常量表达式。
如果条件不满足,编译器会产生错误信息,阻止编译。静态断言非常适用于检查结构体大小、类型对齐等编译时可知的条件。
#include <stdio.h>
#include <assert.h> // 提供 static_assert 宏
// 静态断言:确保 int 类型至少 4 字节
static_assert(sizeof(int) >= 4, "int 类型太小,至少需要4字节");
struct Student {
int id;
char name[50];
float score;
};
// 确保结构体没有意外填充(不推荐这样严格要求,仅演示)
static_assert(sizeof(struct Student) == (sizeof(int) + 50 + sizeof(float)),
"结构体存在额外填充");
int main() {
printf("程序编译成功\n");
return 0;
}
fopen、malloc、scanf 等,不要假设它们一定会成功。perror 或自定义日志函数时,应包含文件名、行号等信息,方便定位。#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
// 读取整个文件到动态分配的内存
char* readFile(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
fprintf(stderr, "无法打开文件 %s: %s\n", filename, strerror(errno));
return NULL;
}
// 获取文件大小
if (fseek(fp, 0, SEEK_END) != 0) {
perror("fseek 失败");
fclose(fp);
return NULL;
}
long size = ftell(fp);
if (size == -1) {
perror("ftell 失败");
fclose(fp);
return NULL;
}
rewind(fp);
// 分配内存(多一个字节用于存储 '\0')
char *buffer = (char*)malloc(size + 1);
if (buffer == NULL) {
fprintf(stderr, "内存分配失败\n");
fclose(fp);
return NULL;
}
// 读取文件内容
size_t readBytes = fread(buffer, 1, size, fp);
if (readBytes != (size_t)size) {
fprintf(stderr, "读取文件不完整: 期望 %ld 字节,实际 %zu 字节\n", size, readBytes);
free(buffer);
fclose(fp);
return NULL;
}
buffer[size] = '\0'; // 添加字符串结束符
fclose(fp);
return buffer;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s 文件名\n", argv[0]);
return 1;
}
char *content = readFile(argv[1]);
if (content == NULL) {
return 1;
}
// 使用断言检查 content 非空(已处理,但可用来验证逻辑)
assert(content != NULL);
printf("文件内容 (%s):\n%s\n", argv[1], content);
free(content);
return 0;
}
fopen、malloc 等的返回值,直接使用可能为NULL的指针,导致崩溃。errno,因此错误检查前应主动设置 errno = 0;。strerror 返回的字符串可能被后续调用覆盖,如需保存应复制一份。
错误处理和断言是编写健壮、可维护C程序的重要技术。通过合理使用 errno、返回值检查、断言以及自定义错误机制,你可以使程序在遇到异常时更稳定、更易于调试。
在实际开发中,建议结合日志系统(如 syslog 或自定义日志)进一步增强错误报告能力。