懂了汽车,就懂了 C 语言面向对象编程

用汽车这个每个人都熟悉的例子,彻底搞懂 C 语言怎么写 OOP。


可以微 信来关注 青衫嵌入式,会持续发布嵌入式干货。

一、为什么 C 语言也需要面向对象?

做嵌入式或系统软件开发的同学都有过这种体验:项目一开始几十个全局变量、十几个函数,结构清晰一目了然。随着需求叠加,全局变量膨胀到几百个,函数散落在十几个文件里,改一个参数要追着七八个文件跑,整个项目像一张越织越密的蜘蛛网------碰一下,整张网都在抖。

这就是没有结构的 C 代码到了一定规模后必然出现的"复杂性爆炸"。

面向对象编程(OOP)本质上是一种组织代码的思维方式 ------把数据和对数据的操作绑定在一起,对外隐藏实现细节,对内保持职责单一。它不依赖任何语言特性:Java 有 class 关键字,C++ 有 class 关键字,但 C 语言没有 ------可这并不妨碍我们用 C 写出漂亮的面向对象代码。 拿汽车这个任何人都熟悉的例子------我们一步步拆解,看看 C 语言如何模拟面向对象的四大核心概念:封装、继承、多态、接口。

源码地址:gitcode.com/qingshan120...

类继承关系:

燃油车子类(继承汽车基类+包含燃油引擎组件),实例化宝马530Li;

纯电车子类(继承汽车基类 +包含电动马达组件),实例化特斯拉Model3;

混动车子类(继承汽车基类 +包含混动系统组件 ),实例化比亚迪秦L,其中混动系统组件 是由燃油引擎 +电动马达两个组件整合而成

二、封装:你的数据,只有你的方法能碰

痛点

传统 C 开发中,结构体往头文件一扔,谁都能直接 car.speed = -100 来一手------改错了?排查半天。

解决方案

封装的核心思想是:"外部使用者不关心你的数据长什么样,只关心你能做什么"。在 C 语言里,这通过不透明结构体 + 操作函数实现:

c 复制代码
/* car.h ------ 对外只暴露函数接口 */
typedef struct Car Car;     /* 前置声明:外部不知道 Car 里面有什么 */

void  carInit(Car* c, const char* brand, const char* model);
void  carDrive(Car* c, float distance);
float carGetSpeed(const Car* c);
float carGetMileage(const Car* c);

使用者只能通过 carInit、carDrive 这些"方法"来操作 Car 对象------至于 Car 内部的 speed、mileage 字段怎么存的?藏起来了,你碰不到。这跟 C++ 里 class 的 public 方法完全是一个思路。

注:本项目中因为子类继承需要(见下一节),struct Car 的定义实际放在头文件中------但通过后文要讲的向上转型宏,使用层面仍然保持了封装性。


三、继承:把基类嵌在子类的"头"上

概念

燃油车是一辆车,纯电车是一辆车,混动车也是一辆车。三者共享"品牌、型号、速度、里程"这些通用属性,但各自又有独特的动力系统。

OOP 用继承来表达这种"is-a"关系------子类自动获得父类的所有属性和方法,再叠加自己的特性。

C 语言的实现手法

在 C 语言中模拟继承,只有一个标准手段:把基类作为子类结构体的第一个字段。

c 复制代码
/* fuelCar.h ------ 燃油车:继承 Car + 组合 FuelEngine */
typedef struct FuelCar {
    Car        base;    /* ★ 第一个字段 = 继承 Car(is-a) */
    FuelEngine engine;  /* 普通成员 = 拥有发动机(has-a) */
} FuelCar;

C 标准(ISO C11 第 6.7.2.1 节第 15 条)明确保证:结构体的第一个成员地址等于 结构体自身的地址。因此 (Car*)&fuelCar 永远安全,且零运行时开销------编译器在编译期就能搞定。

子类初始化的"三步曲"

以燃油车为例,它的初始化遵循一个固定流程:

