封装:struct 数据与 me 指针
02 封装:struct 数据与 me 指针
C 语言 OOP 系列导航
本系列面向会 C 和单片机基础、刚进入企业项目的同学,从
struct封装一路讲到ops、多态和类 Linux 驱动框架。
刚开始写驱动时,很多人会按“设备名 + 动作”写函数:
1 | void red_led_on(void) |
三颗灯,三段代码。看起来直观,也很容易调通。
问题是项目不会停在三颗灯。关灯、翻转、设置亮度、闪烁模式都加上后,函数数量会快速膨胀:
1 | void red_led_off(void); |
一句话先懂:封装的第一步,是把“同一类东西的数据”放进同一个 struct,再让同一份函数通过 me 指针操作不同对象。
先看问题:代码复制会把小改动放大
这种写法的本质是:每颗 LED 都拿到一份独立逻辑。
| 写法 | 后果 |
|---|---|
| 每颗 LED 一套函数 | 函数数量随设备数量增长 |
| 引脚号写死在函数里 | 换引脚要改代码 |
| 状态没有统一结构 | 亮度、开关、初始化状态难管理 |
| 上层知道具体硬件 | 业务代码被底层细节污染 |
企业项目里最怕这种“复制后稍微改一下”。一旦某个副本漏改,现场问题会很难查。
用 struct 描述一个 LED
一颗 LED 至少有这些数据:接在哪个引脚、当前亮度、现在是否点亮。
1 |
|
有了 Led_t,红灯、绿灯、蓝灯不再需要三套函数,而是三份对象数据:
1 | Led_t red_led; |
这里的关键变化是:LED 的数据有了固定归属。以后看到 Led_t,就知道它代表一颗 LED 的状态。
me 指针决定这次操作谁
只有数据还不够,函数还要知道这次操作哪颗灯。C 语言没有隐式 this,所以我们显式传入对象指针:
1 | int led_on(Led_t *me) |
me 可以读成“当前这个对象”。调用时传不同对象地址:
1 | led_on(&red_led); |
于是同一份 led_on() 可以服务多颗 LED。区别不再写进函数名,而是放进对象数据。
初始化把对象填完整
声明结构体只是开了一块内存。真正使用前,要把它填成一个有效对象:
1 | int led_init(Led_t *me, uint8_t pin) |
完整流程变成:
1 | Led_t red_led; |
接口变化很明显:
| 改造前 | 改造后 |
|---|---|
red_led_on() |
led_on(&red_led) |
green_led_on() |
led_on(&green_led) |
blue_led_on() |
led_on(&blue_led) |
| 为每颗 LED 写一份函数 | 一份函数服务多个对象 |
再看一个动作:设置亮度
设置亮度也不用为每颗灯单独写函数:
1 | int led_set_brightness(Led_t *me, uint8_t brightness) |
调用形式仍然统一:
1 | led_set_brightness(&red_led, 80); |
这就是封装带来的第一个收益:新增动作时,只需要给“LED 这种类型”加函数,不需要给每个具体 LED 重复写一遍。
新人常见误区
| 误区 | 更好的写法 |
|---|---|
struct 里只放配置,不放状态 |
配置和运行状态都应该按归属放进去 |
me 名字很奇怪,可以不用 |
名字可换成 self,但第一个参数必须表达操作对象 |
一个全局 g_led_pin 更简单 |
单实例时简单,多实例时很快失控 |
直接传 pin 就够了 |
只传 pin 管不住亮度、状态、初始化信息 |
me 不是魔法。它只是把“这次操作谁”说清楚。
C/C++ 对照
C++ 里同样的设计可能写成:
1 | class Led { |
对照关系:
| 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指针决定这次操作谁。










