switch-case/if-else
对于条件/分支处理的程序设计,我们惯性地会选择switch-case
或者if-else
,这也是C语言老师当初教的。以下,我们用一个播放器的例子来说明,要实现的功能如下:
首先,将命令定义成enum
类型:
enum
{
CMD_PLAY,
CMD_PAUSE,
CMD_STOP,
CMD_PLAY_NEXT,
CMD_PLAY_PREV,
};
然后,用switch-case
的分支处理:
switch(cmd)
{
case CMD_PLAY:
// handle play command
break;
case CMD_PAUSE:
// handle pause command
break;
case CMD_STOP:
// handle stop command
break;
case CMD_PLAY_NEXT:
// handle play next command
break;
case CMD_PLAY_PREV:
// handle play previous command
break;
default:
break;
}
实际上,这也没什么毛病。但是,时间长了,需求不断变更,程序不断迭代,这个switch-case
会变得非常冗长而很难维护。你不相信?我曾经见到过>1000行的类似这样的代码。如果让你接手维护这样的代码,你内心会不会狂奔着万千草泥马?
但是,我不敢更改这个祖传的switch-case
啊,那么小心翼翼地将这些命令处理封装成函数。像这样:
#define FUNC_IN() printf("enter %s \r\n", __FUNCTION__)
void func_cmd_play(void* p)
{
FUNC_IN();
}
void func_cmd_pause(void* p)
{
FUNC_IN();
}
void func_cmd_stop(void* p)
{
FUNC_IN();
}
void func_cmd_play_next(void* p)
{
FUNC_IN();
}
void func_cmd_play_prev(void* p)
{
FUNC_IN();
}
void player_cmd_handle(int cmd, void* p)
{
switch(cmd)
{
case CMD_PLAY:
func_cmd_play(p);
break;
case CMD_PAUSE:
func_cmd_pause(p);
break;
case CMD_STOP:
func_cmd_stop(p);
break;
case CMD_PLAY_NEXT:
func_cmd_play_next(p);
break;
case CMD_PLAY_PREV:
func_cmd_play_prev(p);
break;
default:
break;
}
}
后来,甲方还是不断地更改需求,导致播放器的命令越来越多,几十个上百个了……痛定思痛,我——要——改——革!!
switch-case/if-else
脑子里想来想去,度娘上翻来翻去,于是定义了个结构体:
typedef void(*pFunc)(void* p);
typedef struct
{
tCmd cmd;
pFunc func;
}tPlayerStruct;
tPlayerStruct player_cmd_func[] =
{
{
CMD_PLAY, func_cmd_play) },
{
CMD_PAUSE, func_cmd_pause) },
{
CMD_STOP, func_cmd_stop) },
{
CMD_PLAY_NEXT, func_cmd_play_next) },
{
CMD_PLAY_PREV, func_cmd_play_prev) },
};
#define ARR_LEN(arr) sizeof(arr)/sizeof(arr[0])
void player_cmd_handle(int cmd, void* p)
{
for(int i = 0; i < ARR_LEN(player_cmd_func); i++)
{
if(player_cmd_func[i].cmd == cmd && NULL != player_cmd_func[i].func)
{
player_cmd_func[i].func(p);
break;
}
}
}
咦?好像代码简洁了不少哦,改完之后好有成就感。
身为追求卓越的程序员,我还是有点不满意,可不可以不用for
循环,直接使用player_cmd_func[cmd].func(p);
,这样还可以免去查询的步骤,提高效率?
想法是好的,如果上面的程序不用for
循环,有可能数组越界,还有如果有命令增加,顺序下标不对应的问题。
之前,我在《C语言的奇技淫巧之五》中的第50条提到过这个方法,还立了个flag
,我要用MACRO写个更高效更好的代码!
你听说过X-MACRO
么?听过没听过都没关系,来,我们一起耍起来!
MACRO
或者说宏定义(书上或者规范上一般讲预处理)基本原因都很简单,看看就很容易学会。看起来好像也是平淡无奇,似乎没什么大作用。但是,你可别小看它,我们将其安上个"X"就很牛逼(不知道这个是啥传统,对于某些函数的扩展,喜欢在其前面或后面加个“X”,然后这个函数比之前的函数功能强大很多,Windows里面的Api就有这案例)。
X-MACRO是一种可靠维护代码或数据的并行列表的技术,其相应项必须以相同的顺序出现。它们在至少某些列表无法通过索引组成的地方(例如编译时)最有用。
此类列表的示例尤其包括数组的初始化,枚举常量和函数原型的声明,语句序列和切换臂的生成等。X-MACRO的使用可以追溯到1960年代。它在现代C和C ++编程语言中仍然有用。X-MACRO应用程序包括两部分:
- 列表元素的定义。
- 扩展列表以生成声明或语句的片段。
该列表由一个宏或头文件(名为LIST)定义,该文件本身不生成任何代码,而仅由一系列调用宏(通常称为“ X”)与元素的数据组成。 LIST的每个扩展都在X定义之前加上一个list元素的语法。 LIST的调用会为列表中的每个元素扩展X。
好了,少扯淡,我们是实战派,搞点有用的东西。对于MACRO
有几个明显的特征:
MACRO
实际上就是做替换工作;undef
取消,然后再重新反复定义。我们就用这几个特征把MACRO
耍到牛X起来!
#define X(a,b) a
int x = DEF_X(1,2);
#undef DEF_X
#define DEF_X(a,b) b
int y = DEF_X(1,2);
从上面可以看到,这个x
和y
的值是不一样的。
于是可以定义一个这样的宏:
#define CMD_FUNC \
DEF_X(CMD_PLAY, func_cmd_play) \
DEF_X(CMD_PAUSE, func_cmd_pause) \
DEF_X(CMD_STOP, func_cmd_stop) \
DEF_X(CMD_PLAY_NEXT, func_cmd_play_next) \
DEF_X(CMD_PLAY_PREV, func_cmd_play_prev) \
CMD
的enum
可以这样定义:
typedef enum
{
#define DEF_X(a,b) a,
CMD_FUNC
#undef DEF_X
CMD_MAX
}tCmd;
预编译后,这实际上就是这样的:
typedef enum
{
CMD_PLAY, CMD_PAUSE, CMD_STOP, CMD_PLAY_NEXT, CMD_PLAY_PREV, CMD_MAX
}tCmd;
接着,我们按这种套路定义一个函数指针数组:
const pFunc player_funcs[] =
{
#define DEF_X(a,b) b,
CMD_FUNC
#undef DEF_X
};
甚至,我们可以定义一个命令的字符串,以作打印信息用:
const char* str_cmd[] =
{
#define DEF_X(a,b) #a,
CMD_FUNC
#undef DEF_X
};
只要这个DEF_X(a,b)
里面的a
和b
是对应关系正确的,CMD_FUNC
后面的元素顺序是所谓了,这个比前面的结构体有天然优势。这样,我们就可以直接用下标开始操作了:
void player_cmd_handle(tCmd cmd, void* p)
{
if(cmd < CMD_MAX)
{
player_funcs[cmd](p);
}
else
{
printf("Command(%d) invalid!\n", cmd);
}
}
这不仅提高了效率,还不用担心命令的顺序问题。这种X-MACRO的用法对分支结构,特别是消息命令的处理特别的方便高效。
关注公众号“嵌入式软件实战派”可获得完整测试源码。
另,留个作业题:
如何灵活地将一个结构体的内容系列化到一个数组中,以及如何将一个数组的内容解系列化到结构体中?
例如,将以下结构体s的内容copy到data中(别老想着memcopy哦):
typedef struct STRUCT_DATA
{
int a;
char b;
short c;
}tStruct;
unsigned char data[100];
另见:宏定义X-MACRO的高级应用(高阶版)
关注公众号“嵌入式软件实战派”,获得更多精品。