[一点五编程]--对于嵌入式开发中 使用面向对象的理念 通过C语言中的结构体和函数指针实现C++三大特性

“一点五编程”–以函数指针override重写 以结构体作为传参 实现多态

将应用逻辑层和硬件层通过构建接口层来实现隔离 使代码不拘于硬件、平台,只面向功能,以获得代码的复用性、移植性 且增加了代码的易读性

io_led.light_i.open(struct led* self,int flags) 由于self结构体内参数不同 实现多态效果

面向对象

将任务经过面向对象进行抽象,比如将led灯、pwm灯、rgb灯抽象为 “灯”

在"灯"中有着统一的硬件层接口 如开灯open 关灯close.应用逻辑层上 无论是点亮led灯还是pwm灯 统一调用"灯"接口中的open函数

open函数会根据传参的不同来override重写接口函数 得到C++中多态的效果

函数指针

typedef void (*p)(int args);
typedef int (*painter_draw_point_fn_t)(void *self, struct point pos, int color);

通过typedef定义一个函数指针,该函数指针指向的是某一个抽象出来的功能,如open开灯 close关灯

结构体

struct p_i
{
fn_t
}

struct painter_i {
painter_draw_point_fn_t draw_point;
};

自顶向下 丢掉嵌入式开发自底向上的刻板思维 或者理解为当前功能已经实现,只需设计应用逻辑

先设计应用层 一路往下设计 直到驱动层
比如我们项目要求是led闪烁 pwm灯随着每一次led闪烁渐明渐暗以实现呼吸灯
可以先写出应用层逻辑

io_led_open();
pwm_led_open();
delay(100);
io_led_close();
pwm_led_close();
delay(100);

上面即为应用层逻辑代码 以往的嵌入式开发思维 会认为这是两个不同的模块 其根据是通过硬件功能实现决定的

抛弃刻板思维 通过功能属性来进行模块化编程! 将io_led和pwm_led根据底层功能划分为同一个模块–“灯”
那么如何设计"灯"的接口呢 秘诀在于–函数指针和结构体

通过函数指针定义接口函数

在灯的设计中 要用到的功能有开灯(led_open) 关灯(led_close)
所以typedef函数指针

typedef void (*led_open_fn_t)(void *self, struct point pos, int color);

模块化编程

传统的嵌入式开发流程:只对功能划分 如led灯 pwm灯 oled显示 超声波模块 蓝牙模块等进行分文件分模块的设计

一点五编程:不仅对功能进行划分 同时对于具有相同抽象属性的功能进行接口设计
在应用层设计中没有iic等硬件接口的概念 只关注对象接口例如灯、传感器、显示器

        如灯类功能(核心操作为开灯、关灯) 显示类功能(核心功能为打点函数)
        虽然io_led和pwm_led实际驱动的方式不同 但可以通过设计相同的软件接口 通过override重写接口函数来适配硬件接口
        以实现软件层面的设计统一 代码更能体现功能的特性 以及更好的可读性和修改性

通过以功能属性抽象划分的接口设计 在软件代码逻辑不变 硬件改变的情况下 只需要修改接口函数中的硬件接口实现即可

如果在当前代码基础上增加一些具有相同功能 但硬件接口的不同的模块时 比如iic_led通过iic来发送数据以控制led亮灭的功能 也能快速适配到代码

流程

接口实现(灯为例)


typedef int (*light_open_fn_t)(void *self);
typedef int (*light_close_fn_t)(void *self);

struct light_i {
    light_open_fn_t light_open;
    light_close_fn_t light_close;
};
将硬件进行抽象处理,提取需要操作的接口
比如led抽象为灯,对于灯只需要开启和关闭两种操作
还有无刷电机、伺服电机等,虽然控制方法不一致,但是都可以抽象为电机,也是开启和关闭两个操作
然后根据实际电机的操控方式去重写接口函数即可

对象实现(led为例)

创建led.c/.h

1、在led.h中 创建对象结构体

struct io_led
{
    struct light* interface;
    const char* name;
    int status;
};
第一个属性为接口interface,其余属性任意设定

2、在led.c中
创建实际的接口结构体

int io_led_open(struct io_led *self);
int io_led_close(struct io_led *self);

struct light_i io_led_interface =
        {
                .light_open = (light_open_fn_t) io_led_open,
                .light_close = (light_close_fn_t) io_led_close
        };
};
编写时 只有最后的io_led_open、io_led_close视情况自己定,其余内容均和接口文件内的结构体一致,可直接复制
其格式为如下
struct 接口结构体类型 结构体名称 = {
        .接口函数 = (接口函数强制转换)实际函数,
        .接口函数 = (接口函数强制转换)实际函数,
};
需要注意的点
1 逗号隔开
2 强制转换

