《从控制一盏灯到控制无数盏灯——嵌入式C语言多实例OOP突破 | self指针·方法表·多态》
2026/6/8 7:02:39 网站建设 项目流程

⚠️ 阅读前必看(尤其如果你是刚了解嵌入式的新人)

本文是一篇“思想笔记”,不是一份“可抄作业的工程模板”。

整篇文章的核心目的,是让你理解嵌入式 C 语言封装背后的动机和演进逻辑——为什么要分层、为什么用函数指针、为什么要“继承”。文中的代码都是为说明思想而刻意简化的最小示例(伪代码级别),省略了大量工业级项目里必须考虑的细节,比如:

  • 外设的初始化(时钟、GPIO 配置)
  • 对象的实例化(this 指针、构造函数)
  • 错误处理、参数校验
  • 代码的跨平台与可移植性工程实践

这些“壳子”是为了让你以后省事,但现在故意找麻烦的。
本文只负责把“为什么要找这个麻烦”讲清楚,至于“怎么系统地找这个麻烦(完整实现)”,那是下一阶段的事情。

如果你是新人:请不要误以为这就是 C 语言 OOP 的全部,也不要直接把这些示例代码不加修改地用在真实项目里。


⚠️ 关于函数指针的一点重要提醒(新手必读)

下面介绍的"函数指针封装"是一个很强大的工具,但它不是免费的午餐

什么时候用会很舒服?

  • 你的 MCU 资源比较充裕(比如 STM32、ESP32、ARM Cortex-M 系列,ROM > 32KB,RAM > 4KB)
  • 你需要运行时动态切换硬件实现(比如同时兼容 STM32 和 GD32)
  • 你写的驱动库要给多个项目复用

什么时候要慎重考虑?
你用的是资源极其受限的 MCU,例如:

  • 51 单片机(尤其 ROM < 8KB、RAM < 256B)
  • PIC、AVR 的小容量型号
  • 任何 RAM 只有几百字节、ROM 只有几 KB 的芯片

函数指针会带来两个代价:

  1. 间接调用开销:比直接调用函数慢一点(虽然通常你感觉不到,但在高频中断里可能出问题)
  2. 代码体积增加:函数指针表、函数地址存储会额外占用宝贵的 ROM/RAM

给新人的一句话总结:
如果你刚开始学嵌入式,用的又是 STM32 这种资源充足的芯片,放心用函数指针去理解封装思想。如果你玩到 51 单片机或做极致成本优化时,再回来记住"函数指针有代价"就够了。别因为这个提醒就不敢学,也别以后忘了这个提醒滥用它。

希望这篇笔记能帮你建立起"封装思维",而不是给你一个生硬的模板。


之前的思想篇《为了以后更省事,而现在故意找的麻烦——嵌入式C语言OOP封装思想》主要讲解了继承这个思想。

一、先看:之前的思想篇简化版有什么问题?

伪代码:

// 简化版的操作表typedefstruct{void(*turn_on)(void);// 没有参数!void(*turn_off)(void);// 没有参数!}light_interface_t;// STM32的实现voidstm32_turn_on(void){GPIOA->BSRR=GPIO_BSRR_BS1;// 写死了PA1引脚!}

这个stm32_turn_on函数里,引脚号是写死的!它永远只能点亮 PA1 这一个灯。
如果你想点亮 PA6 的第二个灯怎么办?你得再写一个stm32_turn_on2()函数:

伪代码:

