|
本帖最后由 chinacool 于 2022-3-3 16:45 编辑
以前单片机中写程序,基本都是C语言开发,包括底层驱动和上层应用程序。诚然,用C完全能够完成我的项目,何况人家LINUX那
么大的系统内核代码也基本都是C开发的。
但是当系统软件规模上去后,务必要对整个系统各个模块要进行划分,同时要深入嵌入面向对象开发的思想,否则越往后,软件将越来越
难以维护。另外,在开发软件过程中,经常会遇到一个情况:由于成本或部件停产等等基他原因,原来的某个部件要换型号或厂家,
如果软件没有很好的做到接口与实现分离,后期的改动将很痛苦。而“接口与实现分离”是面向对象编程的基本原则。
再则,C++提供了STL库,函数丰富,同时支持各种高级的搞法,如类,多态,模板等等,这些都可以通过C语言模拟出来,但最后会发现那指针指得头发晕,
如果用C++写可以让代码写得更加优雅和轻松。
废话不多说了,来个小demo,
场景:我原来机器上用了一个柱塞泵A,后来因需要又要换一个厂家的柱塞泵B,接口协议啥的都不一样。
解决思路:将不同厂家柱塞泵相同的东西抽离出来,设计成基类,将不同部分设计成派生类。柱塞泵都有一些相同的动作,比如:吸液,排液,滴定控制,停止等等
// 定义注塞泵/注射泵父类结构
class pistonpump_base
{
protected:
uint8_t isManual; //0=自动,1=手动
uint8_t stopflag; //0=无动作,1=急停
int16_t motor_steps; //滴定时电机步进数
uint32_t titrate_num; //滴定数
public:
explicit pistonpump_base(uint8_t mode, uint8_t flag, int16_t steps, uint32_t num)
: isManual(mode), stopflag(flag), motor_steps(steps), titrate_num(num)
{}
virtual ~pistonpump_base(){};
.....
virtual int drainEmpty(void)=0; //排空
virtual int suction(void)=0; //吸满
virtual int titrate(void)=0; //滴定
virtual int stop(void)=0; //停止
virtual int get_position(int*)=0; //查询活塞位置
virtual int valve_ctrl(Piston_Valve_Ctrl_t )=0; //阀控制 0=关闭,1=打开
};
柱塞泵A:继承自柱塞泵基类。
class keyto_pump : public pistonpump_base
{
public:
keyto_pump();
~keyto_pump(){}
virtual int drainEmpty(void) override; //排空
virtual int suction(void) override; //吸满
virtual int titrate(void) override; //滴定
virtual int stop(void) override; //停止
virtual int get_position(int*) override; //查询活塞位置
virtual int valve_ctrl(Piston_Valve_Ctrl_t ) override; //阀控制 0=关闭,1=打开
};
在派生类中去实现具体的应用接口。
某一天,公司说要再增加一款柱塞泵,或者更换一款泵,这时就可以从容的再加一点代码,
#include "pistonpump.hpp"
/////////////////////////////////////////////////////////////////////////////////////////
class runze_sy08 : public pistonpump_base
{
public:
runze_sy08();
~runze_sy08(){}
virtual int drainEmpty(void) override; //排空
virtual int suction(void) override; //吸满
virtual int titrate(void) override; //滴定
virtual int stop(void) override; //停止
virtual int get_position(int*)override; //查询活塞位置
virtual int valve_ctrl(Piston_Valve_Ctrl_t ) override; //阀控制 0=关闭,1=打开
};
应用程序基本不用作任何修改,因为应用程序调用的是基类的接口。至于下面倒底是谁在实现跟本不用管,通常我需要添加两个相应的文件.cpp, .h就行了。
//手动操作: 排空/吸液/滴定
switch(act.step)
{
case STEP_DRAIN: //柱塞泵排液
res = pistonpump->drainEmpty();
break;
case STEP_SUCTION: //柱塞泵吸液
res = pistonpump->suction();
break;
case STEP_TITRATE: //柱塞泵滴定
res = pistonpump->titrate();
break;
default:
break;
}
如果再要增加其他柱塞泵,那就再接着加好了,心中一点不慌。这样就实现了接口与实现的完全分离。而实现这样效果的就是C++中的多态性。
用C语言也是可以 实现多态性的,操作起来C++效果差不多,但在应用 程序中理解起来就比较废劲了,因为指针用的很多,最开始我用这种方法实现,
后来我自己也受不了了,就全部改成C++实现了:
这里要补充说明一下,C++实现多态的核心是虚函数,当类中有虚函数声明时,编译器会给类创建一个虚表vtab,这个虚表中装着基类或派生类的虚函数地址,
在创建类对象时,编译器会给对象内存中安插一个指针vptr, 这个指针指向vtab. 用C模拟C++多态也就要手动做一个类似的虚表。
看另外一个C实现的例子:
.h 文件.....................................
struct potentio_VTable; //虚表前置声明
// 定义父类结构
typedef struct potentio_s
{
int Pot_addr;
int Pot_ref_reg;
int Pot_ana_reg;
struct potentio_VTable *vptr; // 虚表指针
}potentio_t;
// 父类中的虚表
struct potentio_VTable{
HAL_StatusTypeDef (*para_set)(potentio_t*, Potentio_Reg_typedef, int, int);
};
.c 文件 .....................................
static HAL_StatusTypeDef _potentio_set_base(potentio_t *this, Potentio_Reg_typedef reg_type, int value, int wipe)
{
printf("Pure virtual function is don't have instance\r\n");
return HAL_OK;
}
void potentio_ctor(potentio_t *this)
{
static struct potentio_VTable vtab = {
_potentio_set_base,
};
this->vptr = &vtab;
this->Pot_addr = 0;
this->Pot_ana_reg = 0;
this->Pot_ref_reg = 0;
}
//外部调用
HAL_StatusTypeDef potentio_set(potentio_t *this, Potentio_Reg_typedef reg_type, int value, int wipe)
{
return (this->vptr->para_set(this, reg_type, value, wipe));
}
派生类:
typedef struct //ad5423_s
{
potentio_t parent; //继承父类
struct potentio_VTable *vptr; //虚表指针
}ad5243_t;
//构造函数
void ad5423_ctor(ad5243_t * this)
{
static struct potentio_VTable vtab = {
_ad5423_set,
};
potentio_ctor(&this->parent); //构造父类对象
this->parent.vptr = &vtab; //重点:将父类的虚表指针重映射指向派生类的虚表,以实现多态性。
this->parent.Pot_addr = 0x5E;
this->parent.Pot_ana_reg = 0x80;
this->parent.Pot_ref_reg = 0x00;
}
==========================
//外部调用
HAL_StatusTypeDef potentio_set(potentio_t *this, Potentio_Reg_typedef reg_type, int value, int wipe)
{
return (this->vptr->para_set(this, reg_type, value, wipe));
}
后面在应用 程序中调用 potentio_set()函数时,就可以根据传入的对像的实际值调用相应的设置函数。
由此,我们可以派生不同的器件,但应用程序不用关心,也不用修改代码。
但是像this->vptr->para_set() 这种操作 ,估计对代码不熟的人找出处都要找半天。一般的IDE也无法直接跳转到para_set()函数。
而这种操作,在LINUX内核中真的是随处可见,代码读起来真的费劲,跳来跳去,不用多久就晕头转向。
目前项目代码应用层的代码大量使用C++编写,维护的确是要方便不少。
|
评分
-
查看全部评分
|