【摘要】 表驱动是一种在C语言里常见的编程模式,从表里面查找信息而不使用逻辑语句(if和case)。核心操作是将输入因素作为直接或者间接的索引,到数组里找到直接的结果或者对应的处理(通常是函数指针)。
模式介绍
在传统的23种面向对象设计模式里,并没有表驱动这种模式。这种模式是强烈依赖数组或者多维数组的一种设计模式,不涉及类,继承等关系,所以在C语言等非面向对象编程里得到了广泛的应用。
表驱动是一种在C语言里常见的编程模式,从表里面查找信息而不使用逻辑语句(if和case)。核心操作是将输入因素作为直接或者间接的索引,到数组里找到直接的结果或者对应的处理(通常是函数指针)。
表驱动实质上把逻辑和数据进行了分离。因素和结果之间的映射关系能够全部存放到数组里,而不是混杂在if,else的流程代码里。当映射关系发生改变的时候,只需要改变数组就可以,不需要修改代码。管理和维护起来非常方便。甚至可以把数据作为配置文件存放到硬盘上,需要的时候读取进来,避免了代码重新编译。
模式分类
根据输入因素的不同,按照“代码大全”一书的分类,有如下三种。
-
直接访问
直接访问是最基本最本质的一种方式。前提条件是输入因素是整数,或者是非常简单的一对一转化成能作为数组元素序号。
-
索引访问
输入元素由于某些原因,难以作为数组的序号。将输入通过函数转化为序号,接下来和直接访问模式一样。
-
阶梯访问
输入元素是分段的范围,不好转化为数组的序号。
根据输入因素的个数,可以分为一维表查找和多维表查找。
模式实现
大部分资料拿查月份有多少天来作为例子说明。月份的数字就是自然数,月份减一恰好就可以作为数字的序号。先用这个比较俗的例子示意下。后续在举一个实际开发中典型的例子。
普通代码
int get_days_in_month(int mon)
{
int days;
if(1==mon){days=31;}
elseif(2==mon){days=28;}
elseif(3==mon){days=31;}
elseif(4==mon){days=30;}
elseif(5==mon){days=31;}
elseif(6==mon){days=30;}
elseif(7==mon){days=31;}
elseif(8==mon){days=31;}
elseif(9==mon){days=30;}
elseif(10==mon){days=31;}
elseif(11==mon){days=30;}
elseif(12==mon){days=31;}
return days;
}
表驱动设计的代码
Days_in_Month[12]={31,28,31,30,31,30,31,31,30,31,30,31};
int get_days_in_month(int mon)
{
return Days_in_Month[mon-1];
}
从上可以看到,查月份有多少天这个例子属于典型的直接访问模式,一维表查找的例子。
直接模式,多维数组-实际编程样例
在实际编程中,极少遇到这种输入因素本身就是数字的情形。一般是把输入的因素分类,假设为N类,然后用宏从0开始定义,以宏为序号。通常还需要设定一个MAX,来表示异常。
假设有如下场景:
某设备的工作流有几个因素决定,(acq_cnt, zsl, cap_nzsl, small_line_buffer)。acq_cnt表示摄像头个数,一般就1个或者2个。Zsl表示是否支持zsl,cap_nzsl是另外一个特性,small_line_buffer和设备buffer大小相关。
普通代码如下
int get_uc(int acq_cnt, int zsl, int cap_nzsl, int small_line_buffer)
{
int df_type = 0;
switch (acq_cnt)
{
case 1:
{
if (zsl)
{
if (small_line_buffer < xxx)
{
df_type = DF_S1;
}
else
{
df_type = DF_M1;
}
}
else
{
if (cap_nzsl)
{
if (small_line_buffer < xxx)
{
df_type = DF_S3;
}
else
{
df_type = DF_M3;
}
}
else
{
if (small_line_buffer < xxx)
{
df_type = DF_S2;
}
else
{
df_type = DF_M2;
}
}
}
break;
}
case 2:
{
if (zsl && (small_line_buffer < xxx))
{
df_type = DF_D1;
}
else
{
if (cap_nzsl)
{
df_type = DF_D3;
}
else
{
df_type = DF_D2;
}
}
break;
}
default:
{
loge("acquire camera count = %d", acq_cnt);
break;
}
}
return df_type;
}
以上代码看起来就很烦,容易出错。如果因素改变了,还要改代码。但是用表查找还有几个问题。摄像头个数acq_cnt好说,用acq_cnt-1当作需要就可以。这个方法在月份的例子用过。small_line_buffer是个麻烦,因为不可能按照buffer的byte数作为序号。不过逻辑上只需要判断两个状态就可以,不如定义新的small_line_buffer为(老small_line_buffer < xxx),转化为0,1。老small_line_buffer < xxx为1, 不满足条件新的small_line_buffer就为0。Zsl和cap_nzsl规定为0,1状态就可以了。
特别注意的是,这里的small_line_buffer改造,就有索引模式的意味了,因为buffer大小的数值是有很多的取值,但是small_line_buffer最后只有1个值,这就违反了直接模式里的输入因素和索引一对一的关系。
另外两个参数,zsl和cap_nzsl是特性,显然原始含义也不是整数,而是各自表示两种状态。这就需要把状态定义成从0开始的宏或者枚举。我一般习惯宏。
经过改造后,表驱动如下
flow_type_e df_type_tab[2][2][2][2] =
{
[0][0][0][0] = DF_M2,
[0][0][0][1] = DF_MAX,
[0][0][1][0] = DF_M3,
[0][0][1][1] = DF_S3,
[0][1][0][0] = DF_M1,
[0][1][0][1] = DF_S1,
[0][1][1][0] = DF_MAX,
[0][1][1][1] = DF_MAX,
[1][0][0][0] = DF_MAX,
[1][0][0][1] = DF_D2,
[1][0][1][0] = DF_MAX,
[1][0][1][1] = DF_D3,
[1][1][0][0] = DF_MAX,
[1][1][0][1] = DF_D1,
[1][1][1][0] = DF_MAX,
[1][1][1][1] = DF_MAX
};
因为原逻辑中,有好几个排列组合没有df_type合法值,那么就定义一个DF_MAX来表示。改造完的表驱动代码如下。
small_line_buffer = 1:0 ? line_buffer < xxx;
df_type = df_type_tab[g_msg_mgr.acq_cnt -1][zsl][cap_nzsl][small_line_buffer];
索引访问模式
采用索引模式通常有几个原因
1. 输入因素取值范围太大,而且大部分都是没有用的,或者很多值对应的结果影响是相同的。这样会造成数组的大量浪费,或者大到内存无法满足。
2. 没法量化,比如有的输入值是浮点。
有的观点认为,索引就一种编程形式:因为原因素取值太多,那就再建立一个查找数组表,输入为原始因素,输出为一个中介序号。数据密集存放在数据里,靠中介序号来访问。这样相当于二级直接访问的叠加。中介表的每项只不过是一个输出索引的大小,通常远小于原始数据项的大小。
不过这种名词上的争议对编程没有帮助。一般来讲,可以把难以作为序号的输入因素转化为可以作为数组序号认为是一种没有定数的方法,不必称为一种模式。处理超大范围的输入有不同的方法,不一定就是靠一个中介的表驱动。如上面设备工作流例子中对small line buffer的处理。当然也可以把buffer size作为序号,放一个size大小的数组,输出结果是0,1,但是还是太麻烦。
直接模式和索引模式在查找结果数据的方式上没有本质不同,和阶梯模式都有明显不同。
举个汽车管理的例子,按照重量来区分汽车类型。具体数值是否符合汽车专业的定义不影响代码的讨论。这个例子既可以用来演示索引访问模式,又可以演示阶梯访问模式。虽然对这个例子而言最好的方式是阶梯访问模式。
可以看到,写一个5000个元素的数组,每一项表示类型,和采取的动作,如交费,审查等函数操作。太浪费了。那么可以先做一个长度为5000的char数组,输出结果只有4个值,微型,小型,中型和重型。
然后再写一个数组,长度只有4,将汽车类型定义为宏,值从0-3。数组的内容就是交费,审查等函数指针。
#define MINI_CAR 0
#define SMALL_CAR 1
#define MIDDLE_CAR 2
#define HEAVY_CAR 3
#define MAX_CAR 4
int weigth2type[] =
{
MINI_CAR,
MINI_CAR,
...
SMALL_CAR
MIDDLE_CAR,
MIDDLE_CAR,
...
HEAVY_CAR
};
void car_action1()
{
printf("10, express lane");
}
void car_action2()
{
printf("15, ticket");
}
void car_action3()
{
printf("20, check");
}
void car_action4()
{
printf("50, special lane, check tire");
}
void (*func[3])() =
{
car_action1,
car_action2,
car_action3,
car_action3
}
int car_type = weigth2type[wight];
(*func[car_type])();
索引的另外一个好处是,以不同的方式来生成索引,可以从不同的方面来便捷访问数据。比如一个数组存放了姓名,年龄,体重,血压。可以根据体重年龄等来生成索引。在嵌入式实际编码中,这种场景用的不多。
阶梯访问模式
当输入值是一段很大的范围,尤其是浮点时,有些时候无法转化为确定的索引来查表。干脆就放弃直接查表的机会,而且把范围上下限按照次序放在表里,用for循环对比上下限的进行判断。
阶梯模式的输入只能是范围,这个和索引模式有所不同。索引在应付比较离散,没有大小判断规律的输入时也是有效的。
还是用索引模式的汽车例子,不考虑异常情况。
int weigth2type[4]
{
1000,
2000,
3000,
5000
}
for(i = 0; i < 4; i++)
{
if(weight <= weigth2type[i])
{
(*func[i])();
}
}
经典应用
表驱动最常见在状态机设计模式里作为状态转移数组,在命令设计模式里作为命令码数组。
可以参见
设计模式的C语言应用-状态机模式
https://bbs.huaweicloud.com/blogs/a4e37991c45811e7b8317ca23e93a891
设计模式的C语言应用-命令模式
https://bbs.huaweicloud.com/blogs/90b909e8c81711e7b8317ca23e93a891
模式总结
表驱动模式总体思路就是把逻辑关系写到数组里,采用序号获得结果。当代码里出现较多的if,else或者switch的时候,会大幅增加圈复杂度,就需要考虑改写为表驱动了。