3、实际的接口函数

int io_led_open(struct io_led *self);
int io_led_close(struct io_led *self);

int io_led_open(struct io_led *self) {
    // TODO: Implement open logic for the LED
    self->status = 1;
    printf("openLED: %s status = %d\n",self->name,self->status);
    return 0;
}

int io_led_close(struct io_led *self) {
    // TODO: Implement close logic for the LED
    self->status = 0;
    printf("closeLED: %s status = %d\n",self->name,self->status);
    return 0;
}
传入的参数是定义的实际对象结构体 通过结构体传入参数 来override接口函数

4、初始化函数

int io_led_init(struct io_led *self, const char *name) {

    self->interface = &io_led_interface;
    self->status = 0;
    self->name = name;
    // Initialize LED hardware (e.g., GPIO setup)

    return 0;
}
这里并未按照一点五编程中进行封装(*p)->f(p)
初始化函数没有特定格式 自己随意编写就行

4、main初始化及调用

struct io_led light1;
io_led_init(&light1, "light1");
在main函数中创建一个实例 然后调用初始化函数进行初始化
    printf("test light1\n");
    light1.interface->light_open(&light1);
    light1.interface->light_close(&light1);
实际调用如上

为什么第一个字段必须是接口结构体

当你写下 p_light2 = (struct light_i **)light2; 时,你将 light2(一个 struct io_led * 类型的指针)强制转换为 struct light_i ** 类型的指针。这种强制转换要求 struct io_led 结构体的起始部分必须与 struct light_i 的内存布局一致,以确保通过 p_light2 访问接口时的行为是正确的。

理解结构体指针转换

在 C 语言中,结构体的内存布局是顺序的,即结构体成员在内存中是按声明顺序存储的。当你把 struct io_led * 转换为 struct light_i ** 时,编译器认为 light2 所指向的内存开始部分是一个 struct light_i 类型的对象。

例子

假设你有如下两个结构体:

struct light_i {
    void (*light_open)(struct light_i *self);
    void (*light_close)(struct light_i *self);
};

struct io_led {
    struct light_i interface;
    int status;
    const char *name;
};

在这个例子中:

  • struct io_led 的第一个成员是 struct light_i interface;,所以 struct io_led 的起始部分和 struct light_i 的内存布局是一致的。
  • 当你将 light2 强制转换为 struct light_i ** 时,编译器认为 light2 所指向的内存地址包含一个 struct light_i 对象,你可以通过 p_light2 来访问这些函数指针。

为什么起始部分需要是 struct light_i

  1. 内存布局的一致性:如果 struct io_led 的第一个成员不是 struct light_i,那么将 light2 转换为 struct light_i ** 后,访问 (*p_light2)->light_open 会读取错误的内存区域,导致未定义行为。

  2. 指针解引用的正确性:当你通过 p_light2 解引用指针访问 light_openlight_close 函数时,C 语言会根据 struct light_i 的布局解读 light2 的内容。如果 light2 的起始部分与 struct light_i 不匹配,函数指针可能会指向错误的地址,导致程序崩溃或行为异常。

简单示例

考虑以下内存布局:

// 定义两个结构体
struct light_i {
    void (*light_open)(struct light_i *self);
    void (*light_close)(struct light_i *self);
};

struct io_led {
    struct light_i interface;  // 必须位于结构体的开始位置
    int status;
    const char *name;
};

// 创建一个 io_led 实例
struct io_led light;

// 假设 light2 是指向这个 io_led 实例的指针
struct io_led *light2 = &light;

// 强制转换为 light_i ** 类型
struct light_i **p_light2 = (struct light_i **)light2;

// 现在 p_light2 指向的区域可以安全地被解释为 struct light_i
(*p_light2)->light_open(*p_light2);

如果 struct io_led 的起始部分是 struct light_i,那么 p_light2 可以被安全地用来访问 light_openlight_close 函数指针。

总结

  • 当你把 struct io_led * 转换为 struct light_i ** 时,你是在告诉编译器,从 light2 指向的内存起始位置开始,有一个 struct light_i 类型的对象。
  • 如果 struct io_led 的第一个成员确实是 struct light_i interface;,那么这种转换是安全的,因为内存布局保证了这一点。
  • 如果 struct io_led 的起始部分不是 struct light_i,这种转换将导致访问错误内存区域,从而引发未定义行为。

通过这种设计,你可以使用通用的接口(struct light_i)来操作不同类型的灯对象(struct io_led),从而实现多态性和接口封装。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

注意沈题!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值