c 复制代码
void fuelCarInit(FuelCar* fc, const char* brand, const char* model,
                 float tankCap, float fuelCons)
{
    /* 步骤 1: 调用基类的"构造函数" */
    carInit(&fc->base, brand, model);

    /* 步骤 2: 初始化自己的组件 */
    fuelEngineInit(&fc->engine, tankCap, fuelCons);

    /* 步骤 3: 将组件的接口注入基类(关键!) */
    PowerSource* ps = carGetPowerSource(&fc->base);
    *ps = fuelEngineGetInterface(&fc->engine);
}

第一步搞定通用属性(品牌、型号),第二步初始化特有的油箱和油耗数据,第三步把发动机的"能力"注册到基类------后续基类的 carDrive 方法就能通过这个接口调用到燃油发动机的具体消耗逻辑。

锦上添花:向上转型宏

如果你觉得每次调用 carDrive((Car*)&bmw5, 300) 还要手动强转太啰嗦------我们写了一个宏自动搞定:

c 复制代码
#define carDrive(p, dist)    _carDrive((Car*)(p), (dist))
#define carGetSpeed(p)       _carGetSpeed((const Car*)(p))
#define carGetMileage(p)     _carGetMileage((const Car*)(p))

现在直接写 carDrive(&bmw5, 300) 就行,宏在背后帮你完成向上转型。


四、多态与接口:同一个操作,不同的行为 ------ 这才是灵魂

如果说封装和继承是"规矩",那多态就是"灵魂"------它让同一个函数调用在不同对象上表现出不同行为。

回到汽车:同样是"行驶 300 公里",燃油车要烧油,纯电车要耗电,混动车要先用电、电不够再烧油。调用方不想知道这些细节------它只想说"开",然后各自各的方式去开。

核心手段:虚函数表(vtable)

在 C++ 里,虚函数表是编译器帮你生成的。在 C 语言里,我们手动构造它------用函数指针结构体:

c 复制代码
/* PowerSource ------ 动力源接口:一组纯虚函数的集合 */
typedef struct PowerSourceOps {
    void  (*refill)(void* self, float amount);       /* 补充能量(加油/充电) */
    float (*consume)(void* self, float distance);    /* 消耗能量行驶 */
    float (*getRemaining)(void* self);               /* 查询剩余能量 */
    float (*getRange)(void* self);                   /* 查询剩余续航 */
    const char* (*getType)(void* self);              /* 能量类型名 */
} PowerSourceOps;

typedef struct PowerSource {
    void*          self;  /* 指向实际引擎数据(多态的关键:void* 的 this 指针) */
    PowerSourceOps ops;   /* 虚函数表 */
} PowerSource;

这个 PowerSource 就是一个"接口"------它只定义契约,不包含实现。任何实现了这五个函数的结构体,都可以"伪装成"一个 PowerSource。

三种实现者

燃油发动机 填充了烧油逻辑:

c 复制代码
PowerSource fuelEngineGetInterface(FuelEngine* e)
{
    PowerSource ps;
    ps.self = e;
    ps.ops.consume      = fuelEngineConsume;   /* 按百公里油耗计算 */
    ps.ops.getRemaining = fuelEngineGetRemaining;
    ps.ops.getType      = fuelEngineGetType;    /* 返回 "汽油" */
    /* ... */
    return ps;
}

电机 填充了耗电逻辑------结构完全镜像,只是操作"电"而非"油"。

混动系统 是全场最佳------它内部同时组合了 FuelEngine 和 ElectricMotor,在 consume 函数里实现了策略调度:

c 复制代码
static float hybridConsume(void* self, float distance)
{
    HybridSystem* h = (HybridSystem*)self;

    /* 策略:优先用电 */
    float elecDist = h->electric.chargeLevel / (elecPerKm);
    if (elecDist >= distance) {
        /* 纯电就够 */
        h->electric.chargeLevel -= consumed;
    } else {
        /* 用完电,剩下的烧油 */
        h->electric.chargeLevel = 0;
        h->fuel.fuelLevel -= fuelNeeded;
    }
}

