《为了以后更省事,而现在故意找的麻烦——嵌入式C语言OOP封装思想 | 分层·函数指针·继承》
⚠️ 阅读前必看(尤其如果你是刚了解嵌入式的新人)
本文是一篇“思想笔记”,不是一份“可抄作业的工程模板”。
整篇文章的核心目的,是让你理解嵌入式 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 单片机或做极致成本优化时,再回来记住"函数指针有代价"就够了。别因为这个提醒就不敢学,也别以后忘了这个提醒滥用它。希望这篇笔记能帮你建立起"封装思维",而不是给你一个生硬的模板。
面向对象相当于套壳子
这些壳子全都是 “为了以后更省事,而现在故意找的麻烦”。
一、先看:不套任何壳子的代码是什么样的
伪代码:
// 直接操作寄存器点亮 LED
void light_on(void) { GPIOA->BSRR = GPIO_BSRR_BS1; }
void light_off(void) { GPIOA->BSRR = GPIO_BSRR_BR1; }
int main(void)
{
while(1)
{
light_on();
delay_ms(500);
light_off();
delay_ms(500);
}
}
优点:简单、直接、一眼就能看懂
缺点:换芯片的时候,非常麻烦
二、现在换成 GD32 芯片或者其他芯片呢?
你要做什么?
- 把
GPIOA->BSRR改成 GD32 的GPIOA->BOP - 把
GPIO_BSRR_BS1改成GPIO_BOP_BOP1 - 把
GPIO_BSRR_BR1改成GPIO_BOP_BR1
如果只有这两个函数,改起来很简单对吧?
但如果你的程序里有 100 个这样的函数呢?
- UART 发送、UART 接收
- SPI 读写、I2C 读写
- ADC 采样、PWM 输出
- 定时器中断、外部中断
你要改 100 个地方!而且很容易漏改,漏改一个就是一个 bug。
三、第一层壳子:把硬件相关的代码单独放一个文件
这是第一个最简单的壳子,也是 90% 的项目都应该用的,没有任何复杂概念:
第一步:创建 light_drv.h 头文件
伪代码:
// light_drv.h
#ifndef __LIGHT_DRV_H__
#define __LIGHT_DRV_H__
void light_on(void);
void light_off(void);
#endif
第二步:创建 light_drv_stm32.c 源文件
伪代码:
// light_drv_stm32.c
#include "light_drv.h"
#include "stm32f10x.h"
void light_on(void) { GPIOA->BSRR = GPIO_BSRR_BS1; }
void light_off(void) { GPIOA->BSRR = GPIO_BSRR_BR1; }
第三步:主函数只包含头文件,直接调用
伪代码:
// main.c
#include "light_drv.h"
int main(void)
{
while(1)
{
light_on();
delay_ms(500);
light_off();
delay_ms(500);
}
}
现在换 GD32 芯片,你只需要做一件事:
- 删除
light_drv_stm32.c - 新建一个
light_drv_gd32.c,里面写 GD32 的实现 - 把
light_drv_gd32.c加到工程里
main.c 一行都不用改!
这就是第一层壳子的价值:把硬件相关的代码和上层应用代码分开。
四、第二层壳子:函数指针(解决“同时支持多个硬件”的问题)
刚才的第一层壳子有一个小问题:同一时间只能支持一个硬件。你不能在同一程序里同时实现 STM32 的 LED 和 GD32 的 LED。
什么时候会遇到这种情况?
- 你做的产品有两个版本:标准版用 STM32,低配版用 GD32
- 你做的开发板要兼容多种芯片
- 你写的驱动库要给多个项目用
这时候就需要第二层壳子:函数指针。
伪代码(头文件):
// light_drv.h
#ifndef __LIGHT_DRV_H__
#define __LIGHT_DRV_H__
// 定义一个操作接口表
typedef struct {
void (*turn_on)(void);
void (*turn_off)(void);
} light_interface_t;
// 声明不同硬件的接口实例
extern const light_interface_t stm32_light_ops;
extern const light_interface_t gd32_light_ops;
#endif
伪代码(STM32 实现):
// light_drv_stm32.c
#include "light_drv.h"
#include "stm32f10x.h"
static void stm32_turn_on(void) { GPIOA->BSRR = GPIO_BSRR_BS1; }
static void stm32_turn_off(void) { GPIOA->BSRR = GPIO_BSRR_BR1; }
const light_interface_t stm32_light_ops = {
.turn_on = stm32_turn_on,
.turn_off = stm32_turn_off,
};
伪代码(GD32 实现):
// light_drv_gd32.c
#include "light_drv.h"
#include "gd32f10x.h"
static void gd32_turn_on(void) { GPIOA->BOP = GPIO_BOP_BOP1; }
static void gd32_turn_off(void) { GPIOA->BOP = GPIO_BOP_BR1; }
const light_interface_t gd32_light_ops = {
.turn_on = gd32_turn_on,
.turn_off = gd32_turn_off,
};
现在主函数怎么用?
伪代码(主函数):
// main.c
#include "light_drv.h"
// 运行时决定用哪一套硬件操作
const light_interface_t *light_iface;
int main(void)
{
light_iface = &stm32_light_ops;
// light_iface = &gd32_light_ops;
while(1)
{
light_iface->turn_on();
delay_ms(500);
light_iface->turn_off();
delay_ms(500);
}
}
看到了吗? 现在可以在同一个程序里同时支持 STM32 和 GD32,只需要改一行初始化代码。
这就是第二层壳子的价值:在运行时动态切换硬件实现。
五、第三层壳子:继承(解决“多个设备有相同功能”的问题)
刚才的第二层壳子还有一个小问题:每个设备都要重新写一遍相同的代码。
比如你有两个灯:一个是普通的 GPIO 灯,一个是可以调光的 PWM 灯。它们都有 turn_on 和 turn_off 功能,只有 set_brightness 是 PWM 灯特有的。
如果不用继承,你就要写两个完全独立的接口表:
伪代码(重复定义):
typedef struct {
void (*turn_on)(void);
void (*turn_off)(void);
} gpio_light_ops_t;
typedef struct {
void (*turn_on)(void);
void (*turn_off)(void);
void (*set_brightness)(int brightness);
} pwm_light_ops_t;
这就重复了!如果以后要加一个 toggle 功能,你就要在两个结构体里都加一遍。
这时候就需要第三层壳子:结构体嵌套(也就是所谓的“继承”)。
我们把相同的部分抽出来:
伪代码(基础 + 派生):
// 所有灯都有的基础操作
typedef struct {
void (*turn_on)(void);
void (*turn_off)(void);
} light_base_ops_t;
// GPIO 灯:仅基础操作
typedef struct {
light_base_ops_t base;
} gpio_light_ops_t;
// PWM 灯:基础操作 + 调光
typedef struct {
light_base_ops_t base;
void (*set_brightness)(int brightness);
} pwm_light_ops_t;
现在如果要加 toggle 功能,只需要在 light_base_ops_t 里加一行,所有灯都会自动拥有这个功能。
这就是第三层壳子的价值:代码复用,避免重复。
这里为了大家方便理解继承思想,使用的是简化版本,如果想进一步了解请前往
《“我”指针:嵌入式C多实例OOP的分水岭》
希望可以为大家提供帮助。
更多推荐
所有评论(0)