用C语言实现面向对象(第二版)
上个月闲的无聊凑空闲时间写了个C语言面向对象,原文链接:
https://www.bilibili.com/read/cv11292041
专栏发布后自己也重新看了下,功能很不完善,重新看了遍代码发现还有很多BUG。由于暑假时间比较多,就重新把这框架修了一遍,修复了一些BUG以及更新了很多实用的功能模块。
下面进入正题,本文包括三个部分,基本功能模块、实用功能模块介绍以及功能演示。
1. 基本功能模块:
(1) 宏函数new()的实现
(2) this指针介绍
(3) 对象调用宏函数$()的实现
(4) 宏函数instanceof()的实现
(5) 创建自己的类
(6) GC实现
(7) 宏函数GC_INIT()介绍
2. 实用功能模块:
(1) List对象的使用
(2) Map对象的使用
(3) String对象的使用
(4) JsonObject对象的使用
基本功能模块
该部分介绍了面向对象相关功能基本实现思路,以及如何使用,使用时需要注意哪些事项。
(1) 宏函数new()

上面的代码中通过调用new()创建了一个List对象,调用过程中执行了以下几个步骤:
1.内存分配,使用malloc()函数在堆区分配空间
2.计算内存对齐,GC提示创建对象,分配了多少字节空间
3.更新对象地址范围,更新已分配的堆内存地址范围
4.调用该类构造函数
5.将this指针设置为当前
代码实现:

上面第3步需要解释下,对象地址范围和堆内存地址范围的作用。众所周知在C语言中,free()函数如果传入的指针不是堆区内存指针,free()函数就会报错。为了解决这个问题,在free()之前必须判断传入的指针是不是堆区的指针,通过定义堆内存地址范围,每次调用malloc()时记录下分配过的最大地址和最小地址,那么只要是在这个范围内的,一定就是堆区内存指针。由于堆区内存指针有两种情况:对象和数据,所以使用对象地址范围来判断该指针是不是对象。
(2) this指针


上面的代码是String类的equals()函数实现,this指针用于在函数中访问当前引用的对象,如果不使用this指针的话,在使用对象调用equals()函数时就必须将引用作为参数传进去,手感很差,比如:$(s).equals(s,"hello"); 这样写起来十分不爽。
注意下上面代码中的String对象并没有使用new()创建,是因为我为了优化手感,在String对象的创建上参照了《JVAV》,可以直接将字符串指针赋值给String对象,在对象调用时会自动创建String对象,并赋初值,下面在讲对象调用宏函数$()时会详细说明。
关于this指针的线程安全问题,因为this指针是共享的全局变量,使用不当会导致线程安全问题。首先GC线程里所有的对象引用都是不使用this指针的,所以GC线程不会影响this指针,关于实际使用时产生的线程安全问题,需要注意:
在函数实现时,函数中的第一条语句必须是Object temp=(Object) this;用于在栈空间中保存当前this指针,后续调用就使用temp对象。
(3) 对象调用宏函数$()


上面是对象调用的两种方式,其实本质上都是调用宏函数 $(n) 去引用对象,不过第二种方式打起来更加方便,如果一个对象使用频率比较高,推荐使用第二种方式。第一种方式 $(n).func();类似于jQuery,手感还行。
$(n)代码实现:

上面是对象调用宏函数$(n)的实现,调用过程中执行了以下几个步骤:
1.将this指针指向当前对象
2.如果当前对象不是对象而是字符串指针,则将其转换为String对象
3.当前对象计数值清零
4.当前对象引用次数加一
其中第3和第4步与GC相关,由于GC根据超时时间和引用次数来回收对象,所以当对象产生引用时,需要将计数值清零,防止被GC回收
(4) 宏函数instanceof()的实现

这里的宏函数instanceof()是仿照《JVAV》中的instanceof关键字实现的,功能与其一致,它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
代码实现:
实现原理十分简单,由于每个对象有一个基类Object,instanceof()通过将其强转为Object对象后,获取它的attr.type属性即可知道该对象是哪个类的实例。
注意上图中的$String定义在DataType.h中,每一个类都必须定义用于判断对象类型,例如:
#define $JsonObject 9
下面是两个与instanceof()功能相近的函数:
GetObjectType(void* obj);
GetObjectTypeName(void* obj);

