找回密码
 立即注册
首页 业界区 业界 C语言一点五编程实战:纯 C 的模块化×继承×多 ...

C语言一点五编程实战:纯 C 的模块化×继承×多态框架

甘子萱 2025-6-2 23:38:48
本文将大量涉及C语言高级操作,如函数指针、结构体指针、二级指针、指针频繁引用解引用、typedef、static、inline和C语言项目结构等知识,请确保自己不会被上述知识冲击,如果没有这顾虑,请尽情享受~
摘要:

本文探讨在C语言中模拟面向对象编程(OOP)的"一点五编程"技术,通过函数指针、结构体嵌套和二级指针强制转换实现类、接口与多态。开发流程分声明(接口/类结构体、类型转换函数)、实现(方法绑定、初始化)和使用三阶段,强调方法集指针必须位于类结构体首地址以实现动态绑定。该方法将硬件驱动与业务逻辑解耦,结合嵌入式场景展示模块化设计,附伪实现循迹小车项目验证继承特性,为C语言赋予OOP的封装性、扩展性,提升嵌入式代码可维护性。
渊源

一开始时候,我是不知道这个技术的。在某一天我在刷B站的时候,看到一个作者为“一点五编程”的视频。他提出了一种编程思想,命名为“一点五编程”。其中:
"一"指的是模块化思想
“点五”指的是(*p)->f(p)技巧
我一看,好像是一种高端的技巧哇,于是开始看他的视频,发现讲解这一技术核心的视频全都是充电的!!!好吧,那我只好翻你文档看了,找到了他的个人博客。唔,好像什么都写了,但好像少点什么,,,哦,没教我到底怎么组织文件。
然后我继续翻网页,在CSDN上发现三篇文章,讲的是对“一点五编程”的解读。但是后来在自己实操过程中,还是发现了其中的错误。
看了这么多文章和视频,脑子一拍,这不就是面向对象的编程范式吗,只不过C语言是面向过程的语言,没有现成的面向对象的组件,但是思想上完全就是OOP那套嘛!
于是我开始自己扒,终于,也是让我扒出来了~
在文章的最后我会放上一个循迹小车的项目,当然,功能上伪实现(狗头),那接下来先讲下这门技术的基本理论和开发流程吧
理论

面向对象编程

我们先来说一下面向对象编程是什么:
面向对象编程是一种编程范式,它通过定义类和对象来组织和设计程序。
在面向对象编程中,程序猿通过创建类来定义数据结构和行为,通过创建对象来实例化这些类,并通过对象之间的交互来实现程序的功能。这种方法使得程序的结构更加清晰和易于维护。
面向对象编程有几个特性,分别是:封装、继承、多态、抽象,这里就不再说了,只要知道本文会体现就行,(纠结)因为毕竟还是挺难理解的,我也讲不明白,可以看看别的大佬的文章。
为什么要把面向对象编程拉出来说呢?众所众知,嵌入式是一个软硬件结合的学科,这就会存在一个问题,就是我们会非常在乎硬件的实现,上层的功能实现就实现了,也不会在乎开发的结构、后期的维护等,这一点在初学者身上体现的淋漓尽致。
而面向对象编程就致力于让程序更加模块化,通过继承和多态,使得大量代码复用,它还有模拟现实世界中对象和关系的能力。这样,就为开发者提供了一种自顶向下的开发思路。同时,它将上层实现与下层驱动相隔离,让更换开发平台变得简单。
流程

我将整个C语言面向对象编程的开发分为三个阶段,分别是声明、实现和使用。
声明

声明阶段又可以分为五个步骤,这些都是在头文件中写入的,分别是:

  • 声明接口函数
  • 定义接口结构体
  • 定义类结构体
  • 定义类型转换内联函数
  • 声明方法实例
    其中,声明接口函数、定义接口结构体和定义类型转换内联函数仅需书写一次,另外两个步骤可以根据实际需求定义更多的类和方法实例。
声明接口函数

在这里,接口就是类的行为方法集,控制整个类的行为方式。以循迹模块为例,读取循迹信息就是它的一个行为;以电机驱动模块为例,控制电机停转、正转、反转和控制转动速度就是它的一系列行为。我们首先要思考我们所抽象出的类有哪些行为方法,写成下面形式:
  1. typedef int (*Method0FnT)(void* self, ...);
  2. typedef int (*Method1FnT)(void* self, ...);
  3.   .
  4.   .
  5.   .
  6. typedef int (*MethodnFnT)(void* self, ...);
复制代码
定义接口结构体

接下来,我们要将上面的接口函数放到一个接口结构体中,方便由各个类使用:
  1. typedef struct
  2. {
  3.         Method0FnT method0Fn;
  4.         Method1FnT method1Fn;
  5.           .
  6.           .
  7.           .
  8.         MethodnFT methodnFn;
  9. } MethodsT;
