尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
嵌入式C语言面向对象编程:封装继承多态的C实现与串口驱动实战
上一篇讲轮询、回调、观察者把底层变化通知应用层的线理清了。这篇往更根本的地方挖一步嵌入式用 C 写为什么绕不开面向对象做嵌入式开发时间久了你一定在技术论坛和公众号里反复看到面向对象这个词。有人纳闷嵌入式主要用 C 语言C 又不是面向对象语言谈这个干嘛这个疑问很正常。我刚入行那几年写单片机程序一个 main.c 从头写到尾GPIO 初始化、定时器配置、中断处理、通信协议、业务逻辑全堆在一起。代码量不大的时候自己写的自己维护倒也不觉得有什么问题。但嵌入式项目的复杂度这些年在肉眼可见地增长。十年前一个典型的 MCU 项目可能就几千行代码现在呢一个中等规模的嵌入式产品代码量轻松过万行要对接各类传感器支持 OTA 升级底下跑着 RTOS还得兼顾不同硬件平台的适配。代码量上去了如果还是所有东西堆一起的写法几个现实问题就冒出来改一个模块的代码莫名其妙影响了另一个模块换一颗芯片应用层代码跟着大面积修改新同事接手项目看代码看得头皮发麻多人协作时频繁出现代码冲突。面向对象思想说白了就是一套组织代码的方法论。它不要求你用 C 或者 Java它提供的是一种思路把数据和操作数据的方法打包在一起通过清晰的接口对外提供服务隐藏内部实现细节。C 没有 class 关键字但这套思路完全能用 C 实现而且嵌入式里用得比你想的普遍得多。封装、继承、多态在嵌入式里到底是什么面向对象三个核心概念在 C 里都有对应的手法一个一个说。封装把复杂性藏起来。把相关的数据和操作放在一起只暴露必要的接口内部细节外面不需要知道也不应该知道。在 C 语言中封装靠 .h 文件暴露接口、.c 文件隐藏实现来完成。最干净的手法是不透明指针opaque pointer。头文件里只前向声明结构体不放定义/* serial_device.h - 只暴露接口结构体定义藏起来 */ typedef struct serial_device serial_device_t; /* 前向声明外部看不到成员 */ int serial_init(serial_device_t *dev, uint32_t baudrate); int serial_send(serial_device_t *dev, const uint8_t *data, uint16_t len); void serial_deinit(serial_device_t *dev);/* serial_device.c - 结构体定义在这里外部文件拿不到成员 */ struct serial_device { const char *name; const serial_ops_t *ops; void *priv; /* 私有数据 */ };外部代码只能拿到serial_device_t *指针没法dev-name直接访问成员编译器直接报错因为成员定义在 .c 里它看不见。这就是 C 的封装比 C 的 private 还硬是物理隔离不是约定隔离。继承复用已有的东西。C 语言没有语法直接支持继承但核心思想能在 C 中体现让一个子类型复用父类型的数据结构和行为。最常见的方法是结构体嵌套把基类结构体作为子类结构体的第一个成员。/* 基类所有设备共有的属性 */ typedef struct { const char *name; int id; } device_base_t; /* 子类串口设备把基类放第一个成员 */ typedef struct { device_base_t base; /* 必须是第一个成员 */ uint32_t baudrate; uint8_t parity; } serial_dev_t; serial_dev_t uart1; uart1.base.name uart1; /* 通过 base 访问基类成员 */ uart1.baudrate 115200;为什么必须是第一个成员因为 C 标准保证结构体首成员的地址等于结构体本身的地址。这样serial_dev_t *和device_base_t *能安全互转传给期望基类指针的函数时直接强转就行。这是 Linux 内核container_of宏的基础也是 C 实现继承的底层依据。多态同一接口不同实现。多态是面向对象中最有价值的部分。在 C 语言中多态靠函数指针实现。把函数指针放在结构体里不同的子类在初始化时把各自的实现函数赋给这些函数指针。typedef struct { int (*init)(serial_device_t *dev, uint32_t baudrate); int (*send)(serial_device_t *dev, const uint8_t *data, uint16_t len); int (*recv)(serial_device_t *dev, uint8_t *buf, uint16_t len); void (*deinit)(serial_device_t *dev); } serial_ops_t;serial_ops_t就是一组函数指针相当于一张方法表。STM32 的串口填一套实现ESP32 的串口填另一套应用层调dev-ops-send(dev, ...)时具体跑哪套代码由dev指向的实例决定。这就是多态同一调用不同行为。把三个概念归纳成一句话封装管藏继承管复用多态管切换。三者配合使用才能发挥面向对象的真正威力。面向对象在嵌入式里到底好在哪第一硬件抽象换芯片不用重写应用层。把硬件相关的操作封装在底层应用层只通过统一的接口调用。换芯片时只改底层驱动实现应用层一行不动。这是产品线多、硬件版本多的项目最看重的。第二代码复用同类设备共用一套框架。为同类设备定义统一的接口和通用逻辑只有真正不同的部分才各自实现。比如所有串口共用serial_send的框架逻辑只有寄存器操作那几行因芯片而异。第三可维护性代码能看懂能改动。模块之间通过接口隔离每个模块职责单一。改一个模块不用担心牵连别的因为别的模块只依赖你的接口不依赖你的实现。第四团队协作接口定好各写各的。项目开始时先把模块划分好、接口定义好各模块并行开发。A 写串口驱动B 写业务逻辑只要接口对得上互不阻塞。这一条在大团队里价值尤其大。实战用 C 写一个串口设备驱动以串口设备驱动为例先看传统写法直接操作寄存器每路串口复制粘贴一套函数uart1_send、uart2_send、uart3_send各写一遍换芯片又复制一遍。再看面向对象写法把上面三个概念全用上。// serial_device.h - 抽象接口 typedef struct serial_device serial_device_t; typedef struct { int (*init)(serial_device_t *dev, uint32_t baudrate); int (*send)(serial_device_t *dev, const uint8_t *data, uint16_t len); int (*recv)(serial_device_t *dev, uint8_t *buf, uint16_t len); void (*deinit)(serial_device_t *dev); } serial_ops_t; struct serial_device { const char *name; const serial_ops_t *ops; void *priv; // 私有数据 }; // 对外统一接口 int serial_init(serial_device_t *dev, uint32_t baudrate); int serial_send(serial_device_t *dev, const uint8_t *data, uint16_t len); void serial_deinit(serial_device_t *dev);// stm32_uart.c - 具体硬件驱动 typedef struct { USART_TypeDef *instance; uint32_t clk_freq; } stm32_uart_priv_t; static int stm32_uart_init(serial_device_t *dev, uint32_t baudrate) { stm32_uart_priv_t *priv (stm32_uart_priv_t *)dev-priv; priv-instance-BRR priv-clk_freq / baudrate; priv-instance-CR1 USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; return 0; } static int stm32_uart_send(serial_device_t *dev, const uint8_t *data, uint16_t len) { stm32_uart_priv_t *priv (stm32_uart_priv_t *)dev-priv; for (uint16_t i 0; i len; i) { while (!(priv-instance-SR USART_SR_TXE)); priv-instance-DR data[i]; } return len; } static const serial_ops_t stm32_uart_ops { .init stm32_uart_init, .send stm32_uart_send, .recv NULL, .deinit NULL, }; // 实例化具体设备 static stm32_uart_priv_t uart1_priv { .instance USART1, .clk_freq 72000000 }; static stm32_uart_priv_t uart2_priv { .instance USART2, .clk_freq 36000000 }; serial_device_t uart1_dev { .name uart1, .ops stm32_uart_ops, .priv uart1_priv }; serial_device_t uart2_dev { .name uart2, .ops stm32_uart_ops, .priv uart2_priv };应用层使用时不管底层是哪路串口调用方式完全一样serial_init(uart1_dev, 115200); serial_send(uart1_dev, msg, sizeof(msg));这段代码里封装、继承、多态三个要素全体现了封装硬件寄存器操作被隐藏在 stm32_uart.c 内部外部只看到serial_send继承serial_device_t作为基类priv指针挂载各芯片的私有数据多态serial_send通过函数指针调用具体实现STM32 和 ESP32 各填一套ops。这里有个设计点值得单独说priv指针。抽象层serial_device_t不认识具体芯片的寄存器但它留了个void *priv让具体驱动把私有数据挂上去。stm32_uart_priv_t里放 USART 实例和时钟频率ESP32 的 priv 里放 UART 端口号和配置。抽象层只管透传priv具体驱动自己强转回自己的类型。这个手法让抽象层和具体实现彻底解耦是 C 面向对象里最常用的一招。为什么不直接用 C聊到这肯定有人问既然要面向对象为什么不直接用 C嵌入式又不是不能跑 C。能跑但有代价。第一C 的运行时特性异常、RTTI、虚函数表会吃 Flash 和 RAM资源紧张的 MCU 扛不住。第二C 编译器对嵌入式平台支持参差不齐有些 MCU 的工具链 C 支持很弱。第三团队里 C 的功底普遍比 C 扎实用 C 写面向对象门槛低人人能维护。所以嵌入式的主流还是 C但用 C 的语法实现面向对象的思想。这套手法不是奇技淫巧是 Linux 内核、FreeRTOS、LVGL 这些大项目都在用的成熟实践。你不用它不是它不好是你还没遇到痛点。初学者该怎么学面向对象思维第一步先把 C 语言基础打扎实。结构体、函数指针、typedef、.h/.c 分离、static 关键字这些是面向对象的砖头砖头不齐盖不起来。第二步从封装开始练。把你项目里某个模块用 .h/.c 分离重构头文件只放函数声明结构体定义藏 .c 里。这是最立竿见影的一步重构完模块立刻清爽。第三步再学函数指针做多态。用函数指针替换 switch-case 分支体会同一接口不同实现的好处。上一篇讲的观察者、这篇的 ops 表都是这一步的应用。第四步最后才学继承和设计模式。结构体嵌套做继承工厂、观察者、策略这些模式。到这一步你已经能看懂 Linux 内核和 LVGL 的源码架构了。这四步别跳。很多人一上来就啃设计模式结果连封装都没练熟模式用得生硬反而把代码搞复杂。面向对象是手段不是目的目的是把代码组织得能维护、能扩展、能协作手段用错了适得其反。有用的话点个在看让更多还在一个 main.c 写到底的嵌入式工程师看到。标签嵌入式 C语言 面向对象 封装继承多态 函数指针
RELATED