(5) 创建自己的类

上图为类的基本结构,包括结构体,结构体指针,有参构造函数和无参构造函数。
结构体:名字必须是下划线+类名,在new()调用时会根据这个名字创建对应的临时变量。结构体中第一个元素必须是基类Object对象,用于强制类型转换和基本对象属性的存储。最后是你自己的属性和函数。
结构体指针:名字必须是类名,用于存储当前对象
有参构造函数和无参构造函数:名字必须是Const+下划线+类名+(ARGS), 在new()时会根据名字来调用对应的构造函数,两个构造函数必须都要声明,否则new()中将会找不到构造函数导致异常。

在构造函数中,必须调用Object_init(void*,int,boolean);对基类Object对象进行初始化。第一个参数是当前对象,第二个参数是当前对象类型,第三个参数是是否可以被GC回收。其次,在构造函数中还需要对函数指针进行赋值,否则函数将无法被正确调用。
关于类的创建只有这么多,十分简单,上图的TEST类就是一个模板,直接复制后把参数名修改修改就能用了。
(6) GC实现
关于GC,我放弃了之前的直接使用计数法实现方式,转而采用引用次数权重+引用周期权重+优先级的方法实现。再次简述下基本实现原理。
在系统运行的过程中,对象的引用可以大体分为一下三类:
对象被高频周期性引用,如定时器,while(1)循环中调用等
对象创建时高频引用,然后再也不引用,只有偶尔会产生几次引用,如中断事件,标志位触发等
对象引用周期长的周期性引用,大部分时间在等待,只有很少时间被调用
由于有上述三种情况,单纯的采用计数法会导致长引用周期的对象优先级低,容易被误回收,偶尔产生引用的对象更容易被回收,因此采用计数法实现是不科学的。为了解决这个问题,我必须动态的去适配每个对象的引用周期,如果采用固定超时时间,一旦对象超时就会被回收,这样误回收的概率极高,为了简化问题,我将上述三种情况融合为一种情况。
在系统开机后,对象被创建出来,并高频调用其函数和属性以初始化系统,初始化结束后,对象开始进入周期性引用,周期性引用持续了1天后,对象的引用开始进入随机触发,非周期性调用。这种情况下,要动态的适配对象在不同环境下的引用周期,计数法就不再适用了。由于对象一开始会产生高频调用,说明该阶段的引用周期短,引用次数多,总引用次数会被拉的很高,如果采用平均值引用周期的话,第二阶段的周期性引用就很难把周期值拉大,也就是易受到高频影响。此时采用权重法,记录下平均引用周期和最大引用周期值,根据总引用次数在平均周期下需要执行的的次数与最大周期下需要执行的次数的占比来控制输出的引用周期。计算出权重,比值越大说明周期接近平均值居多,反之周期长且频率低居多,这样产生的结果就能动态的跟随趋势,趋向于短周期时间或长周期时间,在周期时间不断变化时,产生的权重也会随着趋势动态的更新。

定义A 为在一段时间T内,用最短周期可以产生多少次调用
定义B为 在一段时间T内,用最长周期可以产生多少次调用
A由于周期短,所以频率高,A的值越大
B由于周期长,所以频率低,B的值越小
同理,实际调用次数C如果越大,则说明短周期占比高,反之长周期占比高
如果C在(A+B)中的占比越大,说明这一段时间里对象是被高频引用的,则A的权重越大
如果C在(A+B)中的占比越小,说明这一段时间里对象是被低频引用的,则B的权重越大
例如一段时间T内实际调用次数为30,A为100,B为3,则占比为30/103=0.291,说明短周期调用可能是突发的,随机的,而非周期性的
例如一段时间T内实际调用次数为300,A为350,B为3,则占比为300/353=0.849,说明短周期调用很可能是周期性的,而长周期调用可能是delay()等函数后产生的调用
其中A,B,C,T都是随时间不断累积的,最长和最短周期也会动态更新,随着总时间的不断加长,实际调用次数和A+B之间的占比就越精确。
假设系统运行了3600秒,总调用次数100,A为3600,B为60,则权重为0.027
假设系统运行了3600000秒,总调用次数100,A为3600000,B为60000,则权重为0.000027
目前GC再加上语法检测功能就完善了,根据C11新特性的__FILE__和__LINE__属性获取文件名和行数,如果同文件同一行存在同一类型的new()创建对象的话,说明很可能是在循环中调用的new(),如果循环跳到下一轮,且上一轮的对象没有存在容器中,则上一轮的对象将会失去引用,而语法检测就是需要规避这一点,直接清除上一轮对象。由于时间紧张,就得放到3.0实现了。
GC代码实现:


