04 手搓class:前缀、init/deinit和生命周期
04 手搓class:前缀、init/deinit和生命周期
C 语言 OOP 系列导航
C 语言没有 class,没有命名空间,也没有构造函数、析构函数。但工程代码不能因此乱写。
在 C 里,一个成熟模块通常靠三件事撑起”类”的样子:函数前缀、init/deinit、初始化状态检查。
痛点:所有模块都叫 init(),迟早撞名
如果你有 LED 模块和电机模块,可能一开始会这样写:
1 | int init(void); |
然后电机模块也想写:
1 | int init(void); |
C 语言没有类作用域,函数名在链接阶段是全局符号。两个模块都叫 init(),就会冲突。
| 问题 | 后果 |
|---|---|
| 函数名太短 | 不知道属于哪个模块 |
| 多模块同名 | 链接冲突 |
| 缺少统一前缀 | 阅读代码要猜上下文 |
| 无生命周期约定 | 可能没初始化就使用 |
函数前缀就是类名
LED 模块统一用 led_ 前缀:
1 | int led_init(Led_t *me, uint8_t pin); |
电机模块统一用 motor_ 前缀:
1 | int motor_init(Motor_t *me, uint8_t pin); |
这样一看就知道函数归属:
| C 函数前缀 | 等价理解 |
|---|---|
led_ |
LED 类 |
motor_ |
Motor 类 |
button_ |
Button 类 |
uart_ |
UART 类 |
init() 是构造函数
对象不是声明出来就能用的。它需要被初始化。一个合格的 led_init() 通常要完成三件事:参数验证、硬件配置、默认状态填充。
1 |
|
led_init() 就是 C 语言里的构造函数。
deinit() 是析构函数
有初始化,就应该有反初始化。led_deinit() 至少要做两件事:关闭硬件(退出时保持安全状态)、清除状态(防止对象被误用)。
1 | int led_deinit(Led_t *me) |
C++ 的析构函数离开作用域会自动调用,C 不会。所以 C 代码必须明确写:
1 | Led_t led; |
initialized:防止没开门就营业
如果调用者忘了 led_init(),直接调用 led_on(&led),led.pin 可能是随机值。嵌入式里这可能意味着操作错误引脚。
所以公开接口要检查初始化状态:
1 | int led_on(Led_t *me) |
这不是啰嗦,是防御式设计:
| 防御点 | 防住的问题 |
|---|---|
me == NULL |
空指针访问 |
pin 范围检查 |
操作非法引脚 |
initialized |
未初始化对象被使用 |
| 返回错误码 | 调用者能处理失败 |
一个 .h + .c 就是一个类
可以把 C 模块理解成这样:
| C 模块元素 | OOP 概念 |
|---|---|
led.h |
类的公共声明 |
led.c |
类的实现 |
Led_t |
对象数据 |
led_init() |
构造函数 |
led_deinit() |
析构函数 |
led_on() |
成员方法 |
static helper |
private 方法 |
led_ 前缀 |
类名 / 命名空间 |
C++ 替你管理了一部分生命周期,C 要你自己把规则写清楚。
嵌入式项目意义
外设驱动非常依赖初始化顺序。GPIO 没配就写电平,UART 没开时钟就发数据,PWM 没配置就改占空比,这些都可能让系统进入奇怪状态。
统一 init/deinit 的好处是初始化顺序清楚、资源释放明确、错误能被发现、接口风格统一。LED、电机、按键都能照这个模板写 — 这是 C 语言里最可靠的”类”组织方式。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Rvosy的小破站!