相关推荐

快速排序与桶排序:核心原理、实战优化与场景选择指南

快速排序与桶排序:核心原理、实战优化与场景选择指南

1. 项目概述:从理论到实战的排序算法精讲在软件开发的日常里,排序算法就像厨师的刀工,是处理数据这道大餐的基本功。无论是处理海量用户数据,还是优化内存中的缓存顺序,一个高效的排序算法往往能带来性能上的质变。今天…

📅 2026/10/7 15:59:18
AI编程工具本地化部署:从Cursor汉化到Ollama集成实战

AI编程工具本地化部署:从Cursor汉化到Ollama集成实战

在实际 AI 开发和应用领域,模型迭代、工具更新和生态变化的速度远超传统软件。对于开发者而言,仅仅关注模型参数和榜单排名是不够的,更重要的是理解这些变化背后的技术趋势、对现有工作流的影响,以及如何将新能力安全、高效地集成…

📅 2026/10/8 8:45:04
AUTOSAR CanNm网络管理深度剖析|全域协同唤醒休眠与PN局部网络精准控耗、助力新能源整车低功耗量产、解决驻车亏电误唤醒难题、搭载全量状态机工程代码

AUTOSAR CanNm网络管理深度剖析|全域协同唤醒休眠与PN局部网络精准控耗、助力新能源整车低功耗量产、解决驻车亏电误唤醒难题、搭载全量状态机工程代码

