C语言异常处理

前言

错误与异常:
错误与异常都是在程序编译或者运行时出现的错误, 不同的是,异常可以被开发人员捕捉和处理;而错误,一般不需要开发人员处理(也无法处理),比如内存溢出,如果异常未及时被处理,也可能产生错误。


在开发中,不可避免的需要对异常进行处理,如函数调用时候的异常:

  • 不是指函数设计上的错误
  • 而是可以预见的非正常功能的分支

例:

char* strcpy(char* des,const char* source)
{
	char* r=des;
    assert((des != NULL) && (source != NULL));
 	while((*r++ = *source++) != '\0');
	return des;
}

异常处理的意义:

  • 软件开发过程中,大多数时候都在处理异常情况
  • 异常不是错误,但是可能导致程序无法正常运行
  • 异常处理直接决定软件的鲁棒性和稳定性

一、 异常表达

在C语言中通常通过错误码表示异常,例如Linux中的错误码
优势:错误码定义简单,使用方便
劣势:同一个错误码,在不同程序中意义不仅相同

ErrnoErrno号码说明
EINTR4系统调用中断。
EAGAIN11资源临时不可用。
EBUSY16资源正忙。
EMFILE24每个进程文件描述符表已满。
EPIPE32管道断开。

异常表示的通用设计方法:采用整数分区域的方式对异常进行表示

bit31bit30 - bit16bit15-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
// 将最高位设为1构成错误码
#define ERR_CONSTRUCT(_error_) ((_error_) | (1 << 31))
// 根据模块标识确认第一个错误标识的值
#define ERR_CODE_BEGIN(_module_) ((_module_) << 16)
// 获取模块ID
#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"
	};

// 初始化 s_error_str_array 数组
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;
	// 根据错误码获取16位错误标识
	uint16_t error_code = ERR_GET_ERROR_INDEX(errno);
	// 根据错误码获取15位模块标识
	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;
	}
	// 如果函数无法处理直接exit结束
	if(!ret)
	{
		exit(0);
	}
	return ret;
}

int main()
{
	err_t ret = lcd_init();
	if(LCD_ERR_OK != ret)
	{
		LOG(ret);
		lcd_error_handle(ret);
	}
}

尽量在发生异常的地方报告异常,有助于找到异常发生时候的调用路径。
尽量在上层函数中统一异常,集中处理异常有助于提高代码的可维护性。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咕咚.萌西

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值