有一种说法是c语言是一门面向过程的语言,其实这种说法是比较浅显的,面向对象是一种编程设计思想,并不是各个编程语言的属性差异。在语法上,C语言支持的oop(面向对象)机制比较薄弱,但完全可以使用c语言写出面向对象的程序,只不过很多细节没有语法支持,需要编程人自己去实现。实际上编程实现机制的方式也并不只有提高工作量和门槛的弊端,它可以是更加灵活的。
这里使用的实例是一个c版本的面向对象框架,是根据RT-Thread 的rt-robot软件包(c语言)简化,旨在总结c语言的oop。
RT-Thread的软件包地址:https://github.com/sogwms/rt-robot
此用例(简化版)地址:https://github.com/AntigravityC/rt-robot-to-cpp
的c-version分支
我改写的C++版本的 rt-robot 框架程序地址:上述地址的main分支
我们可以从这两者的对比中了解c语言如何实现面向对象编程
首先从应用的角度介绍一下这个RT-Thread的rt-robot框架,这是我的一个朋友向rt-thread贡献的开源项目,可以使用此框架对智能车、无人机等机器人模型进行对象创建及拼接版的简单编程。例如:一台具有闭环控制功能的智能车的驱动部分由几个轮子组成、轮子由电机和编码器组成、电机提供输出、编码器提供反馈,再加上指令和pid等控制器算法就可以形成一个简单却经典的闭环控制系统。
在用c实现这样一个程序时,我们当然可以书写几个电机旋转、编码器读取、控制算法计算等各个模块功能的函数,将信息在它们之间传递,从而实现对应的功能。但是使用面向对象的方式去构建它可以更加具有更好的模块化、低耦合高内聚、高复用性、易维护性和更加灵活。试想我们的小车可能在某一天需要加装一个电机、或者需要换掉所有的编码器,在面向过程的程序中可能牵一发而动全身,而在将这些传感器和执行器作为对象进行构建的程序中做更改将是一件方便并且优雅的事情。
以电机为例:继承是面向对象的要素,用c++的话我们实现基类 motor,实现派生类:各种类型的电机(双 PWM、单 PWM 的直流电机),如双pwm直流电机dual_motor来继承motor,并添加具体直流电机旋转的功能的方法。编码器也是如此:如使用AB 相编码器来继承编码器基类。
这样使用此派生类创建对象的时候就创造了一个具有功能的小车轮子驱动。最后根据小车上器件的数量进行对象的创建和车子模型的组合。
下文将从程序是实现的角度进行对比分析
注:只展示部分核心程序,如需完整工程请跟进前文提示到github下载
基类是已存在的用来派生新类的类为父类,使用基类我们可以实现通用、复用性接口。
//.hpp
class motor
{
public:
motor();
~motor();
virtual bool set_speed(int thousands) = 0;
virtual bool motor_run(int thousands) = 0;
};
//.cpp
motor::motor()
{}
motor::~motor()
{}
这里set_speed和motor_run是纯虚函数,因为这个motor类是抽象基类,创建它是为了提供一个通用性的接口
因为C语言没有继承机制,所以实现基类接口需要如下准备工作
//.h
typedef struct motor
{
bool(*set_speed)(void *mot, int thousands);
bool (*destroy)(void *mot);
}motor_t;
motor_t *motor_create(uint8_t size);
bool motor_destroy(motor_t *mot);
bool motor_run(motor_t *mot, int thousands);
1.空间创建不能像c++直接使用new,而是使用内存申请并层层调用
motor_t *motor_create(uint8_t size)
{
motor_t*new_motor = (motor_t*)malloc(size);
if (new_motor == NULL)
{
printf("Falied to allocate memory for new motor\n");
return NULL;
}
return new_motor;
}
bool motor_destroy(motor_t *mot)
{
assert(mot != NULL);
printf("free motor");
free(mot);
return true;
}
2.通过motor_t *mot这种形参来将创建的对象指针传递进来作为动作主体(主语),并且动作操作(谓语,这里是set_speed)也需要在后续用指针指定方式来继承,以此完成通用接口的实现。
bool motor_run(motor_t *mot, int thousands)
{
assert(mot != NULL);
mot->set_speed(mot, thousands);
return true;
}
派生类是利用继承机制,从已有的类中派生的类。
直流电机dual_pwm_motor继承基类 motor,并将纯虚函数进行实现,相对抽象基类、这里派生类要具有具体的对象的功能操作函数:set_speed里的pwm_set,可理解为硬件层面的使电机旋转的功能操作函数。
//.hpp
class dual_pwm_motor : public motor
{
public:
dual_pwm_motor();
~dual_pwm_motor();
char *dev;
bool set_speed(int thousands);
bool motor_run(int thousands);
};
//.cpp
bool dual_pwm_motor::set_speed(int thousands)
{
if (thousands == 0)
{
pwm_set(thousands); //do something to make motor run
}
printf("dual_pwm_motor_set_speed %d\n",thousands);
return true;
}
bool dual_pwm_motor::motor_run(int thousands)
{
set_speed(thousands);
return true;
}
//.h
typedef struct dual_pwm_motor
{
struct motor mot;
char *dev;
}dual_pwm_motor_t;
dual_pwm_motor_t *dual_pwm_motor_create(char *pwm);
如前文所说,c实现的派生类的空间创建需要层层调用、而通用接口里的操作方法应该用指针指向进行继承,即new_motor->mot.set_speed = dual_pwm_motor_set_speed;
//.c
static bool dual_pwm_motor_set_speed(void *mot, int thousands)
{
dual_pwm_motor_t* mot_sub = (dual_pwm_motor_t*)mot;
if (thousands == 0)
{
//pwm_set(thousands);//do something to make motor run
}
printf("dual_pwm_motor_set_speed %d\n",thousands);
return true;
}
dual_pwm_motor_t *dual_pwm_motor_create(char *dev)
{
dual_pwm_motor_t *new_motor = (dual_pwm_motor_t*)motor_create(sizeof(struct dual_pwm_motor));
if (new_motor == NULL)
{
return NULL;
}
new_motor->dev = dev;
new_motor->mot.set_speed = dual_pwm_motor_set_speed;
return new_motor;
}
前文介绍一个轮子由编码器和电机组成,根据我们前文的具体到型号的电机和编码器等类可以实现派生类wheels,是一个组合可以表现这个闭环系统的类,我们可以使用它进行最终的对象创建和组合。
传入目标速度,读取编码器值得到现在的速度,根据两者差值进行控制计算,最后输出,这就是这个最终类的表现。
//.hpp
class wheel : public dual_pwm_motor,public ab_phase_encoder
{
public:
wheel();
~wheel();
int target;
int measure;
int output;
bool set_target(int speed);
void controller_update(int target,int measure,int *output);
void update();
};
//.cpp
bool wheel::set_target(int speed)
{
target = speed;
return true;
}
#define KP 1
void wheel::controller_update(int target,int measure,int *output) //这里实现了一个简单的比例P控制
{
// do somethig to control
int err = target - measure;
*output = KP * err;
}
void wheel::update(void)
{
measure = read();
controller_update(target, measure,&output);
motor_run(output);
}
//.h
typedef struct wheel
{
motor_t *w_motor;
encoder_t *w_encoder;
int target;
int measure;
int output;
}wheel_t;
wheel_t *wheel_create(motor_t *w_motor, encoder_t *w_encoder);
bool wheel_destroy(wheel_t *whl);
void wheel_update(wheel_t *whl);
bool wheel_set_speed(wheel_t *whl, int speed);
//.c
wheel_t *wheel_create(motor_t *w_motor, encoder_t *w_encoder)
{
// 1. Malloc memory for wheel
wheel_t *new_wheel = (wheel_t*)malloc(sizeof(struct wheel));
if (new_wheel == NULL)
{
printf("Falied to allocate memory for new wheel");
return NULL;
}
// 2. Initialize wheel
new_wheel->w_motor = w_motor;
new_wheel->w_encoder = w_encoder;
return new_wheel;
}
bool wheel_destroy(wheel_t *whl)
{
assert(whl != NULL);
printf("Free wheel");
motor_destroy(whl->w_motor);
encoder_destroy(whl->w_encoder);
free(whl);
return true;
}
bool wheel_set_speed(wheel_t *whl, int speed)
{
assert(whl != NULL);
whl->target = speed;
return true;
}
#define KP 1
void wheel_controller_update(int target,int measure,int *output)
{
// do somethig to control
int err = target - measure;
*output = KP * err;
}
void wheel_update(wheel_t *whl)
{
assert(whl != NULL);
whl->measure = encoder_read(whl->w_encoder);
wheel_controller_update(whl->target, whl->measure,&whl->output);
motor_run(whl->w_motor, whl->output);
}
int main(int argc,char *argv[])
{
wheel *left_wheel = new wheel();
wheel *right_wheel = new wheel();
// cout << "main begin\n" << endl;
printf("main begin\n");
while(1)
{
left_wheel->set_target(ENCODE_TARGET);
right_wheel->set_target(ENCODE_TARGET*2);
left_wheel->update();
right_wheel->update();
printf("left %d %d %d\n",left_wheel->target,left_wheel->measure,left_wheel->output);
//printf("right %d %d %d\n",right_wheel->target,right_wheel->measure,right_wheel->output);
usleep(100000);
}
while(1);
}
最后是c语言实现创建对象并使用,方式是创建具体型号的对象,如电机和编码器,将其“组装”到轮子上
int main(int argc,char *argv[])
{
wheel_t** c_wheels = (wheel_t**)malloc(sizeof(wheel_t*) * 2);
if (c_wheels == NULL)
{
printf("Failed to malloc memory for wheels");
return -1;
}
dual_pwm_motor_t *left_motor = dual_pwm_motor_create(NULL);
ab_phase_encoder_t *left_encoder = ab_phase_encoder_create(NULL,0);
c_wheels[0] = wheel_create((motor_t*)left_motor, (encoder_t*)left_encoder);
// ...
while(1)
{
wheel_set_speed(c_wheels[0] ,100);
wheel_update(c_wheels[0] );
printf("%d %d %d\n",c_wheels[0]->target,c_wheels[0]->measure,c_wheels[0]->output);
sleep(1);
}
return 0;
}
总结:C实现的面向对象框架,有两点特性需要注意,实现效果是创建时使用具体的模拟类的结构体如dual_motor,而使用时则是不分型号的wheel_update,也就是通用接口。
注:这个例子是智能车框架,这里最上层一步却只到轮子,看起来有些奇怪,实际上RT-Thread的完整的c语言的机器人框架的最上层是到car的,还有几种不同的型号器件、算法选择等功能、这里是以最简形式举例的,所以只有简化版的驱动部分。
最后感谢开源,让我们共同学习,共同进步。