目录 一、前言 二、AUTOSAR CanNm核心概念与车载量产价值 2.1 CanNm通俗核心释义 2.2 CanNm两大核心基础能力 2.3 CanNm配套BSW分层协作体系 2.4 车载量产不可替代核心价值 三、CanNm标准报文协议与PN局部网络核心机制 3.1 标准NM心跳报文结构与字段释义 3.2 传统全局…

📅 2026/9/7 10:38:00
MORE NEWS

更多资讯

📰

英文版虚拟机安装实战:VMware+Win7/Ubuntu语言与排错指南

提到“英文版虚拟机安装”,很多人下意识理解成“装一个英文版VMware”。这个理解只对了一半。实际工作里,这三个字至少能拆出三种完全不同的需求:软件本身界面用英文、虚拟机里的操作系统用英文,以及整个交付环境(包括…

📰

麒麟桌面系统输入法状态自动切换详解:窗口记忆与程序绑定配置指南

最近有朋友问我,在麒麟桌面系统上做日常开发办公,到底怎么才能让输入法别那么“傻”——从 IDE 切到聊天窗口,还是英文状态,打完一串中文才发现没切换;从聊天窗口切回终端,又困在中文状态下,“&…

📰

LangChain4j实战:Java工程师如何快速接入大模型应用开发

说实话,我后台收到这个“项目标题”的时候,第一反应是:兄弟,你手滑了吧,LangcChain4J?正确的拼写是 LangChain4j。但转念一想,这个拼写错误反而很真实——很多Java开发第一次听到这个框架名&…

📰

基于Spring+Vue的校园勤工俭学平台毕业设计实战指南

校园勤工俭学这类题目在计算机毕业设计里一直属于“稳中带卷”的类型——需求清晰、业务闭环完整、技术栈可以自由选择深浅,而基于springvue来做,更是这几年最主流的组合。不少同学拿到“基于springvue的校园勤工俭学平台[spring]-计算机毕业设计源码LW文…

📰

IDEA暂存和Git stash分不清?一文搞懂操作与区别

平时写代码最怕遇到什么?不是需求改来改去,而是你正改到一半,产品经理突然跑过来说:“先别写了,线上有个紧急Bug,你马上切分支修一下。”这时候你手头那堆半成品代码怎么办?提交吧,c…

📰

Java Web网址导航系统:纯JSP+Servlet实战项目

简介:这是一套基于Java开发的开源网址导航网站完整项目源码,面向计算机相关专业学生及初级开发者,适用于课程设计、大作业、项目实战与毕设参考。资源包含可直接运行的后端Java代码、前端HTML/JS/CSS页面、数据库SQL脚本及配套说明文档&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