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

发布时间:2026/7/12 5:20:30
懂了汽车,就懂了 C 语言面向对象编程 用汽车这个每个人都熟悉的例子彻底搞懂 C 语言怎么写 OOP。可以微 信来关注青衫嵌入式会持续发布嵌入式干货。一、为什么 C 语言也需要面向对象做嵌入式或系统软件开发的同学都有过这种体验项目一开始几十个全局变量、十几个函数结构清晰一目了然。随着需求叠加全局变量膨胀到几百个函数散落在十几个文件里改一个参数要追着七八个文件跑整个项目像一张越织越密的蜘蛛网——碰一下整张网都在抖。这就是没有结构的 C 代码到了一定规模后必然出现的复杂性爆炸。面向对象编程OOP本质上是一种组织代码的思维方式——把数据和对数据的操作绑定在一起对外隐藏实现细节对内保持职责单一。它不依赖任何语言特性Java 有class关键字C 有class关键字但 C 语言没有——可这并不妨碍我们用 C 写出漂亮的面向对象代码。拿汽车这个任何人都熟悉的例子——我们一步步拆解看看 C 语言如何模拟面向对象的四大核心概念封装、继承、多态、接口。源码地址https://gitcode.com/qingshan1206/EmbedStory/tree/main/cobject类继承关系燃油车子类继承汽车基类包含燃油引擎组件实例化宝马530Li纯电车子类继承汽车基类包含电动马达组件实例化特斯拉Model3混动车子类继承汽车基类包含混动系统组件实例化比亚迪秦L其中混动系统组件是由燃油引擎电动马达两个组件整合而成二、封装你的数据只有你的方法能碰痛点传统 C 开发中结构体往头文件一扔谁都能直接car.speed -100来一手——改错了排查半天。解决方案封装的核心思想是“外部使用者不关心你的数据长什么样只关心你能做什么”。在 C 语言里这通过不透明结构体 操作函数实现/* car.h —— 对外只暴露函数接口 */typedefstructCarCar;/* 前置声明外部不知道 Car 里面有什么 */voidcarInit(Car*c,constchar*brand,constchar*model);voidcarDrive(Car*c,floatdistance);floatcarGetSpeed(constCar*c);floatcarGetMileage(constCar*c);使用者只能通过carInit、carDrive这些方法来操作 Car 对象——至于 Car 内部的 speed、mileage 字段怎么存的藏起来了你碰不到。这跟 C 里class的public方法完全是一个思路。注本项目中因为子类继承需要见下一节struct Car的定义实际放在头文件中——但通过后文要讲的向上转型宏使用层面仍然保持了封装性。三、继承把基类嵌在子类的头上概念燃油车是一辆车纯电车是一辆车混动车也是一辆车。三者共享品牌、型号、速度、里程这些通用属性但各自又有独特的动力系统。OOP 用继承来表达这种is-a关系——子类自动获得父类的所有属性和方法再叠加自己的特性。C 语言的实现手法在 C 语言中模拟继承只有一个标准手段把基类作为子类结构体的第一个字段。/* fuelCar.h —— 燃油车继承 Car 组合 FuelEngine */typedefstructFuelCar{Car base;/* ★ 第一个字段 继承 Caris-a */FuelEngine engine;/* 普通成员 拥有发动机has-a */}FuelCar;C 标准ISO C11 第 6.7.2.1 节第 15 条明确保证结构体的第一个成员地址等于结构体自身的地址。因此(Car*)fuelCar永远安全且零运行时开销——编译器在编译期就能搞定。子类初始化的三步曲以燃油车为例它的初始化遵循一个固定流程voidfuelCarInit(FuelCar*fc,constchar*brand,constchar*model,floattankCap,floatfuelCons){/* 步骤 1: 调用基类的构造函数 */carInit(fc-base,brand,model);/* 步骤 2: 初始化自己的组件 */fuelEngineInit(fc-engine,tankCap,fuelCons);/* 步骤 3: 将组件的接口注入基类关键 */PowerSource*pscarGetPowerSource(fc-base);*psfuelEngineGetInterface(fc-engine);}第一步搞定通用属性品牌、型号第二步初始化特有的油箱和油耗数据第三步把发动机的能力注册到基类——后续基类的carDrive方法就能通过这个接口调用到燃油发动机的具体消耗逻辑。锦上添花向上转型宏如果你觉得每次调用carDrive((Car*)bmw5, 300)还要手动强转太啰嗦——我们写了一个宏自动搞定#definecarDrive(p,dist)_carDrive((Car*)(p),(dist))#definecarGetSpeed(p)_carGetSpeed((constCar*)(p))#definecarGetMileage(p)_carGetMileage((constCar*)(p))现在直接写carDrive(bmw5, 300)就行宏在背后帮你完成向上转型。四、多态与接口同一个操作不同的行为 —— 这才是灵魂如果说封装和继承是规矩那多态就是灵魂——它让同一个函数调用在不同对象上表现出不同行为。回到汽车同样是行驶 300 公里燃油车要烧油纯电车要耗电混动车要先用电、电不够再烧油。调用方不想知道这些细节——它只想说开然后各自各的方式去开。核心手段虚函数表vtable在 C 里虚函数表是编译器帮你生成的。在 C 语言里我们手动构造它——用函数指针结构体/* PowerSource —— 动力源接口一组纯虚函数的集合 */typedefstructPowerSourceOps{void(*refill)(void*self,floatamount);/* 补充能量加油/充电 */float(*consume)(void*self,floatdistance);/* 消耗能量行驶 */float(*getRemaining)(void*self);/* 查询剩余能量 */float(*getRange)(void*self);/* 查询剩余续航 */constchar*(*getType)(void*self);/* 能量类型名 */}PowerSourceOps;typedefstructPowerSource{void*self;/* 指向实际引擎数据多态的关键void* 的 this 指针 */PowerSourceOps ops;/* 虚函数表 */}PowerSource;这个PowerSource就是一个接口——它只定义契约不包含实现。任何实现了这五个函数的结构体都可以伪装成一个 PowerSource。三种实现者燃油发动机填充了烧油逻辑PowerSourcefuelEngineGetInterface(FuelEngine*e){PowerSource ps;ps.selfe;ps.ops.consumefuelEngineConsume;/* 按百公里油耗计算 */ps.ops.getRemainingfuelEngineGetRemaining;ps.ops.getTypefuelEngineGetType;/* 返回 汽油 *//* ... */returnps;}电机填充了耗电逻辑——结构完全镜像只是操作电而非油。混动系统是全场最佳——它内部同时组合了FuelEngine和ElectricMotor在consume函数里实现了策略调度staticfloathybridConsume(void*self,floatdistance){HybridSystem*h(HybridSystem*)self;/* 策略优先用电 */floatelecDisth-electric.chargeLevel/(elecPerKm);if(elecDistdistance){/* 纯电就够 */h-electric.chargeLevel-consumed;}else{/* 用完电剩下的烧油 */h-electric.chargeLevel0;h-fuel.fuelLevel-fuelNeeded;}}注意HybridSystem没有继承FuelEngine 也不继承 ElectricMotor——它组合两者、按策略委托。这就是组合优于继承原则的完美示范优雅避开了 C 里让人头疼的菱形继承问题。多态的统一入口不管什么车最终都通过同一个carDrive函数来行驶voidcarDrive(Car*c,floatdistance){PowerSource*psc-powerSource;floatconsumedps-ops.consume(ps-self,distance);/* ★ 多态分发在此 */c-totalMileagedistance;}carDrive不关心ps-self指向的是 FuelEngine 还是 ElectricMotor——它只认PowerSource接口。具体消耗汽油还是电力完全由子类在初始化时注入的 vtable 决定。跑起来看看三辆车宝马 530Li 燃油、特斯拉 Model 3 电动、比亚迪秦L 混动各行驶 300km输出如下--- 宝马 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 语言也能写出结构清晰、易于扩展的代码。工厂模式、策略模式、观察者模式——这些经典设计模式背后无非是继承、组合、多态的排列组合。想看完整代码和实际效果gitclone https://gitcode.com/qingshan1206/EmbedStory.gitcdcobject cmake-Bbuildcmake--buildbuild ./build/app/carDemo自己动手跑一遍理解会更透彻。