当然GC如果无法满足需求的话,可以选择不用,我还提供了手动内存管理函数:
下面是GC引用周期调试视频:

可以看到在第11秒第一次按下回车触发对象一次调用时,对象的最长引用周期被更新为11秒,第二次按下回车触发对象200ms周期循环调用时,对象的引用周期逐渐趋近于实际周期值200ms
(7) 宏函数GC_INIT()介绍

上图为宏函数GC_INIT()实现,初始化过程中主要执行如下几个功能:
获取常量区起始地址,用于识别常量字符串指针
计算内存对齐系数和最小分配内存大小
适配stm32f1的内存对齐系数
开启GC线程
注意GC_INIT()必须放在main()函数开头位置,以正确获取常量区起始地址和堆内存起始地址。
实用功能模块
(1) List对象的使用

这里的List对象时仿照JVAV中的List实现的,可以看到除了基本的增删功能以外,该List的存储类型是任意类型,图中的List中分别存放了char*,int和JsonObject三种不同类型,但其本质上都是void*,由于该框架支持指针类型识别,所以在调用$(l1).print()时可以根据数据的真实类型输出结果,List中其他函数功能与名字均与JVAV中一样。
(2) Map对象的使用

可以看到Map与List一样,都是仿照JVAV的格式实现的,除了基本的增删功能以外,Map的数据存储类型是char*->void*,也就是Key是字符串,Value是任意类型,与List一样,可以自动识别对象类型后处理。上图为使用Map对象展示一个简单的Map嵌套。
(3) String对象的使用

上图是String对象的两种创建方法,第一种是直接将字符串指针赋值给String对象,第二种则是采用祖传的new()法进行对象创建。这两种方法都能正常创建String对象,不同的是,第一种对象在引用前是没有创建出String对象的(没有调用new()函数),第二种则是直接调用new()创建。不过这两种方式创建的String对象在使用上没有任何区别,推荐使用第一种方式。
下面是String类中几个实用的函数:
乍一看还以为在打JVAV,没错,String的也是仿照JVAV的String实现的。下面就演示几个核心功能,如图:



(4) JsonObject的使用
首先说明只实现了JsonObject,没有实现JsonArray,由于时间紧还得忙别的事,此功能和GC语法检测,以及性能优化,内存池等功能就得放在3.0版本了。先看效果图吧。

运行结果:



上面代码中使用了游戏背包结构做了一个简单的测试,可以正常生成Json数据,Json数据的存储格式也是字符串:任意数据类型,可以自动识别数据类型后生成对应的Json字符串,可以实现嵌套Json对象的Json字符串生成以及解析。
写不动了,已经从凌晨1点写到7点了,中途4点的时候被b站专栏坑了,没保存成功重写到现在。
总结一下,大家千万不要闲的没事干去用C语言仿照JVAV实现面向对象,C++他不香吗,开了一个坑真的流着泪都得填完,千万别乱入坑。
工程代码像以往一样直接放出来:
https://pan.baidu.com/s/1t7_bECM5CIJdBwE0a60Dcw
提取码:ALYA
Clion打开别人的工程会导致路径报错,参考我Clion开发Stm32这篇最后的教程就行https://www.bilibili.com/read/cv11442303