02 封装:结构体与多实例接口
02 封装:结构体与多实例接口
C 语言面向对象系列
本节从三颗 LED 开始,将重复的控制函数整理成一组可复用接口。需要完成两件事:用结构体保存每颗 LED 的数据,让函数通过指针确定本次操作的对象。
分别编写设备函数会产生重复
按“设备名加动作”命名时,点亮三颗 LED 的代码可能如下:
1 | void red_led_on(void) |
三个函数的操作相同,只有引脚号不同。继续增加关闭、翻转、设置亮度或闪烁等操作时,每颗灯又需要一组函数:
1 | void red_led_off(void); |
设备和操作越多,需要维护的副本就越多。引脚号固定在函数内部,换引脚需要修改实现;亮度、开关状态等数据也缺少统一的存放位置。上层还需要知道每颗灯对应哪个接口。
可以把设备之间的差别放进数据中,让相同的逻辑共用一份函数。
用结构体保存一颗 LED 的数据
本例用引脚号、亮度和开关状态描述 LED:
1 |
|
Led_t 定义了数据的结构。红灯、绿灯、蓝灯分别创建一个变量,每个变量就是一个实例:
1 | Led_t red_led; |
每个实例各自保存配置和运行状态。只传一个 pin 参数虽然能指定引脚,但无法同时管理这些相关数据;将它们放在同一个对象中,函数就能通过统一入口访问。
通过 me 指定操作对象
函数接收一个 Led_t * 参数,用它访问当前 LED:
1 | int led_on(Led_t *me) |
me 是普通的参数名,也可以写成 self。本系列约定把对象指针放在第一个参数,便于识别函数操作的对象。
调用形式如下,实际执行前需要先初始化这些对象:
1 | led_on(&red_led); |
&red_led 取得红灯对象的地址。函数接收这个地址后,me->pin 读取红灯的引脚,me->is_on 修改红灯的状态。传入绿灯地址时,相同代码就会访问绿灯的数据。
初始化对象后再调用操作接口
声明变量后,还需要填入配置和默认状态,并完成 GPIO 配置:
1 | int led_init(Led_t *me, uint8_t pin) |
完整的调用顺序是先初始化,再执行操作:
1 | Led_t red_led; |
这里为三颗 LED 分别指定引脚 5、6、7。led_off() 可以采用与 led_on() 相同的组织方式,关闭对象对应的 LED。
接口的变化如下:
| 原来的调用 | 使用对象后的调用 |
|---|---|
red_led_on() |
led_on(&red_led) |
green_led_on() |
led_on(&green_led) |
blue_led_on() |
led_on(&blue_led) |
操作由函数决定,设备由参数决定。之后修改点亮逻辑时,只需要维护同一份 led_on()。
为对象增加亮度设置接口
增加操作时,仍然使用同一个对象指针。下面的接口检查亮度范围,并把数值保存到对象中:
1 | int led_set_brightness(Led_t *me, uint8_t brightness) |
调用方仍然传入对象和参数:
1 | led_set_brightness(&red_led, 80); |
这段 GPIO 示例只区分开关:亮度为 0 时关闭,大于 0 时开启。80 和 30 会分别保存在对象中,但这段代码不会产生不同的实际亮度。后面引入 PWM 实现时,再通过占空比完成调光。
新增这种操作,只需要为 Led_t 编写一个函数,各个 LED 实例都可以使用。
与 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 调用显式传入对象指针,C++ 的非静态成员函数通过 this 访问当前对象。初始化、访问权限等机制仍有区别,后面会分别说明。
将同一写法用于多个设备
假设项目有 10 个 LED、5 个按键和 3 个电机,可以为每类设备定义类型和接口,再按设备数量创建实例。单个全局变量虽然能保存一台设备的状态,但多个实例需要各自的数据。
例如,按前面的方式声明并初始化 red、green、blue 后,可以复用同一个接口:
1 | // 同一份 led_on,三个不同的对象 |
这样既能共享操作逻辑,也能分别保存设备状态。接下来需要进一步约定外部代码如何访问这些数据,避免绕过接口直接修改成员。










