本文将大量涉及C语言高级操作,如函数指针、结构体指针、二级指针、指针频繁引用解引用、typedef、static、inline和C语言项目结构等知识,请确保自己不会被上述知识冲击,如果没有这顾虑,请尽情享受~
摘要:
本文探讨在C语言中模拟面向对象编程(OOP)的"一点五编程"技术,通过函数指针、结构体嵌套和二级指针强制转换实现类、接口与多态。开发流程分声明(接口/类结构体、类型转换函数)、实现(方法绑定、初始化)和使用三阶段,强调方法集指针必须位于类结构体首地址以实现动态绑定。该方法将硬件驱动与业务逻辑解耦,结合嵌入式场景展示模块化设计,附伪实现循迹小车项目验证继承特性,为C语言赋予OOP的封装性、扩展性,提升嵌入式代码可维护性。
渊源
一开始时候,我是不知道这个技术的。在某一天我在刷B站的时候,看到一个作者为"一点五编程"的视频。他提出了一种编程思想,命名为"一点五编程"。其中:
"一"指的是模块化思想
"点五"指的是(*p)->f(p)技巧
我一看,好像是一种高端的技巧哇,于是开始看他的视频,发现讲解这一技术核心的视频全都是充电的!!!好吧,那我只好翻你文档看了,找到了他的个人博客。唔,好像什么都写了,但好像少点什么,,,哦,没教我到底怎么组织文件。
然后我继续翻网页,在CSDN上发现三篇文章,讲的是对"一点五编程"的解读。但是后来在自己实操过程中,还是发现了其中的错误。
看了这么多文章和视频,脑子一拍,这不就是面向对象的编程范式吗,只不过C语言是面向过程的语言,没有现成的面向对象的组件,但是思想上完全就是OOP那套嘛!
于是我开始自己扒,终于,也是让我扒出来了~
在文章的最后我会放上一个循迹小车的项目,当然,功能上伪实现(狗头),那接下来先讲下这门技术的基本理论和开发流程吧
理论
面向对象编程
我们先来说一下面向对象编程是什么:
面向对象编程是一种编程范式,它通过定义类和对象来组织和设计程序。
在面向对象编程中,程序猿通过创建类来定义数据结构和行为,通过创建对象来实例化这些类,并通过对象之间的交互来实现程序的功能。这种方法使得程序的结构更加清晰和易于维护。
面向对象编程有几个特性,分别是:封装、继承、多态、抽象,这里就不再说了,只要知道本文会体现就行,(纠结)因为毕竟还是挺难理解的,我也讲不明白,可以看看别的大佬的文章。
为什么要把面向对象编程拉出来说呢?众所众知,嵌入式是一个软硬件结合的学科,这就会存在一个问题,就是我们会非常在乎硬件的实现,上层的功能实现就实现了,也不会在乎开发的结构、后期的维护等,这一点在初学者身上体现的淋漓尽致。
而面向对象编程就致力于让程序更加模块化,通过继承和多态,使得大量代码复用,它还有模拟现实世界中对象和关系的能力。这样,就为开发者提供了一种自顶向下的开发思路。同时,它将上层实现与下层驱动相隔离,让更换开发平台变得简单。
流程
我将整个C语言面向对象编程的开发分为三个阶段,分别是声明、实现和使用。
声明
声明阶段又可以分为五个步骤,这些都是在头文件中写入的,分别是:
- 声明接口函数
- 定义接口结构体
- 定义类结构体
- 定义类型转换内联函数
- 声明方法实例
其中,声明接口函数、定义接口结构体和定义类型转换内联函数仅需书写一次,另外两个步骤可以根据实际需求定义更多的类和方法实例。
声明接口函数
在这里,接口就是类的行为方法集,控制整个类的行为方式。以循迹模块为例,读取循迹信息就是它的一个行为;以电机驱动模块为例,控制电机停转、正转、反转和控制转动速度就是它的一系列行为。我们首先要思考我们所抽象出的类有哪些行为方法,写成下面形式:
C
typedef int (*Method0FnT)(void* self, ...);
typedef int (*Method1FnT)(void* self, ...);
.
.
.
typedef int (*MethodnFnT)(void* self, ...);
定义接口结构体
接下来,我们要将上面的接口函数放到一个接口结构体中,方便由各个类使用:
C
typedef struct
{
Method0FnT method0Fn;
Method1FnT method1Fn;
.
.
.
MethodnFT methodnFn;
} MethodsT;
定义类结构体
完成上面步骤后,一个类的方法集就总结好了,再由方法集和类的各个属性组成完整的类,这里一定要注意,方法集指针一定要放在类结构体的第一个,否则会出现错误:
C
typedef struct
{
MethodsT* methods;
Type attribute0;
Type attribute1;
.
.
.
Type attributen;
} Class;
定义类型转换内联函数
这里是我们实现多态这一特性最核心的步骤,写成如下格式:
C
static inline int method0Fn(void* self, ...)
{
return (*(MethodsT**)self)->method0Fn(self, ...);
}
static inline int method1Fn(void* self, ...)
{
return (*(MethodsT**)self)->method1Fn(self, ...);
}
.
.
.
static inline int methodnFn(void* self, ...)
{
return (*(MethodsT**)self)->methodnFn(self, ...);
}
使用上面的语句,我们能够将(*p)->f(p)
改写为f(p)
的形式,而且不需要管类的具体函数实现。这里我们将指向类的一级指针强制类型转换为指向接口的二级指针,再解引用就得到了一个仅指向接口的一级指针,再用成员访问符使用接口函数。
这个过程中,要将指向类的一级指针强制类型转换为指向接口的二级指针,就需要类的起始地址与接口的起始地址相同,也就是为什么上面说方法集的指针一定要放在类结构体的第一个,这样指向接口的二级指针解引用后才会指向接口。
声明方法实例
上面定义了抽象的接口和类,该到这个接口函数的具体实现了,当然,还要写上类初始化函数的声明:
C
int classMethod0(void* self, ...);
int classMethod1(void* self, ...);
.
.
.
int classMethodn(void* self, ...);
int classInit(void* self, ...);
实现
头文件内容就完成了,下面是具体的相关函数的实现了,下面部分都在源文件中写入,分为三个步骤:
- 定义方法实例
- 定义接口实例
- 定义类初始化函数
三个步骤的内容均由头文件中的声明限制。
定义方法实例
方法的实例我们已经在头文件中声明过了,在这里我们进行这些方法实例的定义:
C
int classMethod0(void* self, ...);
{
Class* pClass= (Class*)self;
//具体内容实现
//异常处理
return 1;
}
int classMethod1(void* self, ...)
{
Class* pClass= (Class*)self;
...
return 1;
}
.
.
.
int classMethodn(void* self, ...)
{
Class* pClass= (Class*)self;
...
return 1;
}
定义接口实例
具体的方法已经有了,接下来我们要实现具体的接口了,将方法实例的函数指针传入到接口结构体中:
C
static MethodsT classMethods=
{
.method0Fn= classMethod0,
.method1Fn= classMethod1,
.
.
.
.methodnFn= classMethodn
}
定义类的初始化函数
最后我们编写所需类的初始化的函数,类的属性值将通过初始化函数传入:
C
int classInit(void* self, ...)
{
Class* pClass= (Class*)self;
pClass->methods= &classMethods;
pClass->attribute0= ...;
pClass->attribute1= ...;
.
.
.
pClass->attributen= ...;
//其他初始化内容
//异常处理
return 1;
}
使用
使用起来就简单了,首先我们要生成类的实例,然后使用初始化函数进行类初始化,然后,使用!
C
//生成实例可以放在main.h中或者主函数前或者主函数开头
Class class
//初始化要放在开头
classInit(&class, ...);
//使用方法就放在任何你需要其执行的地方即可
method0Fn(&class, ...);
method1Fn(&class, ...);
.
.
.
methodnFn(&class, ...);
就这样,我们的所有功能就实现啦,我相信你一定学会了!(狗头)
附录
循迹小车的伪实现,会体现出上面没有提及的继承特性,见本仓库:
https://github.com/swfeiyu/coop
关于我们更多介绍可以查看云文档:Freak嵌入式工作室云文档,或者访问我们的wiki:https://github.com/leezisheng/Doc/wik
