当前位置:首页 > > 嵌入式大杂烩
[导读]一文了解宏的高级用法。

使用switch-case/if-else

对于条件/分支处理的程序设计,我们惯性地会选择switch-case或者if-else,这也是C语言老师当初教的。以下,我们用一个播放器的例子来说明,要实现的功能如下:

  1. 收到用户操作播放器命令请求,如“播放”、“暂停”等,程序要对命令作区分;

  2. 针对不同的命令请求,作相应的处理;

  3. 输出必要的辅助信息。

首先,将命令定义成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

你听说过X-MACRO么?听过没听过都没关系,来,我们一起耍起来!

MACRO或者说宏定义(书上或者规范上一般讲预处理)基本原因都很简单,看看就很容易学会。看起来好像也是平淡无奇,似乎没什么大作用。但是,你可别小看它,我们将其安上个"X"就很牛逼(不知道这个是啥传统,对于某些函数的扩展,喜欢在其前面或后面加个“X”,然后这个函数比之前的函数功能强大很多,Windows里面的Api就有这案例)。

X-MACRO是一种可靠维护代码或数据的并行列表的技术,其相应项必须以相同的顺序出现。它们在至少某些列表无法通过索引组成的地方(例如编译时)最有用。此类列表的示例尤其包括数组的初始化,枚举常量和函数原型的声明,语句序列和切换臂的生成等。X-MACRO的使用可以追溯到1960年代。它在现代C和C ++编程语言中仍然有用。

X-MACRO应用程序包括两部分:

  1. 列表元素的定义。

  2. 扩展列表以生成声明或语句的片段。

该列表由一个宏或头文件(名为LIST)定义,该文件本身不生成任何代码,而仅由一系列调用宏(通常称为“ X”)与元素的数据组成。LIST的每个扩展都在X定义之前加上一个list元素的语法。LIST的调用会为列表中的每个元素扩展X。

好了,少扯淡,我们是实战派,搞点有用的东西。

对于MACRO有几个明显的特征:

  1. MACRO实际上就是做替换工作;

  2. 宏定义的替换工作是在编译前进行的,即预编译;

  3. 宏定义可以用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);

从上面可以看到,这个xy的值是不一样的。

于是可以定义一个这样的宏:

 #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)   \

CMDenum可以这样定义:

 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)里面的ab是对应关系正确的,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的用法对分支结构,特别是消息命令的处理特别的方便高效。

以下附上该案例的完整测试源码:

 #include 
 
 #define FUNC_IN()   printf("enter %s \r\n", __FUNCTION__)
 
 #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)   \
 
 typedef enum
 {
     #define DEF_X(a,b) a,
     CMD_FUNC
     #undef DEF_X
     CMD_MAX
 }tCmd;
 
 const char* str_cmd[] =
 {
     #define DEF_X(a,b) #a,
     CMD_FUNC
     #undef DEF_X
 };
 
 typedef void(*pFunc)(void* p);
 
 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();
 }
 
 const pFunc player_funcs[] =
 {
     #define DEF_X(a,b) b,
     CMD_FUNC
     #undef DEF_X
 };
 
 void player_cmd_handle(tCmd cmd, void* p)
 {
     if(cmd < CMD_MAX)
    {
         player_funcs[cmd](p);
    }
     else
    {
         printf("Command(%d) invalid!\n", cmd);
    }
 }
 
 int main(void)
 {
     player_cmd_handle(CMD_PAUSE, (void*)0);
     player_cmd_handle(100, (void*)0);
     return 0;
 }

留个作业题:

如何灵活地将一个结构体的内容系列化到一个数组中,以及如何将一个数组的内容解系列化到结构体中?

例如,将以下结构体s的内容copy到data中(别老想着memcopy哦):

 typedef struct STRUCT_DATA
 {
     int a;
     char b;
     short c;

 }tStruct;

tStruct s;

 
 unsigned char data[100];


最后

以上就是本次的分享,如果觉得文章不错,转发、在看,也是我们继续更新的动力。

猜你喜欢:

干货 | 结构体、联合体嵌套使用的一些实用操作

2020年精选原创笔记汇总


1024G 嵌入式资源大放送!包括但不限于C/C++、单片机、Linux等。在公众号聊天界面回复1024,即可免费获取!


免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