01 总纲:C 语言面向对象设计概览
01 总纲:C 语言面向对象设计概览
C 语言面向对象系列
本系列面向已经了解 C 语言和单片机基础、开始编写嵌入式项目的读者。我们以 LED 驱动为例,逐步学习如何组织对象数据、设计操作接口,以及支持不同的硬件实现。
面向对象设计(OOP)可以先理解为:将相关的数据和操作组织在一起,并明确它们的使用方式。C 没有 class 语法,但可以用结构体、函数和函数指针实现这些设计。
从重复的设备接口开始
控制三颗 LED 时,可能会先为每颗灯分别编写接口:
1 | void red_led_on(void); |
这类写法容易上手,但同一逻辑需要维护多份。硬件改成低电平点亮、增加亮度限幅,或要求操作前检查初始化状态时,每组函数都可能需要修改。
除了重复代码,还需要留意数据和接口的关系:
| 情况 | 可能带来的问题 |
|---|---|
| 每颗 LED 各有一套函数 | 相同逻辑重复,修改时容易遗漏 |
| 状态变量分散存放 | 难以区分数据属于哪个设备或模块 |
| 外部直接修改成员 | 软件状态可能与硬件不一致 |
| 初始化顺序只写在注释里 | 调用方可能在对象准备好之前使用它 |
| 上层分别处理每种硬件 | 新增硬件时需要修改多处分支 |
这些问题会影响后续接手、硬件改版、需求扩展和故障排查。对于嵌入式设备,状态错误还可能引起电机误转、继电器误动作或传感器数据误判。
用结构体和函数表示对象
先将一颗 LED 所需的数据放进结构体,再声明操作它的函数:
1 |
|
这里的 Led_t 是类型,每个 Led_t 变量保存一颗 LED 的数据。Led_t *me 指向本次操作的对象,函数通过它读取引脚、修改状态。
led_init() 负责初始化,led_deinit() 负责结束使用和释放资源;led_on()、led_off() 和 led_set_brightness() 则描述对象支持的操作。initialized 用来记录对象是否已完成初始化。
以点亮操作为例,函数先检查指针和初始化状态,再更新对象及硬件:
1 | int led_on(Led_t *me) |
创建两颗 LED 时,只需要准备两份对象数据,调用同一组函数:
1 | Led_t red; |
两次 led_on() 使用同一份实现,传入的对象地址不同,访问的引脚和状态也不同。这是本系列首先要掌握的写法。
各篇代码用于说明当前步骤,gpio_write() 等底层函数需要由具体平台提供。对象应在初始化成功后使用,相关检查和调用顺序会在第 04 篇展开。
从封装逐步扩展到多态
在前面的基础上,可以逐步加入以下设计:
| 设计内容 | C 语言中的做法 | 作用 |
|---|---|---|
| 保存对象数据 | struct |
将单个实例的配置和状态放在一起 |
| 指定操作对象 | Led_t *me |
让同一函数操作不同实例 |
| 组织公开接口 | led_ 等函数前缀 |
明确函数所属模块 |
| 管理生命周期 | init/deinit |
约定初始化、使用和释放的顺序 |
| 隐藏内部实现 | 源文件中的 static 函数 |
限定内部辅助函数的链接范围 |
| 区分数据范围 | 实例成员、模块 static 变量 |
区分独立状态与共享状态 |
| 复用公共部分 | 在结构体中嵌入 base |
复用共同的数据和操作 |
| 替换单个操作 | 函数指针、回调 | 保存或传入要调用的函数 |
| 组织一组操作 | ops 函数指针表 |
为不同设备提供同一组接口 |
| 统一调用不同实现 | base + ops |
由对象的操作表选择具体函数 |
最后一种方式称为多态:调用方使用相同接口,实际执行哪个函数由对象绑定的实现决定。新增一种设备时,如果已有接口能表达它的能力,就可以新增实现和操作表,尽量减少对上层代码的修改。
与 C++ 的对应关系
熟悉 C++ 的读者可以用下面的对照辅助理解;只有 C 基础的读者可以先继续阅读后面的教程。
| 本系列的 C 写法 | C++ 中职责相近的写法 |
|---|---|
typedef struct { ... } Led_t; |
类中的对象数据 |
Led_t *me |
成员函数中的 this |
led_init(&led, pin) |
通过构造函数初始化,例如 Led led(pin) |
led_deinit(&led) |
析构函数 ~Led() 中的清理工作 |
led_on(&led) |
成员函数调用 led.on() |
源文件中的 static 辅助函数 |
供内部实现使用的 private 方法 |
ops 函数指针表 |
虚函数调用所用的操作分发机制 |
这些对应主要说明设计用途。C 代码需要显式传入对象指针、调用初始化与清理函数,并自行约定访问范围。后续章节会在使用这些机制时说明具体限制。
仅仅把变量放入结构体,或者提取几个可复用函数,还不足以说明模块的使用方式。还需要明确哪些数据属于对象、外部通过哪些接口访问,以及对象在什么状态下可以使用。
阅读顺序
13 篇沿着同一个 LED 示例逐步展开:
| 篇章 | 学习内容 |
|---|---|
| 01 总纲 | 了解系列路线和基本写法 |
| 02 封装 | 用同一组函数操作多个实例 |
| 03 信息隐藏 | 区分公开接口和内部实现 |
| 04 模块组织 | 统一命名,管理初始化和释放 |
| 05 数据组织 | 区分实例状态、模块状态和常量 |
| 06 HAL 实例 | 对照 STM32 外设接口理解封装 |
| 07 结构体嵌套 | 复用公共数据和行为 |
| 08 函数指针 | 传递操作,编写回调 |
| 09 操作表 | 将相关操作组织成 ops |
| 10 多态 | 通过统一接口调用不同实现 |
| 11 接口约定 | 处理必选操作、可选操作和默认实现 |
| 12 框架整合 | 组织应用、驱动、板级和平台代码 |
| 13 工程扩展 | 理解注册机制及 Linux 内核中的相关设计 |
驱动、协议栈、HAL 和 RTOS 中都能见到这些组织方式。初学时可以先完成对象、接口和初始化流程;项目需要支持多种实现时,再引入 base、ops 或注册机制。第 13 篇会集中讨论如何选择。










