《从控制一盏灯到控制无数盏灯——嵌入式C语言多实例OOP突破 | self指针·方法表·多态》
⚠️ 阅读前必看(尤其如果你是刚了解嵌入式的新人)
本文是一篇“思想笔记”,不是一份“可抄作业的工程模板”。
整篇文章的核心目的,是让你理解嵌入式 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 的芯片
函数指针会带来两个代价:
- 间接调用开销:比直接调用函数慢一点(虽然通常你感觉不到,但在高频中断里可能出问题)
- 代码体积增加:函数指针表、函数地址存储会额外占用宝贵的 ROM/RAM
给新人的一句话总结:
如果你刚开始学嵌入式,用的又是 STM32 这种资源充足的芯片,放心用函数指针去理解封装思想。如果你玩到 51 单片机或做极致成本优化时,再回来记住"函数指针有代价"就够了。别因为这个提醒就不敢学,也别以后忘了这个提醒滥用它。希望这篇笔记能帮你建立起"封装思维",而不是给你一个生硬的模板。
之前的思想篇《为了以后更省事,而现在故意找的麻烦——嵌入式C语言OOP封装思想》主要讲解了继承这个思想。
一、先看:之前的思想篇简化版有什么问题?
伪代码:
// 简化版的操作表
typedef struct {
void (*turn_on)(void); // 没有参数!
void (*turn_off)(void); // 没有参数!
} light_interface_t;
// STM32的实现
void stm32_turn_on(void) {
GPIOA->BSRR = GPIO_BSRR_BS1; // 写死了PA1引脚!
}
这个 stm32_turn_on 函数里,引脚号是写死的! 它永远只能点亮 PA1 这一个灯。
如果你想点亮 PA6 的第二个灯怎么办?你得再写一个 stm32_turn_on2() 函数:
伪代码:
void stm32_turn_on2(void) {
GPIOA->BSRR = GPIO_BSRR_BS6; // 又写死了PA6引脚!
}
如果你有 100 个灯,你就得写 100 个 turn_on 函数!这显然是不可接受的。
二、怎么解决这个问题?加个参数!
我们给函数加一个 pin_num 参数,不就可以控制任意引脚的灯了吗?
伪代码:
// 改进版的操作表
typedef struct {
void (*turn_on)(int pin_num); // 加了引脚号参数
void (*turn_off)(int pin_num);
} light_interface_t;
// 现在一个函数就能控制所有灯了!
void stm32_turn_on(int pin_num) {
GPIO_SetPin(pin_num, 1); // 不再写死引脚号!
}
但它还有一个小问题:只能传一个参数。
如果一个灯不止有引脚号这一个属性呢?比如:
- 有的灯是高电平点亮,有的是低电平点亮
- PWM 灯还有亮度属性
- I2C 灯还有设备地址属性
这时候一个 int pin_num 参数就不够用了。
三、解决方案:把整个灯对象传进去!
既然一个参数不够用,那我们就把整个灯对象的指针传进去!这样函数就能访问这个灯的所有属性了。
伪代码:
typedef struct {
// 函数接受一个 light_base_t 类型的指针作为参数
void (*turn_on)(light_base_t *self);
void (*turn_off)(light_base_t *self);
} light_base_ops_t;
这个 self 指针是什么?
它就是"当前这个灯对象自己"的指针。就像你说"我"的时候,指的是你自己一样,这个 self 指针指的是当前正在被操作的那个灯对象。
四、完整的标准写法三步走
1. 基类:定义通用接口
伪代码:
// ------------------- 第一步:定义方法表 -------------------
// 规定:所有灯必须能点亮和熄灭,并且接受一个指向自己的指针作为参数
typedef struct {
void (*turn_on)(light_base_t *self); // 点亮"我"这个灯
void (*turn_off)(light_base_t *self); // 熄灭"我"这个灯
} light_base_ops_t;
// ------------------- 第二步:定义基类对象 -------------------
// 所有灯的通用部分:只包含一个指向方法表的指针
typedef struct {
light_base_ops_t *ops; // 指向"我"这个灯的操作方法表
} light_base_t;
伪代码:
2. 派生类:添加私有数据
// ------------------- 第三步:定义具体的灯类型 -------------------
// GPIO 灯:除了通用部分,还有自己特有的属性
typedef struct {
light_base_t base; // 先把通用部分放进来(所谓的"继承")
int32_t pin_num; // GPIO 灯特有的:引脚号
bool active_high; // GPIO 灯特有的:高电平点亮(true)还是低电平点亮(false)
} light_gpio_t;
伪代码:
3. 构造函数:初始化对象
// ------------------- 第四步:写具体的实现函数 -------------------
// GPIO 灯的点亮函数
static void light_gpio_turn_on(light_base_t *self) {
// 这里的 self 是基类指针,我们把它转回原来的 light_gpio_t 指针
// 因为我们知道它本来就是一个 light_gpio_t
light_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 灯的熄灭函数
static void light_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 灯共享这一个方法表!
static const light_base_ops_t gpio_light_ops = {
.turn_on = light_gpio_turn_on,
.turn_off = light_gpio_turn_off,
};
// ------------------- 第六步:构造函数 -------------------
// 初始化一个 GPIO 灯对象
void light_gpio_init(
light_gpio_t *self, // 要初始化的灯对象
const char *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;
}
五、见证奇迹:现在我们可以创建任意多个灯了!
伪代码:
int main(void) {
// 创建第一个灯:PA1,高电平点亮
light_gpio_t red_light;
light_gpio_init(&red_light, "PA1", true);
// 创建第二个灯:PA6,低电平点亮
light_gpio_t green_light;
light_gpio_init(&green_light, "PA6", false);
// 创建第三个灯:PB0,高电平点亮
light_gpio_t blue_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 的,它都能工作!
void light_blink(light_base_t *light, int delay_ms) {
light->ops->turn_on(light);
delay_ms(delay_ms);
light->ops->turn_off(light);
delay_ms(delay_ms);
}
现在我们用这个通用函数来闪烁刚才的三个灯:
伪代码:
int main(void) {
light_gpio_t red_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 灯的定义 -------------------
typedef struct {
light_base_t base; // 继承通用部分
int32_t pwm_channel; // PWM 灯特有的:通道号
uint8_t brightness; // PWM 灯特有的:亮度(0~255)
} light_pwm_t;
// ------------------- PWM 灯的实现 -------------------
static void light_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);
}
static void light_pwm_turn_off(light_base_t *self) {
light_pwm_t *pwm_self = (light_pwm_t *)self;
PWM_SetDutyCycle(pwm_self->pwm_channel, 0);
}
// ------------------- PWM 灯的方法表 -------------------
static const light_base_ops_t pwm_light_ops = {
.turn_on = light_pwm_turn_on,
.turn_off = light_pwm_turn_off,
};
// ------------------- PWM 灯的构造函数 -------------------
void light_pwm_init(light_pwm_t *self, const char *pwm_name, uint8_t brightness) {
self->pwm_channel = PWM_GetChannelByName(pwm_name);
self->brightness = brightness;
self->base.ops = &pwm_light_ops;
}
现在我们可以用同一个 light_blink 函数来闪烁 PWM 灯了!
伪代码:
int main(void) {
// 创建一个 GPIO 灯
light_gpio_t red_light;
light_gpio_init(&red_light, "PA5", true);
// 创建一个 PWM 灯(亮度 50%)
light_pwm_t blue_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 参数。它让函数可以:
- 访问当前对象的所有私有数据
- 不需要知道对象的具体类型
- 同一个函数可以操作任意多个同类型的对象
更多推荐


所有评论(0)