前言
错误与异常: 错误与异常都是在程序编译或者运行时出现的错误, 不同的是,异常可以被开发人员捕捉和处理;而错误,一般不需要开发人员处理(也无法处理),比如内存溢出,如果异常未及时被处理,也可能产生错误。
在开发中,不可避免的需要对异常进行处理,如函数调用时候的异常:
- 不是指函数设计上的错误
- 而是可以预见的非正常功能的分支
例:
char* strcpy(char* des,const char* source)
{
char* r=des;
assert((des != NULL) && (source != NULL));
while((*r++ = *source++) != '\0');
return des;
}
异常处理的意义:
- 软件开发过程中,大多数时候都在处理异常情况
- 异常不是错误,但是可能导致程序无法正常运行
- 异常处理直接决定软件的鲁棒性和稳定性
一、 异常表达
在C语言中通常通过错误码表示异常,例如Linux中的错误码 优势:错误码定义简单,使用方便 劣势:同一个错误码,在不同程序中意义不仅相同
Errno | Errno号码 | 说明 |
---|
EINTR | 4 | 系统调用中断。 | EAGAIN | 11 | 资源临时不可用。 | EBUSY | 16 | 资源正忙。 | EMFILE | 24 | 每个进程文件描述符表已满。 | EPIPE | 32 | 管道断开。 |
异常表示的通用设计方法:采用整数分区域的方式对异常进行表示
bit31 | bit30 - bit16 | bit15-bit0 |
---|
1 | 模块标识 | 错误标识 |
最高位为符号位,固定为1则所有的错误码都是负数。
err_def.h定义了异常标识和模块标识
#ifndef ERR_DEF_H
#define ERR_DEF_H
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
typedef int32_t err_t;
#define MODULE_COUNT 2
#define ERR_CONSTRUCT(_error_) ((_error_) | (1 << 31))
#define ERR_CODE_BEGIN(_module_) ((_module_) << 16)
#define ERR_GET_MODULE_ID(_error_) (((_error_) >> 16) & 0x7fff)
#define ERR_GET_ERROR_INDEX(_error_) ((_error_) & 0xffff)
#define ERR_GET_ERROR_CODE(_error_) ((_error_) & 0x7fffffff)
typedef enum {
TIMER_MODULE,
LCD_MODULE
} module_enum_t;
typedef enum {
TIMER_ERR_OK = ERR_CODE_BEGIN(TIMER_MODULE),
TIMER_ERR_MEM,
TIMER_ERR_BUF,
TIMER_ERR_TIMEOUT,
TIMER_ERR_VAL
} timer_err_enum_t;
typedef enum {
LCD_ERR_OK = ERR_CODE_BEGIN(LCD_MODULE),
LCD_ERR_MEM,
LCD_ERR_BUF,
LCD_ERR_TIMEOUT,
LCD_ERR_VAL
} lcd_err_enum_t;
#endif
二、 异常报告
通常情况下,系统日志是报告异常的主要形式,但是异常报告并不是处理异常,异常报告只负责记录,而异常处理用于阻止异常程序的崩溃。
异常报告实例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "err.h"
#define LOG(errno) printf("[%s:%d] Errno: %x\n", __FILE__, __LINE__, errno)
err_t lcd_init()
{
err_t ret = ERR_CONSTRUCT(LCD_ERR_TIMEOUT);
return ret;
}
int main()
{
err_t ret = lcd_init();
if(LCD_ERR_OK != ret)
{
LOG(ret);
}
}
结果如下,直接打印出错误码,但是可能我们还得自己去查找具体的错误信息。
直接将异常标识和异常打印出来并不友好,不便于定义问题,因此我们希望有更加直观的异常输出方式:
- 直接将错误码对应的常量名字进行输出
- 枚举常量名包含了模块名和模块内部错误标识
类似下面这样的形式
#ifndef MODULE_H
#define MODULE_H
typedef enum {
LCD_ERR_OK = ERR_CODE_BEGIN(LCD_MODULE),
LCD_ERR_MEM,
LCD_ERR_BUF,
LCD_ERR_TIMEOUT,
LCD_ERR_VAL
} lcd_err_enum_t;
static const char *lcd_error_str[] = {
"LCD_ERR_OK",
"LCD_ERR_MEM",
"LCD_ERR_BUF",
"LCD_ERR_TIMEOUT",
"LCD_ERR_VAL"
}
#endif
但是错误标识在开发过程中可能经常发生变动,那么对应的字符数组也要同样变化,在修改过程中非常容易产生错误,需要特别注意,后续将介绍使用自动化代码生成工具,这样只需要定义对应的错误码就可以了,字符串数组自动生成。
接下来要做的就是把所有模块的错误标识字符数组关联起来,通过 const char* errno_to_str(err_t errno)转换成为对应的输出:
#include "err.h"
static struct error_str_t
{
bool exist;
int last_error;
const char ** error_array;
}s_error_str_array[MODULE_COUNT];
static const char *s_timer_error_str[] = {
"TIMER_ERR_OK",
"TIMER_ERR_MEM",
"TIMER_ERR_BUF",
"TIMER_ERR_TIMEOUT",
"TIMER_ERR_VAL"
};
static const char *s_lcd_error_str[] = {
"LCD_ERR_OK",
"LCD_ERR_MEM",
"LCD_ERR_BUF",
"LCD_ERR_TIMEOUT",
"LCD_ERR_VAL"
};
void error_str_init()
{
s_error_str_array[TIMER_MODULE].exist = true;
s_error_str_array[TIMER_MODULE].last_error = 4;
s_error_str_array[TIMER_MODULE].error_array = s_timer_error_str;
s_error_str_array[LCD_MODULE].exist = true;
s_error_str_array[LCD_MODULE].last_error = 4;
s_error_str_array[LCD_MODULE].error_array = s_lcd_error_str;
}
const char* error_to_str(err_t errno)
{
static bool initialized = false;
uint16_t error_code = ERR_GET_ERROR_INDEX(errno);
uint16_t module_id = ERR_GET_MODULE_ID(errno);
if(!initialized)
{
error_str_init();
initialized = true;
}
if(errno > 0)
return "Errno should less than 0";
if(!s_error_str_array[module_id].exist)
return "Error code array isn't exist";
if(s_error_str_array[module_id].last_error < error_code)
return "Error code out of range";
return s_error_str_array[module_id].error_array[error_code];
}
代码中通过模块ID和错误标识找到对应的字符串前需要对s_error_str_array进行初始化(字符数组的生成,初始化,都可以通过自动化完成,但是需要遵循一定的规范)
最后将LOG进行修改,让他可以输出更多信息
#define LOG(errno) do{ \
const char* str = error_to_str(errno); \
printf("[%s:%d] Errno: %s\n", __FILE__, __LINE__, str); \
}while(0)
最终的结果,通过对应名字就可以知道错误类型
三、 异常处理
异常处理示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "err.h"
#define LOG(errno) do{ \
const char* str = error_to_str(errno); \
printf("[%s:%d] Errno: %s\n", __FILE__, __LINE__, str); \
}while(0)
err_t lcd_init()
{
err_t ret = ERR_CONSTRUCT(LCD_ERR_TIMEOUT);
return ret;
}
bool lcd_error_handle(err_t errno)
{
bool ret = true;
err_t err = ERR_GET_ERROR_CODE(errno);
switch(err)
{
case LCD_ERR_MEM:
break;
default:
break;
}
if(!ret)
{
exit(0);
}
return ret;
}
int main()
{
err_t ret = lcd_init();
if(LCD_ERR_OK != ret)
{
LOG(ret);
lcd_error_handle(ret);
}
}
尽量在发生异常的地方报告异常,有助于找到异常发生时候的调用路径。 尽量在上层函数中统一异常,集中处理异常有助于提高代码的可维护性。
|