引言
在单片机程序中,诸如键盘按键扫描、菜单处理等多种逻辑功能,在代码上常采用Switch/Case语句的实现方式。Switch/Case语句是一种简单、初级的逻辑表达式,当代码较为复杂时,不利于程序的功能调试和逻辑修改[1],程序的可扩充性和可移植性都受到影响。为了在单片机常用的结构化程序设计中避免Switch/Case语句过于冗长,提出一种使用函数指针来替代Switch/Case语句的实现思路,并给出两种情况下的代码模型。事实证明,该方法可使程序的结构清晰、易于维护,可提高单片机程序的可读性和设计效率。
1Switch/Case语句的代码模型及特点
1.1 Switch/Case语句代码模型
Switch/Case是C语言中的基本语句,其使用方法在大量教材及书籍中均有介绍[2],这里不再赘述。一般来说,Switch/Case语句代码模型如下:
//功能代码实现
void function(void){
//程序处理
switch (PrmValue){
case Prm_A:
//程序处理
break;
case Prm_B:
//程序处理
break;
case Prm_C:
//程序处理
break;
……
}
}
在该代码模型中,假定function函数形参及返回值均为void类型,其使用Switch/Case分支语句进行不同情况下的程序处理。PrmValue是从前面的程序处理代码中获得的表达式,Prm_A、Prm_B和Prm_C等常量是表达式的值。当表达式的值与某一个Case后面的常量表达式的值匹配时,就执行此Case后面的语句。
1.2 Switch/Case语句的不足
从模型上可以看出,对于简单的功能实现,Switch/Case语句清晰、简便;但当项目功能较为复杂,例如键盘数或菜单级数较多时[3],Switch/Case语句的判断分支较多,导致代码冗长,常常一个Case分支语句动辄跨越数十行,程序整体的可读性和可移植性降低,程序的结构不够清晰。另外,当Switch/Case语句需要扩展功能时,需要在已经臃肿的Case分支上继续“添砖加瓦”,造成了程序语句的进一步冗长,不便于后期程序逻辑功能的修改。
2 使用函数指针替代Switch/Case语句的
两种设计方法
2.1 Case表达式的值连续情况下的设计方法
在Switch/Case语句代码模型中,较为常见的一种情况是:Prm_A、Prm_B、Prm_C等常量是连续的数值。对于这种情况,设计思路是定义相关的函数指针数组[4],然后实现相对应的函数。在使用时,只需调用对应的函数指针数组元素即可。该方式下代码模型如下:
//pFun_A等相关函数的实现
void pFun_A(void){...}
void pFun_B(void){...}
void pFun_C(void){...}
……
//相关的函数指针数组的定义
void (*pFun)(void)[]={pFun_A,pFun_B,pFun_C,}
//功能代码实现
void function(void){
//程序处理
(pFun[PrmValue])();
//调用函数指针替代原本的Switch/Case语句
}
在该代码模型中,假定函数指针指向的函数形参及返回值均为void类型,PrmValue是从前面的程序处理代码中获得的表达式。设计方法中,将每个Case分支的处理代码封装成函数,并将每个函数地址按照Case表达式的值的顺序赋给函数指针数组各元素。在功能代码中,程序的实现主体只需使用PrmValue的值调用对应的处理函数即可,原本冗长的Switch/Case语句被一条简单的函数指针调用代替,形式较为简洁。使用这种设计方法,当程序需要扩展新的功能逻辑的时候,只需要依次增加函数指针数组元素的值并实现对应的函数,功能代码的主体部分不需改动。需要注意的一点是,第2种的代码模型中,假定PrmValue的值是从0开始的,对于Case表达式的值不是从0开始但Case表达式的值连续的情况,上面的代码模型仍然可以使用。例如,如果Case表达式的值是从delta开始的,在这种情况下只需要将第2种代码模型中的(pFun[PrmValue])()表达式改为(pFun[PrmValuedelta])()即可,代码的其他部分均可以保持不变。
2.2 Case表达式的值离散情况下的设计方法
第1种代码模型中,在Prm_A、Prm_B、Prm_C等数值相差较大且不连续的情况下,如果仍然套用2.1节的设计方法,函数指针数组中需要人为填充大量冗余数据,造成资源的浪费,并且在修改时需要做到精确的一一对应,程序的设计反而变得更为麻烦,失去了设计方法的本意。此时需要对第2种代码模型进行增强设计。其设计思路是通过构建一个数据结构,将离散的Case表达式的值和对应的处理函数一一对应,在功能代码中对函数指针进行轮询操作。其实现模型如下:
//pFun_A等相关函数的实现
void pFun_A(void){...}
void pFun_B(void){...}
void pFun_C(void){...}
……
//相关的数据结构及其数组的定义
typedef struct { int iValue; void(*pFun)(void)} StructFun;
StructFun mystruct[]={ {Prm_A,pFun_A}, {Prm_B,Fun_B}, {Prm_C, pFn_C}... };
//功能代码实现
void function(void){
//程序处理
for (int i=0; i<sizeof(mystruct)/sizeof(StructFun); i++){
//轮询比较,找到相等的值并调用对应的函数指针
if( PrmValue== mystruct[i].iValue )
(mystruct[i].pFun)();
}
}
在该代码模型中,构建的StructFun结构体数组实现了Case表达式的值和函数指针的一一对应。当程序需要扩展新的功能逻辑时,只需要添加StructFun结构体数组的内容并实现对应函数体即可,设计方法的优点与2.1节的设计方法类似。实际上,2.1节的设计方法是特殊情况下本设计方法的退化,两种设计方法在思路上仍是保持一致的。
结语
本文介绍的两种使用函数指针替代Switch/Case语句的方法,使程序的任务处理逻辑关系变得简洁明了,易于增加程序状态,更改程序逻辑,程序的可读性、可调试性强,减少了单片机程序设计中的错误。笔者在工程项目中验证了这两种设计方法,实际可行且快捷有效,对于单片机常用的结构化程序开发具有一定的借鉴意义。