04 类的组织:前缀、init/deinit 与生命周期

C 语言 OOP 系列导航

本系列面向会 C 和单片机基础、刚进入企业项目的同学,从 struct 封装一路讲到 ops、多态和类 Linux 驱动框架。

  1. C 语言面向对象设计路线
  2. 封装:struct 数据与 me 指针
  3. 信息隐藏:static 与头文件边界
  4. 类的组织:前缀、init/deinit 与生命周期 ⇐ 当前位置
  5. 数据归位:struct 成员与 static 变量
  6. HAL 映射:工业库的封装结构
  7. 继承:struct 嵌套复用公共字段
  8. 函数指针:把行为作为参数
  9. ops/vtable:行为表与对象绑定
  10. 多态与转型:base 指针与 dispatch
  11. 接口设计:ops 合同与默认实现
  12. 完整框架:抽象层与硬件解耦
  13. 终章:自动注册与 Linux 内核 OOP 结构

企业项目里,一个模块通常不是只有一个函数。LED 模块可能有初始化、开灯、关灯、设置亮度、释放资源;电机模块也有初始化、启动、停止、设置速度。

如果大家都写短名字:

1
2
3
int init(void);
int on(void);
int off(void);

另一个模块也写:

1
2
3
int init(void);
int start(void);
int stop(void);

链接阶段很容易冲突,阅读代码时也不知道函数属于哪个模块。

一句话先懂:C 语言没有 class,所以要用“模块前缀 + init/deinit + initialized 检查”把一个类的边界显式写出来。

先看问题:函数能调用,不代表对象能用

很多嵌入式 bug 都来自生命周期混乱:

问题 现场表现
GPIO 没初始化就写电平 设备不响应或状态异常
UART 没开时钟就发送 数据发不出去
PWM 没配置就设置占空比 波形不对
退出时没释放资源 下次初始化状态不干净

所以模块不只要有“动作函数”,还要有明确的“从不可用到可用,再从可用到退出”的过程。

前缀就是 C 语言里的类名

LED 模块的公共接口统一以 led_ 开头:

1
2
3
4
5
int led_init(Led_t *me, uint8_t pin);
int led_deinit(Led_t *me);
int led_on(Led_t *me);
int led_off(Led_t *me);
int led_set_brightness(Led_t *me, uint8_t brightness);

电机模块统一用 motor_

1
2
3
4
5
int motor_init(Motor_t *me, uint8_t pin);
int motor_deinit(Motor_t *me);
int motor_start(Motor_t *me);
int motor_stop(Motor_t *me);
int motor_set_speed(Motor_t *me, uint8_t speed);

前缀的作用不是装饰,而是建立归属:

C 函数前缀 等价理解
led_ LED 类
motor_ Motor 类
button_ Button 类
uart_ UART 类

看到 led_on(),读者立刻知道它属于 LED 模块;看到 uart_init(),就知道它是 UART 模块的初始化。

init 负责把对象变成可用状态

对象不能声明后直接用:

1
2
Led_t led;
led_on(&led); /* pin、brightness、initialized 都还没准备好 */

一个合格的 led_init() 至少做三件事:

步骤 目的
参数验证 防止空指针、非法引脚
硬件配置 把 GPIO 设置成可用状态
默认状态 初始化成员变量

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define LED_PIN_MAX 15

typedef struct {
uint8_t pin;
uint8_t brightness;
bool is_on;
bool initialized;
} Led_t;

static bool led_is_pin_valid(uint8_t pin)
{
return pin <= LED_PIN_MAX;
}

int led_init(Led_t *me, uint8_t pin)
{
if (me == NULL) {
return -1;
}

if (!led_is_pin_valid(pin)) {
return -2;
}

gpio_init(pin);

me->pin = pin;
me->brightness = 0;
me->is_on = false;
me->initialized = true;

gpio_write(pin, false);

return 0;
}

led_init() 就是 C 语言里的手写构造函数。它负责把一块普通内存变成“可以使用的 LED 对象”。

deinit 负责让对象安全退出

有初始化,就应该有反初始化。led_deinit() 至少要关闭硬件、清理状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int led_deinit(Led_t *me)
{
if (me == NULL) {
return -1;
}

if (!me->initialized) {
return 0;
}

me->is_on = false;
me->brightness = 0;
gpio_write(me->pin, false);
gpio_deinit(me->pin);

me->initialized = false;

return 0;
}

C++ 析构函数能在对象离开作用域时自动执行。C 语言没有自动析构,所以调用流程要写清楚:

1
2
3
4
5
6
7
8
9
10
Led_t led;

if (led_init(&led, 5) != 0) {
return -1;
}

led_on(&led);
led_set_brightness(&led, 80);

led_deinit(&led);

initialized 字段拦住错误调用

公共接口不能假设调用方一定按顺序使用。led_on() 应该主动检查对象是否已经初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int led_on(Led_t *me)
{
if (me == NULL) {
return -1;
}

if (!me->initialized) {
return -2;
}

me->is_on = true;
gpio_write(me->pin, true);

return 0;
}

这不是多余代码,而是模块的自我保护:

防御点 防住的问题
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_ 前缀 类名 / 命名空间

新人常见误区

误区 更稳的理解
反正单片机不释放资源,可以不写 deinit 低功耗、外设复用、异常恢复时都可能需要
初始化失败就返回 void 企业项目里应返回错误码,让上层能处理
initialized 浪费一个字段 它换来的是更可控的失败方式
函数名前缀太啰嗦 C 没有命名空间,前缀就是最低成本的边界

C/C++ 对照

C 写法 C++ 写法
Led_t led; led_init(&led, pin); Led led(pin);
led_deinit(&led); ~Led()
led_on(&led); led.on();
led_ 前缀 Led:: 作用域
initialized 检查 构造后对象有效

嵌入式项目意义

驱动模块的可靠性,很大一部分来自生命周期清楚:

收益 说明
初始化顺序清楚 上层知道先调什么
资源释放明确 退出时硬件状态可控
错误能被发现 未初始化调用返回错误
接口风格统一 LED、电机、按键都能照这个模板写

总结

C 语言没有 class 关键字,但 模块前缀 + struct + init/deinit + initialized 已经能组织出一个可靠的类。