1. 错误处理机制概述
在C/C++开发中,错误处理是每个程序员必须掌握的核心技能。当程序运行出现异常时,系统通常会通过特定的错误码机制来反馈问题原因。strerror、GetLastError和errno是三种常见的错误处理方式,它们分别适用于不同的场景和操作系统环境。
我见过太多项目因为错误处理不当而导致严重问题。有一次线上服务崩溃,就是因为开发团队混用了这些错误处理机制,最终花了整整两天才定位到根本原因。理解它们的区别和适用场景,可以让你少走很多弯路。
2. errno:跨平台的错误指示器
2.1 errno的基本原理
errno是C标准库定义的全局整数变量,用于存储最近发生的错误代码。它定义在<errno.h>头文件中,实际上在现代实现中通常是一个宏,展开为可修改的左值(如函数调用返回的指针)。
当标准库函数执行失败时,它们会设置errno为特定的错误代码。例如:
c复制FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
printf("Error opening file: %d\n", errno); // 输出错误码
}
2.2 常见errno错误码
errno的错误码是标准化的,常见的有:
- EACCES (13): 权限不足
- ENOENT (2): 文件或目录不存在
- EINTR (4): 系统调用被中断
- ENOMEM (12): 内存不足
注意:errno在多线程环境下是线程安全的,现代实现会为每个线程维护独立的errno副本。
2.3 errno的使用陷阱
在实际项目中,我发现开发者常犯的错误包括:
- 在调用可能失败的函数前没有清零errno
- 错误地认为所有函数都会设置errno(如某些系统调用成功时也可能修改errno)
- 在多线程环境中假设errno是全局唯一的
正确的做法是:
c复制errno = 0; // 先清零
double result = strtod(input, NULL);
if (errno == ERANGE) {
// 处理数值超出范围的情况
}
3. strerror:错误码转文本
3.1 strerror函数详解
strerror函数定义在<string.h>中,用于将errno的错误码转换为可读的字符串描述:
c复制char *strerror(int errnum);
使用示例:
c复制FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
printf("Error: %s\n", strerror(errno));
}
3.2 strerror的线程安全版本
标准C库还提供了strerror_r函数,它是线程安全的替代方案:
c复制char buffer[256];
strerror_r(errno, buffer, sizeof(buffer));
3.3 实际应用技巧
在日志系统中,我通常会这样封装错误处理:
c复制#define LOG_ERROR(fmt, ...) \
fprintf(stderr, "[ERROR] %s:%d " fmt ": %s\n", \
__FILE__, __LINE__, ##__VA_ARGS__, strerror(errno))
这样可以在日志中自动附加错误描述,极大简化调试过程。
4. GetLastError:Windows特有的错误机制
4.1 Windows API错误处理特点
与标准C库不同,Windows API使用GetLastError()函数获取错误码。这个机制是Windows特有的,在调用Win32 API失败后使用:
c复制HANDLE hFile = CreateFile("nonexistent.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
printf("Error code: %lu\n", err);
}
4.2 常见Windows错误码
一些典型的Windows错误码:
- ERROR_FILE_NOT_FOUND (2): 文件不存在
- ERROR_ACCESS_DENIED (5): 访问被拒绝
- ERROR_INVALID_HANDLE (6): 无效句柄
- ERROR_NOT_ENOUGH_MEMORY (8): 内存不足
4.3 FormatMessage:Windows版的strerror
Windows提供了FormatMessage函数来转换错误码为可读信息:
c复制LPSTR messageBuffer = NULL;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&messageBuffer, 0, NULL);
printf("Error: %s\n", messageBuffer);
LocalFree(messageBuffer);
5. 三者的对比分析
5.1 适用场景对比
| 特性 | errno | GetLastError | strerror |
|---|---|---|---|
| 适用范围 | 标准C库函数 | Windows API | 错误码转文本 |
| 线程安全 | 现代实现是线程安全的 | 线程安全 | 非线程安全 |
| 头文件 | <errno.h> | <windows.h> | <string.h> |
| 错误码范围 | 标准定义 | Windows系统定义 | 可处理两者错误码 |
5.2 混用问题与解决方案
在跨平台项目中,最常见的错误是混用这些机制。例如:
c复制// 错误示例!
CreateFile(...);
if (errno != 0) { // 错误!应该用GetLastError()
// ...
}
正确的做法是明确区分使用场景:
- 标准C库函数 → 检查errno
- Windows API → 调用GetLastError()
- 需要错误描述 → 根据平台选择strerror或FormatMessage
5.3 性能考量
在实际性能测试中,我发现:
- GetLastError()只是读取线程局部存储,开销极小
- strerror在某些实现中需要查表,可以考虑缓存常用错误描述
- FormatMessage涉及内存分配,性能开销较大,适合错误处理路径
6. 最佳实践与经验分享
6.1 错误处理封装技巧
我通常会封装跨平台的错误处理工具函数:
c复制#ifdef _WIN32
#define GET_LAST_ERROR() GetLastError()
#define GET_ERROR_STR(err) GetWin32ErrorString(err)
#else
#define GET_LAST_ERROR() errno
#define GET_ERROR_STR(err) strerror(err)
#endif
const char* GetWin32ErrorString(DWORD err) {
static __thread char buffer[256];
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
buffer, sizeof(buffer), NULL);
return buffer;
}
6.2 常见错误排查案例
案例1:文件打开失败
- 检查errno/GetLastError()代码
- 确认文件路径是否正确(绝对路径/相对路径)
- 检查文件权限(特别是Linux系统)
案例2:内存分配失败
- 检查errno是否为ENOMEM
- 确认是否真的内存不足(还是内存泄漏)
- 考虑使用malloc的替代方案
6.3 调试技巧
- 在Visual Studio中,可以通过"监视"窗口输入"@err,hr"查看格式化的错误信息
- GDB调试时可以使用"p strerror(errno)"查看当前错误
- 对于难以复现的错误,建议记录完整的错误上下文信息
7. 高级话题与扩展
7.1 自定义错误码系统
大型项目通常会定义自己的错误码体系。实现要点:
- 保留系统错误码范围(如正数)
- 自定义错误码使用负数范围
- 提供类似的转换函数
示例:
c复制enum {
MYAPP_ERROR_INVALID_CONFIG = -1000,
MYAPP_ERROR_NETWORK_FAILURE = -1001,
// ...
};
const char* MyAppErrorString(int err) {
if (err < 0) {
switch (err) {
case MYAPP_ERROR_INVALID_CONFIG: return "Invalid configuration";
// ...
}
}
return strerror(err); // 回退到系统错误
}
7.2 C++异常与错误码的结合
现代C++项目可以结合使用异常和错误码:
cpp复制class SystemError : public std::runtime_error {
public:
SystemError(int err)
: std::runtime_error(strerror(err)), error_code(err) {}
int code() const { return error_code; }
private:
int error_code;
};
void SomeFunction() {
if (some_syscall() == -1) {
throw SystemError(errno);
}
}
7.3 错误处理的设计模式
根据项目规模,可以考虑不同的错误处理策略:
- 小型工具:直接返回错误码
- 中型项目:使用错误码+日志记录
- 大型系统:异常处理+错误码+上下文信息
在分布式系统中,还需要考虑:
- 错误信息的序列化/反序列化
- 跨语言错误码映射
- 错误传播和上下文维护