复制代码
定义类结构体

完成上面步骤后,一个类的方法集就总结好了,再由方法集和类的各个属性组成完整的类,这里一定要注意,方法集指针一定要放在类结构体的第一个,否则会出现错误:
  1. typedef struct
  2. {
  3.         MethodsT* methods;
  4.         Type attribute0;
  5.         Type attribute1;
  6.           .
  7.           .
  8.           .
  9.         Type attributen;
  10. } Class;
复制代码
定义类型转换内联函数

这里是我们实现多态这一特性最核心的步骤,写成如下格式:
  1. static inline int method0Fn(void* self, ...)
  2. {
  3.         return (*(MethodsT**)self)->method0Fn(self, ...);
  4. }
  5. static inline int method1Fn(void* self, ...)
  6. {
  7.         return (*(MethodsT**)self)->method1Fn(self, ...);
  8. }
  9.   .
  10.   .
  11.   .
  12. static inline int methodnFn(void* self, ...)
  13. {
  14.         return (*(MethodsT**)self)->methodnFn(self, ...);
  15. }
复制代码
使用上面的语句,我们能够将(*p)->f(p)改写为f(p)的形式,而且不需要管类的具体函数实现。这里我们将指向类的一级指针强制类型转换为指向接口的二级指针,再解引用就得到了一个仅指向接口的一级指针,再用成员访问符使用接口函数。
这个过程中,要将指向类的一级指针强制类型转换为指向接口的二级指针,就需要类的起始地址与接口的起始地址相同,也就是为什么上面说方法集的指针一定要放在类结构体的第一个,这样指向接口的二级指针解引用后才会指向接口。
1.png

声明方法实例

上面定义了抽象的接口和类,该到这个接口函数的具体实现了,当然,还要写上类初始化函数的声明:
  1. int classMethod0(void* self, ...);
  2. int classMethod1(void* self, ...);
  3.   .
  4.   .
  5.   .
  6. int classMethodn(void* self, ...);
  7. int classInit(void* self, ...);
复制代码
实现

头文件内容就完成了,下面是具体的相关函数的实现了,下面部分都在源文件中写入,分为三个步骤:

  • 定义方法实例
  • 定义接口实例
  • 定义类初始化函数
    三个步骤的内容均由头文件中的声明限制。
定义方法实例

方法的实例我们已经在头文件中声明过了,在这里我们进行这些方法实例的定义:
  1. int classMethod0(void* self, ...);
  2. {
  3.         Class* pClass= (Class*)self;
  4.         //具体内容实现
  5.         //异常处理
  6.         return 1;
  7. }
  8. int classMethod1(void* self, ...)
  9. {
  10.         Class* pClass= (Class*)self;
  11.         ...
  12.         return 1;
  13. }
  14.   .
  15.   .
  16.   .
  17. int classMethodn(void* self, ...)
  18. {
  19.         Class* pClass= (Class*)self;
  20.         ...
  21.         return 1;
  22. }
复制代码
定义接口实例

具体的方法已经有了,接下来我们要实现具体的接口了,将方法实例的函数指针传入到接口结构体中:
  1. static MethodsT classMethods=
  2. {
  3.         .method0Fn= classMethod0,
  4.         .method1Fn= classMethod1,
  5.           .
  6.           .
  7.           .
  8.         .methodnFn= classMethodn
  9. }
复制代码
定义类的初始化函数

最后我们编写所需类的初始化的函数,类的属性值将通过初始化函数传入:
  1. int classInit(void* self, ...)
  2. {
  3.         Class* pClass= (Class*)self;
  4.        
  5.         pClass->methods= &classMethods;
  6.         pClass->attribute0= ...;
  7.         pClass->attribute1= ...;
  8.           .
  9.           .
  10.           .
  11.         pClass->attributen= ...;
  12.         //其他初始化内容
  13.         //异常处理
  14.         return 1;
  15. }
复制代码
使用

使用起来就简单了,首先我们要生成类的实例,然后使用初始化函数进行类初始化,然后,使用!
  1. //生成实例可以放在main.h中或者主函数前或者主函数开头
  2. Class class
  3. //初始化要放在开头
  4. classInit(&class, ...);
  5. //使用方法就放在任何你需要其执行的地方即可
  6. method0Fn(&class, ...);
  7. method1Fn(&class, ...);
  8.   .
  9.   .
  10.   .
  11. methodnFn(&class, ...);
复制代码
就这样,我们的所有功能就实现啦,我相信你一定学会了!(狗头)
附录

循迹小车的伪实现,会体现出上面没有提及的继承特性,见本仓库:
https://github.com/swfeiyu/coop
2.png

关于我们更多介绍可以查看云文档:Freak嵌入式工作室云文档,或者访问我们的wiki:https://github.com/leezisheng/Doc/wik
3.png


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册