04 模块组织:命名约定与对象生命周期
04 模块组织:命名约定与对象生命周期
C 语言面向对象系列
前面已经有了 LED 类型和操作接口。本节把这些内容组织成一个模块:用统一前缀命名函数,通过 init/deinit 管理对象从初始化、使用到释放的过程。这个过程称为对象的生命周期。
用函数前缀区分模块
假设 LED 模块的接口命名如下:
1 | int init(void); |
电机模块也采用相同的命名方式:
1 | int init(void); |
如果两个模块都定义了具有外部链接的 init(),链接时会发生重名冲突。即使没有冲突,单看 init() 也很难判断它属于哪个模块。
可以给 LED 接口统一添加 led_ 前缀:
1 | int led_init(Led_t *me, uint8_t pin); |
电机接口使用 motor_ 前缀:
1 | int motor_init(Motor_t *me, uint8_t pin); |
同样,按键和串口模块可以分别使用 button_、uart_。前缀明确了函数所属的模块,也让调用代码更容易阅读。它是一种命名约定,不会创建 C++ 类或命名空间那样的作用域。
初始化对象和硬件
声明一个 Led_t 变量后,还需要设置成员并配置硬件。下面的 led_init() 依次检查参数、初始化 GPIO、填写默认状态,最后让 LED 保持关闭:
1 |
|
成功返回后,pin、brightness、is_on 和 initialized 都有了明确的值。调用方应检查返回值,再使用对象。
这个函数承担了类似构造函数的初始化工作,不过 C 不会在声明对象时自动调用它。
使用结束后释放资源
led_deinit() 负责关闭 LED、释放 GPIO 配置,并清除初始化标志:
1 | int led_deinit(Led_t *me) |
这与析构函数都有清理资源的用途。C++ 的普通局部对象离开作用域时会自动调用析构函数;这里的 C 接口需要调用方显式调用。
一次完整使用过程如下:
1 | Led_t led; |
对象初始化成功后才能使用,使用结束后再反初始化。GPIO、电机、按键等模块都可以采用类似的调用顺序。
在操作接口中检查状态
公开接口可以先检查对象指针,再检查初始化标志:
1 | int led_on(Led_t *me) |
initialized 为 false 时,函数返回错误,避免继续操作引脚。这个检查的前提是标志本身已有确定的值。对于尚未初始化的局部变量,不能依靠读取其中的 initialized 判断状态;可以在创建时先零初始化,再调用 led_init()。
这些检查分别处理不同问题:
| 检查 | 作用 |
|---|---|
me == NULL |
避免访问空指针 |
pin 范围 |
拒绝非法引脚参数 |
initialized |
检查对象是否处于可用状态 |
| 返回错误码 | 让调用方决定如何处理失败 |
外设操作也有顺序要求。例如,GPIO 需要先配置,UART 发送前需要开启相应时钟,PWM 输出前需要完成配置。初始化接口应集中处理这些准备工作。
一个模块中的职责分工
可以按下面的方式组织 LED 模块:
| 内容 | 职责 |
|---|---|
led.h |
声明公开类型和接口 |
led.c |
实现接口和内部逻辑 |
Led_t |
保存一个对象的数据 |
led_init() |
设置初始状态并配置硬件 |
led_deinit() |
释放资源并结束使用 |
led_on() 等操作函数 |
通过对象指针执行操作 |
static 辅助函数 |
供当前源文件内部使用 |
led_ 前缀 |
标明接口所属模块 |
这种组织方式把对象数据、操作接口和生命周期放在一起。阅读模块时,可以先看头文件了解用法,再进入源文件查看实现。