voidstm32_turn_on2(void){GPIOA->BSRR=GPIO_BSRR_BS6;// 又写死了PA6引脚!}

如果你有 100 个灯,你就得写 100 个turn_on函数!这显然是不可接受的。

二、怎么解决这个问题?加个参数!

我们给函数加一个pin_num参数,不就可以控制任意引脚的灯了吗?

伪代码:

// 改进版的操作表typedefstruct{void(*turn_on)(intpin_num);// 加了引脚号参数void(*turn_off)(intpin_num);}light_interface_t;// 现在一个函数就能控制所有灯了!voidstm32_turn_on(intpin_num){GPIO_SetPin(pin_num,1);// 不再写死引脚号!}

但它还有一个小问题:只能传一个参数
如果一个灯不止有引脚号这一个属性呢?比如:

  • 有的灯是高电平点亮,有的是低电平点亮
  • PWM 灯还有亮度属性
  • I2C 灯还有设备地址属性

这时候一个int pin_num参数就不够用了。

三、解决方案:把整个灯对象传进去!

既然一个参数不够用,那我们就把整个灯对象的指针传进去!这样函数就能访问这个灯的所有属性了。
伪代码:

typedefstruct{// 函数接受一个 light_base_t 类型的指针作为参数void(*turn_on)(light_base_t*self);void(*turn_off)(light_base_t*self);}light_base_ops_t;

这个self指针是什么?
它就是"当前这个灯对象自己"的指针。就像你说"我"的时候,指的是你自己一样,这个self指针指的是当前正在被操作的那个灯对象。

四、完整的标准写法三步走

1. 基类:定义通用接口

伪代码:

// ------------------- 第一步:定义方法表 -------------------// 规定:所有灯必须能点亮和熄灭,并且接受一个指向自己的指针作为参数typedefstruct{void(*turn_on)(light_base_t*self);// 点亮"我"这个灯void(*turn_off)(light_base_t*self);// 熄灭"我"这个灯}light_base_ops_t;// ------------------- 第二步:定义基类对象 -------------------// 所有灯的通用部分:只包含一个指向方法表的指针typedefstruct{light_base_ops_t*ops;// 指向"我"这个灯的操作方法表}light_base_t;

伪代码:

2. 派生类:添加私有数据

// ------------------- 第三步:定义具体的灯类型 -------------------// GPIO 灯:除了通用部分,还有自己特有的属性typedefstruct{light_base_tbase;// 先把通用部分放进来(所谓的"继承")int32_tpin_num;// GPIO 灯特有的:引脚号bool active_high;// GPIO 灯特有的:高电平点亮(true)还是低电平点亮(false)}light_gpio_t;

伪代码:

3. 构造函数:初始化对象

// ------------------- 第四步:写具体的实现函数 -------------------// GPIO 灯的点亮函数staticvoidlight_gpio_turn_on(light_base_t*self){// 这里的 self 是基类指针,我们把它转回原来的 light_gpio_t 指针// 因为我们知道它本来就是一个 light_gpio_tlight_gpio_t*gpio_self=(light_gpio_t*)self;// 现在我们可以访问这个灯的所有私有属性了!if(gpio_self->active_high){GPIO_SetPin(gpio_self->pin_num,1);// 高电平点亮}else{GPIO_SetPin(gpio_self->pin_num,0);// 低电平点亮}}// GPIO 灯的熄灭函数staticvoidlight_gpio_turn_off(light_base_t*self){light_gpio_t*gpio_self=(light_gpio_t*)self;GPIO_SetPin(gpio_self->pin_num,!gpio_self->active_high);}// ------------------- 第五步:创建全局的方法表 -------------------// 所有 GPIO 灯共享这一个方法表!staticconstlight_base_ops_tgpio_light_ops={.turn_on=light_gpio_turn_on,.turn_off=light_gpio_turn_off,};// ------------------- 第六步:构造函数 -------------------// 初始化一个 GPIO 灯对象voidlight_gpio_init(light_gpio_t*self,// 要初始化的灯对象constchar*pin_name,// 引脚名bool active_high// 点亮电平:true = 高电平亮,false = 低电平亮){// 初始化这个灯自己特有的属性self->pin_num=GPIO_GetPinByName(pin_name);self->active_high=active_high;// 让这个灯的方法表指针指向全局的 GPIO 灯方法表self->base.ops=&gpio_light_ops;}

五、见证奇迹:现在我们可以创建任意多个灯了!

伪代码:

intmain(void){// 创建第一个灯:PA1,高电平点亮light_gpio_tred_light;light_gpio_init(&red_light,"PA1",true);// 创建第二个灯:PA6,低电平点亮light_gpio_tgreen_light;light_gpio_init(&green_light,"PA6",false);// 创建第三个灯:PB0,高电平点亮light_gpio_tblue_light;light_gpio_init(&blue_light,"PB0",true);while(1){// 点亮红色灯red_light.base.ops->turn_on((light_base_t*)&red_light);delay_ms(500);// 熄灭红色灯red_light.base.ops->turn_off((light_base_t*)&red_light);// 点亮绿色灯green_light.base.ops->turn_on((light_base_t*)&green_light);delay_ms(500);// 熄灭绿色灯green_light.base.ops->turn_off((light_base_t*)&green_light);// 点亮蓝色灯blue_light.base.ops->turn_on((light_base_t*)&blue_light);delay_ms(500);// 熄灭蓝色灯blue_light.base.ops->turn_off((light_base_t*)&blue_light);}}

看到了吗?我们只写了一个light_gpio_turn_on函数,就可以控制任意多个 GPIO 灯!每个灯都有自己的引脚号和点亮电平,互不干扰。

六、最神奇的地方:通用函数可以操作任何类型的灯

现在我们写一个通用的灯闪烁函数:
伪代码:

// 这个函数可以闪烁任何类型的灯!// 不管是 GPIO 的、PWM 的还是 I2C 的,它都能工作!voidlight_blink(light_base_t*light,intdelay_ms){light->ops->turn_on(light);delay_ms(delay_ms);light->ops->turn_off(light);delay_ms(delay_ms);}

现在我们用这个通用函数来闪烁刚才的三个灯:
伪代码:

intmain(void){light_gpio_tred_light,green_light,blue_light;light_gpio_init(&red_light,"PA5",true);light_gpio_init(&green_light,"PA6",false);light_gpio_init(&blue_light,"PB0",true);while(1){light_blink((light_base_t*)&red_light,500);// 闪烁红色灯light_blink((light_base_t*)&green_light,500);// 闪烁绿色灯light_blink((light_base_t*)&blue_light,500);// 闪烁蓝色灯}}

七、现在我们来加一个 PWM 调光灯,看看有多简单

伪代码:

// ------------------- PWM 灯的定义 -------------------typedefstruct{light_base_tbase;// 继承通用部分int32_tpwm_channel;// PWM 灯特有的:通道号uint8_tbrightness;// PWM 灯特有的:亮度(0~255)}light_pwm_t;// ------------------- PWM 灯的实现 -------------------staticvoidlight_pwm_turn_on(light_base_t*self){light_pwm_t*pwm_self=(light_pwm_t*)self;PWM_SetDutyCycle(pwm_self->pwm_channel,pwm_self->brightness);}staticvoidlight_pwm_turn_off(light_base_t*self){light_pwm_t*pwm_self=(light_pwm_t*)self;PWM_SetDutyCycle(pwm_self->pwm_channel,0);}// ------------------- PWM 灯的方法表 -------------------staticconstlight_base_ops_tpwm_light_ops={.turn_on=light_pwm_turn_on,.turn_off=light_pwm_turn_off,};// ------------------- PWM 灯的构造函数 -------------------voidlight_pwm_init(light_pwm_t*self,constchar*pwm_name,uint8_tbrightness){self->pwm_channel=PWM_GetChannelByName(pwm_name);self->brightness=brightness;self->base.ops=&pwm_light_ops;}

现在我们可以用同一个light_blink函数来闪烁 PWM 灯了!
伪代码:

intmain(void){// 创建一个 GPIO 灯light_gpio_tred_light;light_gpio_init(&red_light,"PA5",true);// 创建一个 PWM 灯(亮度 50%)light_pwm_tblue_light;light_pwm_init(&blue_light,"PA6",128);while(1){// 用同一个函数闪烁不同类型的灯!light_blink((light_base_t*)&red_light,500);light_blink((light_base_t*)&blue_light,500);}}

这个设计的灵魂就是那个light_base_t *self参数。它让函数可以:

  1. 访问当前对象的所有私有数据
  2. 不需要知道对象的具体类型
  3. 同一个函数可以操作任意多个同类型的对象

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询