当前位置: 首页 > news >正文

用 C 语言模拟面向对象编程

作为现代的 C++ 开发者,我们每天都在享受类、继承、虚函数和多态带来的便利。Qt 框架和C++更是将面向对象(OOP)和元对象系统发挥到了极致。但你是否曾想过,在完全没有语言级别 OOP 支持的 C 语言 世界里,如何构建出具有多态性(Polymorphism)封装性(Encapsulation) 的优雅设计?

这是系统级编程、驱动开发和嵌入式领域中一种强大且极其常见的实践。今天,我们就来揭秘这种“用 C 语言模拟面向对象编程”的经典写法,看看它如何为庞大而复杂的世界奠定坚实的基础。

一、核心思想:万物皆可结构体

C++ 编译器在背后为vptrvtable做了大量工作。而在 C 中,我们需要手动模拟这一机制。其核心思想可以概括为:

  1. 结构体存储函数指针:用结构体(struct)来模拟“类”,其成员不仅包含数据,更包含函数指针。这些函数指针定义了类的“方法”。
  2. this指针显式传递:将结构体自身的指针作为每个“方法”的第一个参数显式传入,模拟 C++ 的 this 指针。
  3. 私有数据封装:在结构体内放一个 void *priv_data 指针,指向一个不透明的内部数据结构,从而实现封装信息隐藏

二、代码解剖:一个安全设备接口的诞生

让我们通过一个具体的例子来感受这种设计。假设我们要定义一个安全设备的抽象接口。

1. 定义接口(“抽象基类”)

在头文件 safety_device.h 中,我们定义我们的“抽象类”。

// safety_device.h
#ifndef SAFETY_DEVICE_H
#define SAFETY_DEVICE_H#include <stdint.h>
#include <stdbool.h>// 前向声明,模拟一个类(Class)
typedef struct _SafetyDevice SafetyDevice;// 方法签名定义,所有函数第一个参数都是 "this" 指针
struct _SafetyDevice {//--- 数据输入方法(模拟纯虚函数)---uint32_t (*GetConfigurableInputs)(SafetyDevice *self);bool (*GetRobotEmergencyStop)(SafetyDevice *self);//--- 数据输出方法 ---void (*SetConfigurableOutputs)(SafetyDevice *self, uint32_t bits);bool (*PowerOnRobot)(SafetyDevice *self);//--- 其他方法 ---bool (*IsFaultOccurred)(SafetyDevice *self);//--- "私有" 数据指针,实现封装 ---void *priv_data;
};// 模拟的“构造函数”(需要由具体实现来提供)
// SafetyDevice* SafetyDevice_Create(...);// 模拟的“析构函数”
// void SafetyDevice_Destroy(SafetyDevice** self);#endif // SAFETY_DEVICE_H

这个 SafetyDevice 结构体现在就是一个合约接口。任何想要扮演“安全设备”角色的模块,都必须提供这些函数的具体实现。

2. 实现具体类(“派生类”)

现在,我们来为一个基于 PLC 的设备实现这个接口。我们在 .c 文件中实现封装

// plc_safety_device.c
#include "safety_device.h"
#include <stdlib.h> // for malloc/free// --- 真正的私有数据结构,对外完全隐藏!实现了封装。---
typedef struct {int io_port_address;uint32_t last_known_inputs;// 其他PLC特有的数据...
} PLCPrivateData;// --- 具体函数实现,即“方法”的实现 ---
static uint32_t PLC_GetConfigurableInputs(SafetyDevice *self) {// 1. 将void*转换回我们具体的私有数据类型PLCPrivateData *priv = (PLCPrivateData *)(self->priv_data);// 2. 模拟通过硬件端口读取输入信号// uint32_t inputs = inport(priv->io_port_address);// priv->last_known_inputs = inputs;// 3. 返回结果(这里用假数据模拟)return priv->last_known_inputs;
}static bool PLC_GetRobotEmergencyStop(SafetyDevice *self) {PLCPrivateData *priv = (PLCPrivateData *)(self->priv_data);// 检查第0位是否为1(急停信号)return (priv->last_known_inputs & 0x01) != 0;
}// ... 实现其他函数(SetConfigurableOutputs, PowerOnRobot等)...// --- “构造函数” ---
SafetyDevice* PLC_SafetyDevice_Create(int io_port) {// 分配“对象”内存SafetyDevice *device = (SafetyDevice *)malloc(sizeof(SafetyDevice));if (!device) return NULL;// 分配并初始化“私有”数据PLCPrivateData *priv_data = (PLCPrivateData *)malloc(sizeof(PLCPrivateData));if (!priv_data) {free(device);return NULL;}priv_data->io_port_address = io_port;priv_data->last_known_inputs = 0;// 初始化“方法”(函数指针),指向我们具体的实现device->GetConfigurableInputs = PLC_GetConfigurableInputs;device->GetRobotEmergencyStop = PLC_GetRobotEmergencyStop;device->SetConfigurableOutputs = NULL; // 暂未实现device->PowerOnRobot = NULL;           // 暂未实现device->IsFaultOccurred = NULL;// 关联私有数据device->priv_data = priv_data;return device;
}// --- “析构函数” ---
void PLC_SafetyDevice_Destroy(SafetyDevice **self) {if (self && *self) {free((*self)->priv_data); // 先释放私有数据free(*self);              // 再释放对象本身*self = NULL;             // 避免野指针}
}

3. 享受多态

最后,在应用程序中,我们可以编写只依赖于抽象接口 SafetyDevice 的代码,而无需关心背后是 PLC、TCP 还是模拟设备。

// main.c
#include "safety_device.h"void SafetyRoutine(SafetyDevice *device) {// 这是一个多态函数!它不知道 device 的具体类型。// 1. 检查急停信号if (device->GetRobotEmergencyStop(device)) {// 2. 急停触发,执行断电操作if (device->PowerOnRobot) { // 检查方法是否已实现device->PowerOnRobot(device);}printf("Emergency Stop Activated!\n");}// 3. 获取并打印输入状态uint32_t inputs = device->GetConfigurableInputs(device);printf("Current inputs: 0x%X\n", inputs);
}int main() {// 创建一个“具体派生类”的对象SafetyDevice *plcDevice = PLC_SafetyDevice_Create(0x378);if (plcDevice) {// 享受多态的魅力!SafetyRoutine(plcDevice);// 清理PLC_SafetyDevice_Destroy(&plcDevice);}// 未来可以轻松添加 TCP_SafetyDevice_Create(...);// 而 SafetyRoutine 函数无需任何改动!return 0;
}

