01 C 语言面向对象设计路线

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

刚进企业做嵌入式时,很多人会把“能跑”当成第一目标。比如点三颗 LED,最直接的写法是每颗灯一套函数:

1
2
3
4
5
6
7
void red_led_on(void);
void green_led_on(void);
void blue_led_on(void);

void red_led_off(void);
void green_led_off(void);
void blue_led_off(void);

这段代码不难,也不丑。真正的问题要等项目变大才出现:硬件改成低电平点亮,亮度要加限幅,开灯前要判断初始化状态。这时你会发现同一段逻辑散在很多函数里,改一个需求要翻全工程。

一句话先懂:C 语言面向对象,不是把 C 写得像 C++,而是把数据、操作、生命周期和变化点放回同一个模块里。

先看问题:函数多不是核心,归属混乱才是核心

企业项目里,坏代码常常不是一眼就坏。它通常长这样:

表面现象 真正的问题
每个 LED 一套函数 同一逻辑复制多份
状态变量到处放 数据没有明确主人
外部直接改字段 软件状态和硬件状态可能不一致
初始化靠注释提醒 生命周期靠人脑记忆
上层写很多 if/else 新增硬件时旧代码也要改

这些问题加起来,就是维护成本。代码不是写完就结束,企业项目更常见的是:别人接手、硬件换版、需求追加、现场出 bug。

所以本系列讨论的 OOP,本质是在 C 语言里建立一套工程纪律。

总路线:用 C 显式写出对象系统

C 没有 classthis、构造函数、析构函数、虚函数,但可以用现成语法拼出等价结构:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdbool.h>
#include <stdint.h>

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

int led_init(Led_t *me, uint8_t pin);
int led_deinit(Led_t *me);
int led_on(Led_t *me);
int led_off(Led_t *me);
int led_set_brightness(Led_t *me, uint8_t brightness);

这已经具备对象的雏形:

代码元素 可以怎么理解
Led_t 一颗 LED 的数据包
Led_t *me 这次操作的是哪颗 LED
led_init() 把对象变成可用状态
led_deinit() 让对象安全退出
led_on() / led_off() 对象能执行的动作
initialized 防止没初始化就使用

实现时,关键不是 gpio_write() 有多复杂,而是所有操作都围绕 me 展开:

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

if (!me->initialized) {
return -2;
}

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

return 0;
}

调用侧就变成:

1
2
3
4
5
6
7
8
Led_t red;
Led_t green;

led_init(&red, 5);
led_init(&green, 6);

led_on(&red);
led_on(&green);

同一份 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 语言面向对象,不是模仿语法,而是建立边界:数据有主人,行为有入口,生命周期有规则,实现有隐藏,变化有出口。