当前位置: 首页 > Linux

邵国际:C语言对象设计实例——命令解析器

时间:2023-04-07 01:40:03 Linux

本文转载,版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。作者:ShaoInternational来源:微信公众号linux代码阅读领域(id:linuxdev)简介MCU工程师常常疑惑为什么Linux驱动框架有这么复杂的一套,却不知道这种“复杂”就是面向对象设计的本质。代码的高度抽象和封装可以大大提高软件的可重用性和可维护性。本文从一个简单的例子——51单片机上的串口命令解析器程序入手,比较过程式思维和对象式思维的区别,分享我对OO的浅见。笔者介绍的是计算机专业学生邵国际,擅长动手,热衷于物联网。用技术表达自己。虽然我是个玩过单片机的渣男,但总想做出好玩有趣的东西(软件/硬件),享受其中的乐趣。目前在深圳增长知识,学习嵌入式开发技术。前言传统的单片机单片机编程大多采用过程化思维来组织程序。在单片机资源少、功能简单、代码量小的情况下,“想到什么就写什么”的方法确实可以解决大部分问题。但随着硬件的快速升级,如今的大多数嵌入式工程师已经不需要“捏着内存”来写代码了。当软件规模越来越大、越来越复杂时,如何编写可重用、易于维护的代码就显得尤为重要。本文以一个简单的在51单片机上实现的“串口命令解析器”为例,分析如何通过面向对象的思想编写一个“高内聚低耦合”的C语言程序。本文是学习宋宝华老师的《C语言大型软件设计的面向对象》课程(地址:http://edu.csdn.net/course/de...)后的一些收获。相关阅读:《C语言的面向对象(面向较大型软件)》ppt分享及ppt注释https://mp.weixin.qq.com/s?__...C语言也能面向对象?在很多年轻人眼里,C是一门“老土”、“老土”的编程语言。更可怕的是,“C老头”常年被贴上“面向过程”的标签,与Java、Python等面向对象的高级语言格格不入。其实面向对象只是一种思维,与语言无关(除了C++和Java在语法上天然支持OO),灵活的C语言当然可以实现面向对象编程——这些观点我以前也听过,但只是停留在字面意义上。直到看了宋老师直播中的几个例子,才加深了对面向对象C语言的理解,进一步体会到OO思维的威力。其中,课程中提到的“命令解析器”就是一个典型的例子。下面给大家分享一下思路的本质和具体实现,体会一下传统过程式思维和OO思维的区别。PS:由于作者实在是菜鸟,个人理解难免有偏差,更多的是提神。欢迎指正。命令解析器通过命令来控制计算机是一件很酷的事情,命令行在DOS和Linux系统中也被广泛使用。命令操作的核心是命令解析器(例如Linux中的Shell)。命令解析器接收命令字符串,解析命令并执行相应的操作。在单片机程序中,往往通过串口指令为用户提供操作接口(如AT指令)。简单来说,命令解析器的核心功能就是比较字符串并调用相应的函数,利用C语言的选择结构可以很容易地实现。你甚至可以直接想到相应的代码,所以你写了这样的程序:你巧妙地采用了模块化编程,每个子功能都存储在一个单独的.c文件中。处理cmd.c中的命令,通过条件语句比较命令,匹配后调用gpio.c、spi.c、i2.c文件中相应的操作函数,代码一气呵成。我的第一反应是这样写,嗯,没什么不对的。这是典型的程序化思维——先做你该做的,再做你该做的,把所有分散的操作通过一个时间轴串联起来,没有任何迂回,非常直接。但是这种程序化设计有两个明显的问题:命令的增加导致大量外部函数需要跨模块修改,模块间的高耦合下面会详细说明。1、增加命令导致跨模块修改。假设电流需求发生变化,需要增加GPIO翻转命令来产生相应的电平变化。你很快需要在gpio.c文件中添加一个电平翻转操作函数gpio_toggle(),同时在cmd.c的switch-case语句中添加新的命令和函数……等等,这不是很奇怪吗?只增加了GPIO相关的功能,命令处理逻辑没有改变(还是只是判断字符串是否相等),为什么要改变cmd.c的命令处理逻辑呢?而且我还是加了一个没有任何技术含量的casestatement。。。可能只是硬着头皮改了两个文件。如果项目越来越大,它会导致每一个额外的命令都像“建一堵墙”或“拧螺丝”一样完成。一堆机械的重复工作,这样的代码一点都不酷。2.大量外部函数,模块间耦合度高如果跨模块修改只是一个“麻烦”的问题,勤奋的人不在乎(好吧,你赢了),那么模块间的耦合度高会直接影响到代码。可重用性——代码不是通用的!这不是一个小问题。高复用性可谓是码农的一大追求。谁不想只写一次代码,把各种大项目拼凑起来,轻松赚钱?一年后遇到新系统,同样需要一个命令解析器功能模块,于是兴冲冲的拿了之前写的cmd.c和cmd.h直接用,却发现编译报错gpio_high()找不到,gpio_low(),spi_send()...你的心都碎了。因为gpio_high()、gpio_low()等函数在gpio.c中是外部函数,在cmd.c中直接通过函数名调用,两个文件像一对缠绵的情侣一样高度耦合。这种紧密联系破坏了C语言编程的一个基本原则——模块的独立性。采用模块化编程,但各个模块不能独立使用。重点是什么?面向对象设计针对上面发现的两个问题对症下药,可以得到程序的改进目标:增加或减少命令不影响cmd的处理功能。让我们回到思维层面,比较一下“面向对象”和“面向过程”思维的区别。当我们谈到面向过程的思想时,程序员的角色就像一把尺子,掌管一切,掌握一切。举个典型的例子,把大象装进冰箱需要三个步骤:打开冰箱门,把大象放进冰箱,关上门。操作绑定在时间轴上,是典型的流程思维。回到之前匹配命令的switch-case语句,每增加一个新的命令,程序员都需要手动将命令和函数写入程序中。那么我们会想,命令解析器能不能作为一个主动个体来添加命令呢?这里引入“对象”的概念。什么是对象?我们关注的一切都是一个对象。在“把大象放进冰箱”的问题中,抽取“大象”和“冰箱”这两个名词是两个对象。过程性思维考虑的是解决问题时“需要哪些步骤”,而面向对象的思维考虑的是“需要哪些对象”。还是在这个例子中,把大象放进冰箱只需要两个对象:冰箱大象如何描述一个对象?有两个方面,一个是对象的特性(属性),一个是对象的行为(方法/功能)。由此我们可以列举一些描述大象和冰箱的属性和方法:?大象属性(特征):品种、体型、躯干长度...?大象方法(行为):吃、走、睡...?属性(冰箱的功能):价格、容量、功耗...?冰箱的方法(行为):开关机、开门和关门、除霜和除冰...对象有很多属性和方法,但不是所有的它们实际上可以派上用场。不同的问题涉及对象的不同方面,因此可以忽略不相关的属性和方法。对于“冰箱里放一头大象”的问题,我们只关心“大象的形状”、“冰箱的容量”、“大象走路(也许大象可以自己走进冰箱)”,“打开和关闭冰箱门”等与问题相关的属性和方法。于是程序就变成了“打开冰箱门,大象走进冰箱,告诉冰箱关门”的模型。当操作的主动权交还给对象本身,程序员不再是霸道的统治者,而是扮演管理员的角色。根据自身的属性和方法,协调各个对象完成需要的功能。OO版本的commandparser回归正题,如何解决前面两个问题,让commandparser更加“OO”呢?首先深挖最后一个函数——“commandparser解析命令”,注意两个名词“命令”和“命令解析器”可以抽象为对象。命令类型的封装是可以将“命令”本身封装成一个结构体,它包含两个成员:“命令名”和“对应的操作”。前者是属性,可以用字符数组存储,后者逻辑上是行为/函数,但由于C语言结构体不支持函数,所以可以用函数指针存储。这相当于将“命令”定义为一种新的数据类型,将命令与操作联系起来。//文件名:cmd.h#defineMAX_CMD_NAME_LENGTH20//最大命令名长度,如果太大,51,内存会爆炸#defineMAX_CMDS_COUNT10//最大命令数,如果太大,51、内存会被炸typedefvoid(*handler)(void);//命令操作函数指针类型/*命令结构类型*/typedefstructcmd{charcmd_name[MAX_CMD_NAME_LENGTH+1];//命令名称处理程序cmd_operate;//命令操作函数}CMD;存储的宏MAX_CMD_NAME_LENGTH名字的最大长度,handler是一个指向命令操作函数的指针,所有的命令操作函数都没有参数也没有返回值。命令解析器的封装也是一样的。“命令解析器”模块也可以看作是一个对象。功能模块的封装已经在文件结构中体现出来了,就不用再用结构体了。我们关注对象的内部(即成员变量和成员函数)。成员变量命令解析器需要从一堆命令中匹配出一个命令,所以它需要一个可以存储一组命令的数据结构。这里用一个数组来实现一个线性表://文件名:cmd.h/*命令列表结构类型*/typedefstructcmds{CMDcmds[MAX_CMDS_COUNT];//列表内容intnum;//列表长度}CMDS;通过结构体封装数据类型定义成员变量类型,方便在cmd.c中使用://文件名:cmd.c静态xdataCMDS命令={NULL,0};//全局命令列表,保存已注册命令的集合为了简化程序,线性表的“增删改查”等基本操作没有单独一一实现,而是与命令组合处理(命令的注册和匹配实际上是插入和查找过程)。接下来考虑一个对象的成员函数。成员函数命令解析器涉及哪些行为?第一项任务当然是匹配和执行指令。其次,需要提供一个对外添加命令的接口函数,命令处理功能模块主动注册命令而不是写硬代码,这样就避免了跨模块的修改,硬件无关的代码也提高了可移植性的程序。编写match_cmd()函数实现命令匹配。该函数接收一个待匹配的命令字符串作为参数,对命令列表进行遍历比较操作://文件名:cmd.cvoidmatch_cmd(char*str){inti;如果(strlen(str)>MAX_CMD_NAME_LENGTH){返回;}for(i=0;iMAX_CMDS_COUNT){返回;}for(i=0;i

猜你喜欢