C 语言面向对象设计路线
01 C 语言面向对象设计路线
C 语言 OOP 系列导航
本系列面向会 C 和单片机基础、刚进入企业项目的同学,从
struct封装一路讲到ops、多态和类 Linux 驱动框架。
刚进企业做嵌入式时,很多人会把“能跑”当成第一目标。比如点三颗 LED,最直接的写法是每颗灯一套函数:
1 | void red_led_on(void); |
这段代码不难,也不丑。真正的问题要等项目变大才出现:硬件改成低电平点亮,亮度要加限幅,开灯前要判断初始化状态。这时你会发现同一段逻辑散在很多函数里,改一个需求要翻全工程。
一句话先懂:C 语言面向对象,不是把 C 写得像 C++,而是把数据、操作、生命周期和变化点放回同一个模块里。
先看问题:函数多不是核心,归属混乱才是核心
企业项目里,坏代码常常不是一眼就坏。它通常长这样:
| 表面现象 | 真正的问题 |
|---|---|
| 每个 LED 一套函数 | 同一逻辑复制多份 |
| 状态变量到处放 | 数据没有明确主人 |
| 外部直接改字段 | 软件状态和硬件状态可能不一致 |
| 初始化靠注释提醒 | 生命周期靠人脑记忆 |
上层写很多 if/else |
新增硬件时旧代码也要改 |
这些问题加起来,就是维护成本。代码不是写完就结束,企业项目更常见的是:别人接手、硬件换版、需求追加、现场出 bug。
所以本系列讨论的 OOP,本质是在 C 语言里建立一套工程纪律。
总路线:用 C 显式写出对象系统
C 没有 class、this、构造函数、析构函数、虚函数,但可以用现成语法拼出等价结构:
struct + me + static + init/deinit + ops
整条路线如下:
| 阶段 | C 语言做法 | 对应 OOP 概念 | 解决的问题 |
|---|---|---|---|
| 封装数据 | struct |
对象数据 | 数据有主人 |
| 指定对象 | Led_t *me |
this 指针 |
一份函数操作不同对象 |
| 组织接口 | led_ 前缀 |
类名 / 命名空间 | 函数归属清楚 |
| 管生命周期 | init/deinit |
构造 / 析构 | 不让未初始化对象乱跑 |
| 隐藏实现 | .c 内部 static |
private | 外部不能绕过接口 |
| 数据归位 | 成员进对象,模块数据加 static |
成员变量 / 静态成员 | 消灭裸露全局变量 |
| 复用公共部分 | struct 嵌套 base |
继承 | 公共字段只写一份 |
| 替换行为 | 函数指针 | 回调 | 运行时决定调用谁 |
| 打包行为 | ops 操作表 |
vtable | 一组行为绑定到对象 |
| 自动分发 | base + ops dispatch |
多态 | 上层不关心具体类型 |
先不用急着记术语。你只要记住一条主线:让变化集中在该变化的地方。
一个 C 语言对象长什么样
最小版本只需要一个结构体和一组函数:
1 |
|
这已经具备对象的雏形:
| 代码元素 | 可以怎么理解 |
|---|---|
Led_t |
一颗 LED 的数据包 |
Led_t *me |
这次操作的是哪颗 LED |
led_init() |
把对象变成可用状态 |
led_deinit() |
让对象安全退出 |
led_on() / led_off() |
对象能执行的动作 |
initialized |
防止没初始化就使用 |
实现时,关键不是 gpio_write() 有多复杂,而是所有操作都围绕 me 展开:
1 | int led_on(Led_t *me) |
调用侧就变成:
1 | Led_t red; |
同一份 led_on(),通过不同的 me 操作不同的灯。这就是 C 语言里最朴素的“对象方法”。
C/C++ 对照
把这套写法翻译成 C++,关系很直接:
| C 写法 | C++ 写法 |
|---|---|
typedef struct { ... } Led_t; |
class Led { ... }; |
Led_t *me |
this |
led_init(&led, pin) |
Led led(pin) |
led_deinit(&led) |
~Led() |
led_on(&led) |
led.on() |
.c 里的 static 函数 |
private 方法 |
ops 函数指针表 |
虚函数表 |
C++ 把这些规则交给编译器。C 语言没有这些语法糖,所以要靠命名、结构体、接口和文件边界显式表达。
新人常见误区
| 误区 | 更准确的理解 |
|---|---|
OOP 就是用 struct 包变量 |
struct 只是第一步,还要有接口和生命周期 |
| 函数封装就是面向对象 | 函数复用不等于对象归属清楚 |
| C 写 OOP 是炫技 | 在驱动、协议栈、HAL、RTOS 里很常见 |
| 小项目不需要这些 | 小项目可以少用,但要知道什么时候该升级结构 |
刚开始写 LED 驱动时,不需要一上来就写 ops、继承、注册表。先把对象、接口、初始化边界写清楚,就已经能避开很多坑。
嵌入式项目里为什么重要
嵌入式代码背后连着真实硬件。软件状态写错,可能不是页面显示错,而是电机误转、继电器误吸合、传感器数据错判。
一个好的 C 模块应该能回答这些问题:
| 问题 | 好模块的答案 |
|---|---|
| 这份数据属于谁? | 属于某个 struct 对象 |
| 外部怎么操作它? | 只能调用公开接口 |
| 没初始化能不能用? | 不能,接口会返回错误 |
| 换硬件要改哪里? | 改底层实现,不改上层业务 |
| 新增一种设备怎么办? | 新增实现和 ops,尽量不改旧逻辑 |
这就是 C 语言 OOP 的价值:不是“像 C++”,而是让代码能被长期维护。
全系列怎么读
| 篇章 | 重点问题 |
|---|---|
| 01 总纲 | 建立完整路线 |
| 02 封装 | 一份函数服务多个对象 |
| 03 信息隐藏 | 不让外部乱碰内部状态 |
| 04 类的组织 | 用前缀和生命周期组织模块 |
| 05 数据归位 | 消灭裸露全局变量 |
| 06 HAL 映射 | 看懂工业库的封装结构 |
| 07 继承 | 抽公共字段和公共行为 |
| 08 函数指针 | 让行为可以被传递 |
| 09 ops/vtable | 把一组行为装进对象 |
| 10 多态与转型 | 一个基础指针管理所有对象 |
| 11 接口设计 | 处理必选能力、可选能力和默认实现 |
| 12 完整框架 | 换硬件不改应用 |
| 13 终章 | 映射到 Linux 内核 OOP |
总结
C 语言面向对象,不是模仿语法,而是建立边界:数据有主人,行为有入口,生命周期有规则,实现有隐藏,变化有出口。










