02 封装:struct 数据与 me 指针

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 结构

刚开始写驱动时,很多人会按“设备名 + 动作”写函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void red_led_on(void)
{
gpio_write(5, true);
}

void green_led_on(void)
{
gpio_write(6, true);
}

void blue_led_on(void)
{
gpio_write(7, true);
}

三颗灯,三段代码。看起来直观,也很容易调通。

问题是项目不会停在三颗灯。关灯、翻转、设置亮度、闪烁模式都加上后,函数数量会快速膨胀:

1
2
3
4
5
6
7
void red_led_off(void);
void green_led_off(void);
void blue_led_off(void);

void red_led_toggle(void);
void green_led_toggle(void);
void blue_led_toggle(void);

一句话先懂:封装的第一步,是把“同一类东西的数据”放进同一个 struct,再让同一份函数通过 me 指针操作不同对象。

先看问题:代码复制会把小改动放大

这种写法的本质是:每颗 LED 都拿到一份独立逻辑。

写法 后果
每颗 LED 一套函数 函数数量随设备数量增长
引脚号写死在函数里 换引脚要改代码
状态没有统一结构 亮度、开关、初始化状态难管理
上层知道具体硬件 业务代码被底层细节污染

企业项目里最怕这种“复制后稍微改一下”。一旦某个副本漏改,现场问题会很难查。

用 struct 描述一个 LED

一颗 LED 至少有这些数据:接在哪个引脚、当前亮度、现在是否点亮。

1
2
3
4
5
6
7
8
#include <stdbool.h>
#include <stdint.h>

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

有了 Led_t,红灯、绿灯、蓝灯不再需要三套函数,而是三份对象数据:

1
2
3
Led_t red_led;
Led_t green_led;
Led_t blue_led;

这里的关键变化是:LED 的数据有了固定归属。以后看到 Led_t,就知道它代表一颗 LED 的状态。

me 指针决定这次操作谁

只有数据还不够,函数还要知道这次操作哪颗灯。C 语言没有隐式 this,所以我们显式传入对象指针:

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

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

return 0;
}

me 可以读成“当前这个对象”。调用时传不同对象地址:

1
2
3
led_on(&red_led);
led_on(&green_led);
led_on(&blue_led);

于是同一份 led_on() 可以服务多颗 LED。区别不再写进函数名,而是放进对象数据。

初始化把对象填完整

声明结构体只是开了一块内存。真正使用前,要把它填成一个有效对象:

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

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

gpio_init(pin);
gpio_write(pin, false);

return 0;
}

完整流程变成:

1
2
3
4
5
6
7
8
9
10
11
Led_t red_led;
Led_t green_led;
Led_t blue_led;

led_init(&red_led, 5);
led_init(&green_led, 6);
led_init(&blue_led, 7);

led_on(&red_led);
led_on(&green_led);
led_off(&blue_led);

接口变化很明显:

改造前 改造后
red_led_on() led_on(&red_led)
green_led_on() led_on(&green_led)
blue_led_on() led_on(&blue_led)
为每颗 LED 写一份函数 一份函数服务多个对象

再看一个动作:设置亮度

设置亮度也不用为每颗灯单独写函数:

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

if (brightness > 100) {
return -2;
}

me->brightness = brightness;

if (brightness == 0) {
me->is_on = false;
gpio_write(me->pin, false);
} else {
me->is_on = true;
gpio_write(me->pin, true);
}

return 0;
}

调用形式仍然统一:

1
2
3
led_set_brightness(&red_led, 80);
led_set_brightness(&green_led, 30);
led_set_brightness(&blue_led, 0);

这就是封装带来的第一个收益:新增动作时,只需要给“LED 这种类型”加函数,不需要给每个具体 LED 重复写一遍。

新人常见误区

误区 更好的写法
struct 里只放配置,不放状态 配置和运行状态都应该按归属放进去
me 名字很奇怪,可以不用 名字可换成 self,但第一个参数必须表达操作对象
一个全局 g_led_pin 更简单 单实例时简单,多实例时很快失控
直接传 pin 就够了 只传 pin 管不住亮度、状态、初始化信息

me 不是魔法。它只是把“这次操作谁”说清楚。

C/C++ 对照

C++ 里同样的设计可能写成:

1
2
3
4
5
6
7
8
9
10
11
class Led {
public:
Led(int pin);
void on();
void off();

private:
int pin;
int brightness;
bool is_on;
};

对照关系:

C 语言 C++
Led_t class Led
Led_t *me this
led_init(&led, pin) Led led(pin)
led_on(&led) led.on()
me->pin this->pin

C++ 的 this 由编译器隐式传入;C 的 me 由开发者显式传入。语法不同,思想一致。

嵌入式项目意义

假设项目里有 10 个 LED、5 个按键、3 个电机。如果每个设备都单独写一套函数,代码会很快失控。

struct + me 后:

收益 说明
多实例天然支持 一个类型可以创建多个对象
逻辑只写一份 修 bug 只改一个函数
数据跟对象走 不会把红灯状态写到绿灯上
接口更统一 上层调用风格一致

总结

封装不是把代码塞进函数,而是让同一份逻辑服务不同的数据;struct 放数据,me 指针决定这次操作谁。