C 语言对象化设计实例 —— 命令解析器

嵌入式大杂烩 2020-08-31 00:00

之前有朋友问面向对象相关例子,这篇文章分享的就是面向对象的实例,可以学一学。文章出自RTT工程师国际哥,首发于Linux阅码场

前言


传统单片机 MCU 编程大多使用过程式的思维来组织程序,在单片机资源少、功能简单、代码规模小的情况下,「想到啥写啥」的方法也确实能解决大部分问题。但随着硬件的快速升级,如今的大部分嵌入式工程师已经不再需要「掐着内存」来写代码了。当软件的规模越发庞大、复杂,这时如何编写可复用、便于维护的代码显得尤为重要。本文通过一个在 51 单片上实现的简单「串口命令解析器」例子,分析如何通过面向对象思想编写出「高内聚低耦合」的 C 语言程序。

本文是学习宋宝华老师的《C语言大型软件设计的面向对象》课程(地址:http://edu.csdn.net/course/detail/6496)后的一些收获。

相关阅读:

《C语言的面向对象(面向较大型软件)》ppt分享和ppt注解


C 语言也能面向对象?


在许多年轻人眼里,C 是一门既「老土」又「古板」的编程语言,更可怕的是,「C 老头」常年被人贴上「面向过程」的标签,与 Java、Pyhon 等面向对象的高级语言格格不入。

事实上,面向对象只是一种思想,与语言无关(只不过C++、Java 在语法形式上天然支持 OO),灵活的 C 语言当然也能实现面向对象的编程 —— 这些观点我以前也都听过,但仅仅停留在字面意思的感受。直到看了宋老师的直播中的几个实例,我才加深了对 C 语言面向对象的理解,更进一步体会到 OO 思想的强大。其中课程里提到的「命令解析器」便是典型例子,下面和大家分享一下其中的思想精髓与具体实现,体会传统过程式思维与 OO 思维的差异。

PS:由于笔者真是个菜鸡,个人理解难免会有偏差,更多只是拾人牙慧,欢迎指正。



命令解析器



通过命令操控计算机是一件很酷的事情,在 DOS、Linux 系统中也广泛使用命令行的方式。命令操作的核心便是命令解析器(如 Linux 中的 Shell)。命令解析器实现接收命令字符串,解析命令并执行相应操作,在单片机程序中也常常通过串口命令为用户提供操作接口(如 AT 指令)。



过程式设计



简单来说,命令解析器的核心功能其实就是字符串比较,调用相应函数,使用 C 语言的选择结构便可轻松实现,你甚至能直接想到对应代码,于是你写出了像这样的程序:

你非常机智地采用模块化编程,每个子功能都用单独的 .c 文件存放。在 cmd.c 中进行命令的处理,通过条件语句比较命令,匹配后调用 gpio.c、spi.c、i2.c 文件中对应的操作函数,代码一气呵成。我的第一反应也是这样写,嗯,没毛病。

这是典型的过程式思维 —— 先干什么后干什么,把所有零零散散的操作通过一根时间轴串起来,没有丝毫拐弯抹角,非常直接。但这样的过程式设计存在明显的两个问题:

1. 命令增加引起跨模块修改

2. 大量的外部函数,模块间高耦合

下面来具体解释一下遇到的这两个问题。


1. 命令增加引起跨模块修改

假设现在需求变化,要求增加 GPIO翻转 命令产生对应的电平变化。你赶紧在 gpio.c 文件中需要增加一个电平翻转操作函数 gpio_toggle(),同时在 cmd.c 的 switch-case 语句内部添加新增的命令及函数……

等等,这不是很怪么?只是增加了 GPIO 相关功能,命令处理逻辑没变(依然只是判断字符串相等),为什么却要改动 cmd.c 的命令处理逻辑?而且还是没啥技术含量地加了一条 case 语句……

改两个文件或许咬咬牙就算了,如果工程日益增大,导致每增加一条命令都要像「砌墙」或者「拧螺丝」一样做一堆机械重复的工作,这样的代码一点都不酷。

2. 大量的外部函数,模块间高耦合

如果说跨模块修改只是一个「麻烦点儿」的问题,勤快的人毫不在乎(好吧你们赢了),那模块间高耦合则直接影响了代码的复用性 —— 代码不通用!这就不是小问题了。高复用性可谓码农的一大追求,谁不想只写一次代码就可以拼凑成各种大项目,轻轻松松躺着赚钱呢?

某年后,你遇到了一个新系统,其中也需要命令解析器功能模块,于是你兴冲冲把之前写的 cmd.c和 cmd.h 直接拿过来用,却发现编译报错找不到 gpio_high()、gpio_low()、spi_send()……你的内心是崩溃的。

由于 gpio_high()、gpio_low() 等函数都是 gpio.c 中的外部函数,在 cmd.c 中直接通过函数名调用,两个文件像缠绵的情侣般高度耦合,这种紧密的联系破坏了C 程序设计的一个基本原则 —— 模块的独立性。采用了模块化编程,然而每个模块却不能独立使用,意义何在?



面向对象设计



在前面发现的两个问题上对症下药,可以得到程序的改进目标:

1. 增加或减少命令不影响 cmd.c

2. 命令的处理函数要成为 static,去耦合



OO思想


在解决这两个问题前,让我们回到思维层面,对比「面向对象」与「面向过程」思想的区别。当我们谈论面向过程思维时,程序员的角色像一个统治者,掌管一切、什么都要插一手。

举个典型例子,要把大象装到冰箱需要三步:

1. 打开冰箱门

2. 将大象放进冰箱

3. 关闭冰箱门

这一系列步骤的主动权都牢牢掌握在操作者手里,操作者按部就班地把具体操作与时间轴绑定起来,是典型的过程思维。再回到前面匹配命令的 switch-case 语句上,每增加一条新命令都需要程序员手把手地把命令和函数写死在程序中。于是我们就会想,能不能让命令解析器作为一个主动的个体自己增加命令?

这里就引入了「对象」的概念,什么是对象?我们所关注的一切事物皆为对象。在「把大象装到冰箱」问题中,把「大象」、「冰箱」这两个名词提取出来,就是两个对象。过程式思维解决问题时考虑「需要哪些步骤」,而 OO 思想考虑「需要哪些对象」。

还是这个例子,要把大象装到冰箱只需要两个对象:

1. 冰箱

2. 大象

如何描述一个对象呢?可以通过两个方面,一是对象的特征(属性),二是对象的行为(方法/函数)。由此可以列举出描述大象和冰箱的一些属性和方法:

大象的属性(特征):品种、体形、鼻长……

大象的方法(行为):进食、走路、睡觉……

冰箱的属性(特征):价格、容量、功耗……

冰箱的方法(行为):开关机、开关门、除霜去冰……

对象有如此多的属性和方法,但实际上并不都能用得上。不同问题涉及到对象的不同方面,因此可以忽略无关的属性、方法。对于「把大象装到冰箱」这个问题,我们只关心「大象的体形」、「冰箱的容量」、「大象走路(说不定能让大象自己走进冰箱)」、「冰箱开关门」等这些与问题相关的属性和方法。

于是程序就成了「冰箱开门、大象走进冰箱并告诉冰箱关门」的模式,将操作的主动权归还对象本身时,程序员不再是霸道的统治者,而是扮演管理员的角色,协调各对象基于自身的属性和方法完成所需功能。



OO 版命令解析器



回归正题,如何才能解决前面的两个问题、让命令解析器更「OO」呢?首先对最终功能 ——「命令解析器解析命令」这句话深度挖掘,注意到「命令」、「命令解析器」这两个名词可以抽象成对象。

命令类型的封装

首先是「命令」本身可以封装为包含「命令名」和「对应操作」两个成员的结构体,前者是属性,可用字符数组存储,后者在逻辑上是行为/函数,但由于 C 语言结构体不支持函数,可用函数指针存储。这相当于把「命令」定义成了新的数据类型,将命令与操作联系起来。


// 文件名称:cmd.h

 

#define     MAX_CMD_NAME_LENGTH     20    // 最大命令名长度,过大 51 内存会炸

#define     MAX_CMDS_COUNT          10    // 最大命令数,过大 51 内存会炸

 

typedef void (*handler)(void);        // 命令操作函数指针类型

 

/* 命令结构体类型 */

typedef struct cmd

{

    char cmd_name[MAX_CMD_NAME_LENGTH + 1];   // 命令名 

    handler cmd_operate;                      // 命令操作函数

} CMD;


其中宏 MAX_CMD_NAME_LENGTH 表示所存储命令名的最大长度,handler 为指向命令操作函数的指针,所有命令操作函数均为无参无返回值。

命令解析器的封装

同理,「命令解析器」这一模块也可以看做一个对象,对功能模块的封装已经在文件结构上体现,就没必要用结构体了,我们重点关注对象的内部(即成员变量与成员函数)。

成员变量

命令解析器要从一堆命令中匹配一个,因此需要一种能存储命令集合的数据结构,这里使用数组实现线性表:


// 文件名称:cmd.h

 

/* 命令列表结构体类型 */

typedef struct cmds

{

    CMD cmds[MAX_CMDS_COUNT];  // 列表内容

    int num;                   // 列表长度

} CMDS;


通过结构体封装数据类型定义成员变量类型,方便在 cmd.c 中使用:


// 文件名称:cmd.c

 

static xdata CMDS commands = {NULL, 0};  // 全局命令列表,保存已注册命令集合


为了简化程序,线性表的「增删改查」等基本操作就不一一独立实现了,而是与命令处理过程结合(命令的注册与匹配其实就是插入与查找过程)。下面考虑对象的成员函数。

成员函数

命令解析器涉及到那些行为呢?首要任务当然是匹配并执行指令。其次,要对外提供增加命令的接口函数,由处理命令功能模块主动注册命令,而不是通过代码写死,从而就避免了跨模块修改,硬件无关的代码也提高了程序的可移植性。

编写 match_cmd() 函数实现命令匹配,该函数接收一个待匹配的命令字符串作为参数,对命令列表进行遍历比较操作:


// 文件名称:cmd.c

 

void match_cmd(char *str)

{

    int i;

 

    if (strlen(str) > MAX_CMD_NAME_LENGTH)

    {

        return;

    }

 

    for (i = 0; i < commands.num; i++)  // 遍历命令列表

    {

        if (strcmp(commands.cmds[i].cmd_name, str) == 0)

        {

            commands.cmds[i].cmd_operate();

        }

    }

}


接着再实现注册命令函数,该函数接收一个命令类型数组,插入到命令解析器的命令列表中:


// 文件名称:cmd.c

 

void register_cmds(CMD reg_cmds[], int length)

{

    int i;

 

    if (length > MAX_CMDS_COUNT)

    {

        return;

    }

 

    for (i = 0; i < length; i++)

    {

        if (commands.num < MAX_CMDS_COUNT)  // 命令列表未满

        {

            strcpy(commands.cmds[commands.num].cmd_name, reg_cmds[i].cmd_name);

            commands.cmds[commands.num].cmd_operate = reg_cmds[i].cmd_operate;

            commands.num++;

        }  

    }  

}


至此,命令解析器便大功告成!通过调用两个函数即可完成命令的添加与匹配功能,接下来编写 LED 灯和蜂鸣器的操作函数,测试命令解析器功能。

命令解析器的使用

注册和匹配命令

编写 led.c 文件,实现 LED 的亮灭操作函数,在 led_init() 函数中注册命令并初始化硬件:


// 文件名称:led.c

 

static void led_on(void)

{

    LED1 = 0;

}

 

static void led_off(void)

{

    LED1 = 1;

}

 

void led_init(void)

{

    /* 填充命令结构体数组 */

    CMD led_cmds[] = {

        {"led on", led_on},

        {"led off", led_off}

    };

 

    /* 注册命令 */

    register_cmds(led_cmds, ARRAY_SIZE(led_cmds)); 

 

    /* 初始化硬件 */

    led_off();

}


可以看到,命令处理函数 led_on() 和 led_off() 都是 static 修饰的内部函数,在其他模块中不能通过函数名直接调用,而是通过函数指针的方式传递,实现了模块间解耦。再者,使用结构体数组注册命令,大大增加程序扩展性。

按照同样的套路编写 beep.c 文件实现蜂鸣器控制命令。

最后,在主函数 while(1) 循环中接受串口字符串、解析命令并执行:


// 文件名称:main.c

 

void main()

{

    unsigned char str[20];

 

    uart_init();

    led_init();

    beep_init();

 

    while (1)

    {  

        /* 获取串口命令字符串 */

        uart_get_string(str);

 

        /* 匹配命令并执行 */

        match_cmd(str);

 

        /* 命令回显 */

        uart_send_string(str);

        uart_send_byte('\n');                  

    }

}


增加命令

在经过了高度抽象封装的命令解析器上增加一条命令,如 LED 翻转,只需要在 led.c 中增加 led_toggle() 函数,并往待注册的命令结构体数组初始化列表中添加一个元素,然后……就完了,即使加 100 条新命令也完全不需要动 cmd.c 中的代码,两个模块彼此独立。


// 文件名称:led.c

 

static void led_toggle(void)  // 增加 LED 翻转函数

{

    LED1 = ~LED1;

}

 

void led_init(void)

{

    /* 填充命令结构体数组 */

    CMD led_cmds[] = {

        {"led on", led_on},

        {"led off", led_off},

        {"led toggle", led_toggle}  // 增加 LED 翻转命令

    };

 

    /* 注册命令 */

    register_cmds(led_cmds, ARRAY_SIZE(led_cmds)); 

 

    /* 初始化硬件 */

    led_off();

}


此外,如果 cmd.c 中改用其他数据结构存储命令集合,也与 led.c 无关,彻底切断两个文件的强耦合。cmd.c 现已升级为一个通用的命令解析器。



实验效果





总结



从最初手动往 cmd.c 中添加命令代码,到最后通过函数「智能操作」,OO 思想实现把权利下放,每个模块自己的事自己解决(功能模块需要命令功能时自己主动注册即可),程序员再也不用对所有细节亲力亲为,而是为每个对象赋予该有的能力,然后对它们说上一句:「你办事我放心」!

工程示例代码下载:链接:http://pan.baidu.com/s/1geKE2ll 密码:e0ku


(END)


Linux阅码场原创精华文章汇总

更多精彩,尽在"Linux阅码场",扫描下方二维码关注


=========留言区========


嵌入式大杂烩 专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!
评论 (0)
  • 网约车,真的“饱和”了?近日,网约车市场的 “饱和” 话题再度引发热议。多地陆续发布网约车风险预警,提醒从业者谨慎入局,这背后究竟隐藏着怎样的市场现状呢?从数据来看,网约车市场的“过剩”现象已愈发明显。以东莞为例,截至2024年12月底,全市网约车数量超过5.77万辆,考取网约车驾驶员证的人数更是超过13.48万人。随着司机数量的不断攀升,订单量却未能同步增长,导致单车日均接单量和营收双双下降。2024年下半年,东莞网约出租车单车日均订单量约10.5单,而单车日均营收也不容乐
    用户1742991715177 2025-04-29 18:28 201浏览
  • 浪潮之上:智能时代的觉醒    近日参加了一场课题的答辩,这是医疗人工智能揭榜挂帅的国家项目的地区考场,参与者众多,围绕着医疗健康的主题,八仙过海各显神通,百花齐放。   中国大地正在发生着激动人心的场景:深圳前海深港人工智能算力中心高速运转的液冷服务器,武汉马路上自动驾驶出租车穿行的智慧道路,机器人参与北京的马拉松竞赛。从中央到地方,人工智能相关政策和消息如雨后春笋般不断出台,数字中国的建设图景正在智能浪潮中徐徐展开,战略布局如同围棋
    广州铁金刚 2025-04-30 15:24 169浏览
  • 贞光科技代理品牌紫光国芯的车规级LPDDR4内存正成为智能驾驶舱的核心选择。在汽车电子国产化浪潮中,其产品以宽温域稳定工作能力、优异电磁兼容性和超长使用寿命赢得市场认可。紫光国芯不仅确保供应链安全可控,还提供专业本地技术支持。面向未来,紫光国芯正研发LPDDR5车规级产品,将以更高带宽、更低功耗支持汽车智能化发展。随着智能网联汽车的迅猛发展,智能驾驶舱作为人机交互的核心载体,对处理器和存储器的性能与可靠性提出了更高要求。在汽车电子国产化浪潮中,贞光科技代理品牌紫光国芯的车规级LPDDR4内存凭借
    贞光科技 2025-04-28 16:52 233浏览
  • 你是不是也有在公共场合被偷看手机或笔电的经验呢?科技时代下,不少现代人的各式机密数据都在手机、平板或是笔电等可携式的3C产品上处理,若是经常性地需要在公共场合使用,不管是工作上的机密文件,或是重要的个人信息等,民众都有防窃防盗意识,为了避免他人窥探内容,都会选择使用「防窥保护贴片」,以防止数据外泄。现今市面上「防窥保护贴」、「防窥片」、「屏幕防窥膜」等产品就是这种目的下产物 (以下简称防窥片)!防窥片功能与常见问题解析首先,防窥片最主要的功能就是用来防止他人窥视屏幕上的隐私信息,它是利用百叶窗的
    百佳泰测试实验室 2025-04-30 13:28 239浏览
  • 文/Leon编辑/cc孙聪颖‍2023年,厨电行业在相对平稳的市场环境中迎来温和复苏,看似为行业增长积蓄势能。带着对市场向好的预期,2024 年初,老板电器副董事长兼总经理任富佳为企业定下双位数增长目标。然而现实与预期相悖,过去一年,这家老牌厨电企业不仅未能达成业绩目标,曾提出的“三年再造一个老板电器”愿景,也因市场下行压力面临落空风险。作为“企二代”管理者,任富佳在掌舵企业穿越市场周期的过程中,正面临着前所未有的挑战。4月29日,老板电器(002508.SZ)发布了2024年年度报告及2025
    华尔街科技眼 2025-04-30 12:40 184浏览
  • 一、gao效冷却与控温机制‌1、‌冷媒流动设计‌采用低压液氮(或液氦)通过毛细管路导入蒸发器,蒸汽喷射至样品腔实现快速冷却,冷却效率高(室温至80K约20分钟,至4.2K约30分钟)。通过控温仪动态调节蒸发器加热功率,结合温度传感器(如PT100铂电阻或Cernox磁场不敏感传感器),实现±0.01K的高精度温度稳定性。2、‌宽温区覆盖与扩展性‌标准温区为80K-325K,通过降压选件可将下限延伸至65K(液氮模式)或4K(液氦模式)。可选配475K高温模块,满足材料在ji端温度下的性能测试需求
    锦正茂科技 2025-04-30 13:08 179浏览
  • 文/郭楚妤编辑/cc孙聪颖‍越来越多的企业开始蚕食动力电池市场,行业“去宁王化”态势逐渐明显。随着这种趋势的加强,打开新的市场对于宁德时代而言至关重要。“我们不希望被定义为电池的制造者,而是希望把自己称作新能源产业的开拓者。”4月21日,在宁德时代举行的“超级科技日”发布会上,宁德时代掌门人曾毓群如是说。随着宁德时代核心新品骁遥双核电池的发布,其搭载的“电电增程”技术也走进业界视野。除此之外,经过近3年试水,宁德时代在换电业务上重资加码。曾毓群认为换电是一个重资产、高投入、长周期的产业,涉及的利
    华尔街科技眼 2025-04-28 21:55 147浏览
  •  探针台的维护直接影响其测试精度与使用寿命,需结合日常清洁、环境控制、定期校准等多维度操作,具体方法如下:一、日常清洁与保养1.‌表面清洁‌l 使用无尘布或软布擦拭探针台表面,避免残留清洁剂或硬物划伤精密部件。l 探针头清洁需用非腐蚀性溶剂(如异丙醇)擦拭,检查是否弯曲或损坏。2.‌光部件维护‌l 镜头、观察窗等光学部件用镜头纸蘸取wu水jiu精从中心向外轻擦,操作时远离火源并保持通风。3.‌内部防尘‌l 使用后及时吹扫灰尘,防止污染物进入机械滑
    锦正茂科技 2025-04-28 11:45 107浏览
  • 4月22日下午,备受瞩目的飞凌嵌入式「2025嵌入式及边缘AI技术论坛」在深圳深铁皇冠假日酒店盛大举行,此次活动邀请到了200余位嵌入式技术领域的技术专家、企业代表和工程师用户,共享嵌入式及边缘AI技术的盛宴!1、精彩纷呈的展区产品及方案展区是本场活动的第一场重头戏,从硬件产品到软件系统,从企业级应用到高校教学应用,都吸引了现场来宾的驻足观看和交流讨论。全产品矩阵展区展示了飞凌嵌入式丰富的产品线,从嵌入式板卡到工控机,从进口芯片平台到全国产平台,无不体现出飞凌嵌入式在嵌入式主控设备研发设计方面的
    飞凌嵌入式 2025-04-28 14:43 140浏览
  • 在智能硬件设备趋向微型化的背景下,语音芯片方案厂商针对小体积设备开发了多款超小型语音芯片方案,其中WTV系列和WT2003H系列凭借其QFN封装设计、高性能与高集成度,成为微型设备语音方案的理想选择。以下从封装特性、功能优势及典型应用场景三个方面进行详细介绍。一、超小体积封装:QFN技术的核心优势WTV系列与WT2003H系列均提供QFN封装(如QFN32,尺寸为4×4mm),这种封装形式具有以下特点:体积紧凑:QFN封装通过减少引脚间距和优化内部结构,显著缩小芯片体积,适用于智能门铃、穿戴设备
    广州唯创电子 2025-04-30 09:02 205浏览
  • 随着电子元器件的快速发展,导致各种常见的贴片电阻元器件也越来越小,给我们分辨也就变得越来越难,下面就由smt贴片加工厂_安徽英特丽就来告诉大家如何分辨的SMT贴片元器件。先来看看贴片电感和贴片电容的区分:(1)看颜色(黑色)——一般黑色都是贴片电感。贴片电容只有勇于精密设备中的贴片钽电容才是黑色的,其他普通贴片电容基本都不是黑色的。(2)看型号标码——贴片电感以L开头,贴片电容以C开头。从外形是圆形初步判断应为电感,测量两端电阻为零点几欧,则为电感。(3)检测——贴片电感一般阻值小,更没有“充放
    贴片加工小安 2025-04-29 14:59 195浏览
  • 在CAN总线分析软件领域,当CANoe不再是唯一选择时,虹科PCAN-Explorer 6软件成为了一个有竞争力的解决方案。在现代工业控制和汽车领域,CAN总线分析软件的重要性不言而喻。随着技术的进步和市场需求的多样化,单一的解决方案已无法满足所有用户的需求。正是在这样的背景下,虹科PCAN-Explorer 6软件以其独特的模块化设计和灵活的功能扩展,为CAN总线分析领域带来了新的选择和可能性。本文将深入探讨虹科PCAN-Explorer 6软件如何以其创新的模块化插件策略,提供定制化的功能选
    虹科汽车智能互联 2025-04-28 16:00 174浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