04 手搓class:前缀、init/deinit和生命周期

C 语言 OOP 系列导航

  1. C语言你真的会封装吗?
  2. 三个LED写了三份代码?
  3. 同事改一行,LED全乱了
  4. 手搓class:前缀、init/deinit和生命周期 ⇐ 当前位置
  5. 你的全局变量,该死了
  6. 工业库也是同一套封装套路
  7. struct嵌套消灭复制粘贴
  8. 写死的函数怎么换
  9. 把一组函数指针装进对象
  10. 一个指针管所有LED
  11. 虚函数不实现会怎样
  12. 换硬件不改应用
  13. 从自动注册到Linux内核OOP全貌

C 语言没有 class,没有命名空间,也没有构造函数、析构函数。但工程代码不能因此乱写。

在 C 里,一个成熟模块通常靠三件事撑起”类”的样子:函数前缀、init/deinit、初始化状态检查。

痛点:所有模块都叫 init(),迟早撞名

如果你有 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 语言没有类作用域,函数名在链接阶段是全局符号。两个模块都叫 init(),就会冲突。

问题 后果
函数名太短 不知道属于哪个模块
多模块同名 链接冲突
缺少统一前缀 阅读代码要猜上下文
无生命周期约定 可能没初始化就使用

函数前缀就是类名

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 类

init() 是构造函数

对象不是声明出来就能用的。它需要被初始化。一个合格的 led_init() 通常要完成三件事:参数验证、硬件配置、默认状态填充。

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 语言里的构造函数。

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 不会。所以 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_init(),直接调用 led_on(&led)led.pin 可能是随机值。嵌入式里这可能意味着操作错误引脚。

所以公开接口要检查初始化状态:

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_ 前缀 类名 / 命名空间

C++ 替你管理了一部分生命周期,C 要你自己把规则写清楚。

嵌入式项目意义

外设驱动非常依赖初始化顺序。GPIO 没配就写电平,UART 没开时钟就发数据,PWM 没配置就改占空比,这些都可能让系统进入奇怪状态。

统一 init/deinit 的好处是初始化顺序清楚、资源释放明确、错误能被发现、接口风格统一。LED、电机、按键都能照这个模板写 — 这是 C 语言里最可靠的”类”组织方式。