三、对比 C++ 与这种 C 模式

特性C++ (语言级别支持)C (模拟实现)
类(Class)class MyClass { ... };typedef struct { ... } MyClass;
方法(Method)类内部的成员函数结构体内部的函数指针
this 指针编译器隐式传递 this显式作为第一个参数传递 self
继承class Derived : public Base结构体内嵌,或函数指针表嵌套
多态通过 virtual 关键字和虚函数表手动初始化不同的函数指针
封装private:/public: 访问限定符使用不完整的类型和 void *priv_data
析构自动调用的析构函数手动调用析构函数指针或清理函数

四、总结:为什么这种模式如此重要?

  1. 设计模式的体现:这是策略模式桥接模式依赖倒置原则(DIP)的完美体现。高层模块依赖于抽象接口(SafetyDevice),而非具体实现(PLC_SafetyDevice),极大地降低了模块间的耦合度。
  2. 无与伦比的灵活性:允许在运行时动态改变设备的行为(通过更换函数指针),甚至实现热插拔功能。
  3. 嵌入式领域的基石:在资源受限、拒绝 C++ 复杂性的环境中,这是构建大型、可维护、可扩展系统架构的核心技术。Linux 内核驱动、各种硬件抽象层(HAL)都广泛采用这种模式。
  4. 加深对 OOP 的理解:通过手动模拟,你能更深刻地理解 C++ 编译器在背后为你所做的工作,真正明白虚函数表(vtable)和虚函数指针(vptr)的原理。
http://www.dtcms.com/a/393097.html

相关文章:

  • 联邦学习论文分享:FedKTL
  • 智能体分类:从反应式到混合式的架构演进与实践
  • 【面板数据】上市公司企业ZF连接度数据集(1991-2024年)
  • 让codex像 cladue code一样 自动牛马
  • NeurIPS 2025 spotlight Autonomous Driving VLA World Model FSDrive
  • 多线程JUC
  • Qwen3技术之模型后训练
  • 服务端实现
  • 深入AQS源码:解密Condition的await与signal
  • ceph存储配置大全
  • 数据库造神计划第十六天---索引(1)
  • 【软件推荐】免费图片视频管理工具,让灵感库告别混乱
  • C语言入门教程 | 阶段二:循环语句详解(while、do...while、for)
  • GEO(Generative Engine Optimization)完全指南:从原理到实践
  • Msyql日期时间总结
  • IP地址入门基础
  • 【ROS2】Beginner: CLI tools
  • LeetCode刷题记录----279.完全平方数(Medium)
  • H7-TOOL的250M示波器模组采集CANFD标准波形效果,开口逻辑0,闭口逻辑1
  • 打工人日报#20250920
  • 详解C/C++内存管理
  • SSM(springboot部分)
  • C++ std:string和Qt的QString有哪些差异?
  • FunASR开源项目实战:解锁语音识别新姿势
  • (华为杯)数学建模比赛编程助手
  • 通义千问对postgresql wire协议的连接和执行SQL过程的解释
  • 钣金折弯机被远程锁机了怎么办
  • 基于陌讯AIGC检测算法的高性能部署实践:FastAPI与多进程并发设计详解
  • 群晖 NAS 远程访问痛点解决:神卓 N600 公网 IP 盒实战体验
  • JavaWeb之HttpServletRequest与HttpServletResponse详解及快递管理系统实践