注意:HybridSystem 没有继承 FuelEngine 也不继承 ElectricMotor------它组合两者、按策略委托。这就是"组合优于继承"原则的完美示范,优雅避开了 C++ 里让人头疼的菱形继承问题。

多态的统一入口

不管什么车,最终都通过同一个 carDrive 函数来行驶:

c 复制代码
void carDrive(Car* c, float distance)
{
    PowerSource* ps = &c->powerSource;
    float consumed = ps->ops.consume(ps->self, distance);  /* ★ 多态分发在此 */
    c->totalMileage += distance;
}

carDrive 不关心 ps->self 指向的是 FuelEngine 还是 ElectricMotor------它只认 PowerSource 接口。具体消耗汽油还是电力,完全由子类在初始化时注入的 vtable 决定。

跑起来看看

三辆车(宝马 530Li 燃油、特斯拉 Model 3 电动、比亚迪秦L 混动)各行驶 300km,输出如下:

diff 复制代码
--- 宝马 530Li (燃油车) ---
  行驶 300km, 消耗 21.0 汽油, 剩余续航 671km

--- 特斯拉 Model 3 (纯电车) ---
  行驶 300km, 消耗 45.0 电力, 剩余续航 200km

--- 比亚迪 秦L (混动车: 优先用电) ---
  用电 15.8kWh + 烧油 13.6L
  行驶 300km, 消耗 29.4 油电混合, 剩余续航 734km

同一个 carDrive,三种截然不同的消耗行为------这就是多态。


五、总结

用一张表回顾 C 语言映射 OOP 的四大手法:

OOP 概念 C 语言实现手法 本项目体现
封装 不透明结构体 + 访问器函数 Car 的字段通过 carInit/carGetSpeed 等函数操作
继承 基类作为子类第一个字段 + 指针强转 FuelCar { Car base; } → (Car*)&fuelCar
多态 虚函数表(函数指针结构体) ps->ops.consume → 三种引擎各实现各的
接口 纯函数指针表,不含数据 PowerSource 定义 5 个行为契约

掌握了这四种手法,你会发现 C 语言也能写出结构清晰、易于扩展的代码。工厂模式、策略模式、观察者模式------这些经典设计模式背后,无非是继承、组合、多态的排列组合。

想看完整代码和实际效果?

bash 复制代码
git clone https://gitcode.com/qingshan1206/EmbedStory.git
cd cobject
cmake -B build && cmake --build build
./build/app/carDemo

自己动手跑一遍,理解会更透彻。


相关推荐
blueSatchel19 小时前
UVC驱动源码研究
linux·嵌入式
haliu1 天前
【FHE 同态加密】我们如何实现同态加密推理(十五):实测账本——一跳 1.8 小时,钱花在哪(同态加密推理性能剖析)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理
一直C2 天前
【ARM嵌入式开发】LCD液晶屏显示原理、色彩模式与显存地址映射详解
lcd·嵌入式·arm·寄存器·cortex-a7
Hespethorn2 天前
洪泛、路由、网络栈:LoRa 自组网三条路线的设计取舍与量化对比
物联网·嵌入式
用户0510122572962 天前
RGA(二)——RK3588 的核拓扑与能力差异
linux·嵌入式
用户0510122572962 天前
RGA(一)——基础知识
linux·嵌入式
用户0510122572962 天前
RGA(三)——实宽高、虚宽高与对齐约束
linux·嵌入式
燕卫博2 天前
SSD201 BSP 开发教程
嵌入式·bsp·ssd201
一直C2 天前
【嵌入式 ARM 驱动开发】PWM 脉宽调制原理与 GT9147 电容触控屏驱动开发详解
嵌入式硬件·lcd·嵌入式·arm汇编·imx6ull·cortex-a7
haliu2 天前
【FHE 同态加密】我们如何实现同态加密推理(十四):为什么 `RESULT=PASS` 不是判据(纯 C11 · 零依赖)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理