OOPC开发从未如此简单!

2023-10-22 18:30
文章标签 简单 开发 从未 oopc

本文主要是介绍OOPC开发从未如此简单!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【说在前面的话】


“为什么要使用C语言来实现面向对象开发?”

“直接用C++不就好了么?”

想必很多人在第一次面对 OOPCObject-Oriented-Programming-with-ANSI-C)的时候,都会情不自禁的发出类似的疑问。其实,任何针对上述问题的讨论,其本身都是充满争议的——换句话说,无论我给出怎样的答案,都无法令所有人满意——正因如此,本文也无意去趟这摊浑水。

我写这篇文章的目的是为那些长期在MDK环境下从事C语言开发的朋友介绍一种方法:帮助大家在偶尔需要用到“面向对象”概念的时候简便快捷的使用C语言“搞定”面向对象开发

在开始后续内容之前,我们需要约定和强调一些基本原则:

  • “零消耗”原则:即,我们所要实现的所有面向对象的特性都应该是“零资源消耗”或至少是“极小资源消耗”。这里的原理是:能在编译时刻(Compiletime)搞定的事情,绝不拖到运行时刻(Runtime)

  • 务实原则:即,我们不在形式上追求与C++类似,除非它的代价是零或者非常小。

  • “按需实现”原则:即,对任何类的实现来说,我们并不追求把所有的OO特性都实现出来——这完全没有必要——我们仅根据实际应用的需求来实现最小的、必要的面向对象技术

  • “傻瓜化”原则:即,类的建立和使用都必须足够傻瓜化。最好所见即所得。

在上述前提下,我们就快速进入到今天的内容吧。

【仅需一次的准备阶段】


首先,我们要下载 PLOOC的 CMSIS-Pack,具体链接如下:

https://raw.githubusercontent.com/GorgonMeducer/PLOOC/master/cmsis-pack/GorgonMeducer.PLOOC.4.6.0.pack

当然,如果你因为某些原因无法访问Github,也可以在关注【裸机思维】公众号后发送关键字 “PLOOC” 来获取网盘链接。

下载成功后,直接双击安装包即可。

26d71d9d5e6cab22304ff90b8198149e.png


一般来说,部署会非常顺利,但如果出现了安装错误,比如下面这种:

735f6c723ce42fe45a2e713f101619a2.png

则很可能是您所使用的MDK版本太低导致的——是时候更新下MDK啦。关注【裸机思维】公众号后发送关键字"MDK",即可获得最新的MDK网盘链接。


PLOOC 是 Protected-Low-overhead-Object-Oriented-programming-with-ansi-C 的缩写,顾名思义,是一个强调地资源消耗且为私有类成员提供保护的一个面向对象模板。

它是一个开源项目,如果你喜欢,还请多多Star哦!

https://github.com/GorgonMeducer/PLOOC

f85b63cd1b616dcab49dc6782953270d.png

【如何快速尝鲜】


为了简化用户对 OOC 的学习成本,PLOOC提供了一个无需任何硬件就可以直接仿真执行的例子工程。该例子工程以队列类为例子,展示了:

  • 类的定义方式

  • 如何实现类的方法(Method)

  • 如何为类定义接口(Interface)

  • 如何定义派生类

  • 如何重载接口

  • 如何在派生类中访问基类受保护的成员(Protected Member)

  • ……

很多时候千言万语敌不过代码几行——学习OOC确是如此。

例子工程的获取非常简单。首先打开 Pack-Installer,在Device列表中找到Arm,选择任意一款Cortex-M内核(比如 Arm Cortex-M3)。在列表中选择ARMCMx(比如下图中的ARMCM3)。

3367ae76e35b219a1bee45ede87c2648.png

此时,在右边的Example选项卡中,就可以看到最底部出现了一个名为 plooc_example (uVision Simulator)的例子工程。单击Copy,在弹出窗口中选择一个目录位置来保存工程:

139a588295084385966200dac082c46c.png

单击OK后将打开自动打开如下所示的 MDK 界面:

118bc34242473132e6cce96c2c0e8baf.png

直接单击编译,如果一切顺利,应该没有任何编译错误:

1ae2d71d9c7635c093fb45193cf3273c.png

此时,我们可以直接进入调试模式:

bcb4ec80d069b2d943b4d46a56b22d56.png

可以看到,调试指针停在了 main() 函数的起始位置。我们先不着急开始全速运行。通过菜单打开 "Debug (printf) Viewer" 窗口:

df0d9894cd634e09585b53c4cc0db098.png

一开始该窗口会出现在屏幕下方的窗体中,通过拖动的方式,我们可以将其挪到醒目的位置。此时,全速运行就可以看到例子工程所要展示的效果了:

a1b213d202c92540a347306db18428ff.png

该例子只展示了C99模式下使用PLOOC所构建的队列类(enhanced_byte_queue_t)的效果:

static enhanced_byte_queue_t s_tQueue;printf("Hello PLOOC!\r\n\r\n");do {static uint8_t s_chQueueBuffer[QUEUE_BUFFER_SIZE];const enhanced_byte_queue_cfg_t tCFG = {s_chQueueBuffer,sizeof(s_chQueueBuffer),};ENHANCED_BYTE_QUEUE.Init(&s_tQueue, (enhanced_byte_queue_cfg_t *)&tCFG);} while(0);//! you can enqueueENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'p');ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'L');ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'O');ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'O');ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'C');ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');//! you can dequeuedo {uint_fast16_t n = ENHANCED_BYTE_QUEUE.Count(&s_tQueue);uint8_t chByte;printf("There are %d byte in the queue!\r\n", n);printf("let's peek!\r\n");while(ENHANCED_BYTE_QUEUE.Peek.PeekByte(&s_tQueue, &chByte)) {printf("%c\r\n", chByte);}printf("There are %d byte(s) in the queue!\r\n", ENHANCED_BYTE_QUEUE.Count(&s_tQueue));printf("Let's remove all peeked byte(s) from queue... \r\n");ENHANCED_BYTE_QUEUE.Peek.GetAllPeeked(&s_tQueue);printf("Now there are %d byte(s) in the queue!\r\n", ENHANCED_BYTE_QUEUE.Count(&s_tQueue));} while(0);

其输出为:

03ab4eae07e082411cd4f0db18c382e2.png

enhanced_byte_queue_t 实际上是从基类 byte_queue_t 基础上派生出来的,并添加了一个非常有用的功能:可以连续的偷看(Peek)队列里的内容,并可以在需要的时候,要么1)将已经偷看的内容实际都取出来;要么2)从头开始偷看——上述代码就展示了这一功能。

PLOOC 相较普通的OOC模板来说,除了可以隐藏类的私有成员(private member)以外,还能够以零运行时成本实现多肽(Polymorphism)——用通俗的话说就是:PLOOC允许拥有不同参数数量、不同参数类型的多个函数拥有相同的名字。

要获得这样的功能,就要打开 C11(最好是GNU11)的支持。当我们打开工程配置,在“C/C++”选项卡中将 Language C 设置为 c11(最好是gnu11):

07a7a24d23ebcf70fc2e0d94fb40a0f0.png

重新编译后,进入调试模式,将在输出窗口中看到额外的信息:

93ed6393afad35a01d1c87ae5f0688ba.png

这些信息实际上对应如下的代码:

#if defined(__STDC_VERSION__) && __STDC_VERSION__ > 199901LLOG_OUT("\r\n-[Demo of overload]------------------------------\r\n");LOG_OUT((uint32_t) 0x12345678);LOG_OUT("\r\n");LOG_OUT(0x12345678);LOG_OUT("\r\n");LOG_OUT("PI is ");LOG_OUT(3.1415926f);LOG_OUT("\r\n");LOG_OUT("\r\nShow BYTE Array:\r\n");LOG_OUT((uint8_t *)main, 100);LOG_OUT("\r\nShow Half-WORD Array:\r\n");LOG_OUT((uint16_t *)(((intptr_t)&main) & ~0x1), 100/sizeof(uint16_t));LOG_OUT("\r\nShow WORD Array:\r\n");LOG_OUT((uint32_t *)(((intptr_t)&main) & ~0x3), 100/sizeof(uint32_t));
#endif

你看,同一个函数 LOG_OUT() 当我们给它不同数量和类型的参数时,居然可以实现不同的输出效果,是不是特别神奇——这就是面向对象开发中多态的魅力所在。请记住:

  • 此时我们仍然使用的是C语言,而不是C++

  • 在C99下,我们可以实现拥有不同参数个数的函数共享同一个名字;

  • 在C11下,我们可以实现拥有相同参数个数但类型不同的函数共享同一个名字;

  • 我们在运行时刻的开销是0,一切在编译时刻就已经尘埃落定了。我们并没有为这项特性牺牲任何代码空间。

例子工程可以帮助我们快速的熟悉 OOC 的开发模式,那么在任意的普通工程中,我们要如何使用 PLOOC模板呢?

【PLOOC在任意普通工程中的部署】


PLOOC 模板其实是一套头文件,既没有库(lib)也没有C语言源代码,更别提汇编了。

在任意的MDK工程中,只要你已经安装了此前我们提到过的CMSIS-Pack,就可以通过下述工具栏中标注的按钮,打开RTE配置界面:

a2109020e67aef658225e9e85c3b9cbe.png

找到 Language Extension选项,将其展开后勾选PLOOC,单击OK关闭窗口。

0ab9455e441dace95313590ddefbc734.png

此时,我们就可以在工程管理器中看到一个新的列表项“Language Extension”:

92243e3c81da8665700e64a291cf2e80.png

它是不可展开的,别担心,这就足够了。打开工程配置,如果你使用的是 Arm Compiler 6(armclang)

6f20efe31ce4914b0f0664b742a19521.png

则我们需要在 C/C++选项中:

  • 将Language C设置为 gnu11(或者最低c99):

  • (推荐,而不是必须)在Misc Controls中添加对微软扩展的支持,并在 Define中添加一个宏定义 _MSC_VER

-fms-extensions

581c43a6c1c2db1a34b8f1ef5834b208.png

如果你使用的是 Arm Compiler 5(armcc)

65b70684d83d7f1b25932828f87cf2a6.png

则需要在 C/C++ 选项卡中开启对 GNU Extension 和 C99的支持:

c121ef0d157324b3db186a504f569ee6.png

遗憾的是作为一款已经停止更新的编译器,Arm Compiler 5 既不支持C11,也不支持微软扩展(-fms-extensions),这意味着PLOOC中的多态特性无法发挥最大潜能,着实有点遗憾(但拥有不同参数数量的函数还是允许共享同一个名称的)。

至此,我们就完成了PLOOC在一个工程中的部署。是不是特别简单?

5e1d35eb3bf69ec4d6a22c7291cf913a.jpeg

也许文章到了一半我才问,已经有点迟了——大家都熟悉基本的面向对象概念吧?比如:

  • 类(class)

  • 私有成员(private member)

  • 公共成员(public member)

  • 保护成员(protected member)

  • 构造函数(constructor)

  • 析构函数(destructor)

  • 类的方法(method)

  • ……

如果不熟悉,还请找本C#或者C++的书略微学习一下为好。后面的内容,我将假设你已经对面向对象的基本开发要素较为熟悉。

那么,我们如何快速的在C语言工程中构建一个类呢?

【新建一个类从未如此简单】


假设我们要创造一个新的类,叫做 my_class1

第一步:引入模板

在工程管理器中,添加一个新的group,命名为 my_class1

19c084bfd8ba32eba44cb18baea14551.png

右键单击 my_class1,并在弹出的菜单中选择 "Add New Item to Group my_class1":

9809d1d90b8741665b8e9526bd756155.png

在弹出的对话框中选择 User Code Template:

5a44005f7cf04c8df397976bf7a878a3.png

展开 Language Extension,可以看到有两个 PLOOC模板,分别对应:

  • 基类和普通类(Base Class Template)

  • 派生类(Derived Class Template)

由于我们要创建的是一个普通类(未来也可以作为基类),因此选择“Base Class Template”。单击Location右边的 "..." 按钮,选择一个保存代码文件的路径后,单击“Add”。

此时我们可以看到,class_name.c 被添加到了 my_class1中,且MDK自动在编辑器中为我们打开了两个模板文件:class_name.hclass_name.c

afbf5dca4d596ee52c3176401214c533.png

第二步:格式化

在编辑器中打开或者选中 class_name.c。通过快捷键CTRL+H打开 替换窗口:

  • 在Look in中选择Current Document

  • 去掉Find Opitons属性框中的 Match whold word前的勾选这一步骤很重要

7a02f7780ee2d1fd8489c1786518b7ef.png

接下来,依次:

  • 将小写的 <class_name> 替换为 my_class1

  • 将大写的 <CLASS_NAME> 替换为 MY_CLASS1

完成上述步骤后,保存class_name.c

打开class_name.h,重复上述过程,即:

  • 将小写的 <class_name> 替换为 my_class1

  • 将大写的 <CLASS_NAME> 替换为 MY_CLASS1

完成后保存 class_name.h.

第三步:加入工程编译

在工程管理器中展开 my_class1,并将其中的 class_name.c 删除:

52372302478421199b046814678a153d.png

打开class_name.c 所在文件目录:

e0d1153d03784bdcf2e3f20b63148aed.png

找到我们刚刚编辑好的两个文件 class_name.cclass_name.h

f2750d2d03a897f27b53eea087260bf8.png

用我们的类为这两个文件命名:my_class1.cmy_class1.h

28c6f23465f9d0e1a038fc97df8001a3.png

在MDK工程管理器中,将这两个文件加入 my_class1 下:

f79b1b730922c17cafb3a75edeaec616.png

如果此前你的工程就是可以正常编译的话,在加入了上述文件后,应该依然可以正常编译:

a8db4443bea04878ab28c36344ea859f.png

第四步:如何设计你的类成员变量

打开 my_class1.h,找到 def_class 所在的代码片断:

//! \name class my_class1_t
//! @{
declare_class(my_class1_t)def_class(my_class1_t,public_member(//!< place your public members here)private_member(//!< place your private members here)protected_member(//!< place your private members here)
)end_def_class(my_class1_t) /* do not remove this for forward compatibility  */
//! @}


很容易注意到:

  • 类所对应的类型会自动在尾部添加 "_t" 以表示这是一个自定义类型,当然这不是强制的,当你熟悉模板后,如果确实看它不顺眼,可以改成任何自己喜欢的类型名称。这里,由于我们的类叫做 my_class1,因此对应的类型就是 my_class1_t

  • declare_class(或者也可以写成 dcl_class)用于类型的“前置声明”,它的本质就是

typedef struct my_class1_t my_class1_t;

因此并没有什么特别神秘的地方。

  • def_class用于定义类的成员。其中 public_member用于存放公共可见的成员变量;private_member用于存放私有成员;protected_member用于存放当前类以及派生类可见的成员。这三者的顺序任意,可以缺失,也可以存在多个——非常灵活。

第四步:如何设计构造函数

找到 typedef struct my_class1_cfg_t 对应的代码块:

typedef struct my_class1_cfg_t {//! put your configuration members here} my_class1_cfg_t;

可以看到,这是个平平无奇的结构体。它用于向我们的构造函数传递初始化类时所需的参数。在类的头文件中,你很容易找到构造函数的函数原型:

/*! \brief the constructor of the class: my_class1 */
extern
my_class1_t * my_class1_init(my_class1_t *ptObj, my_class1_cfg_t *ptCFG);

可以看到,其第一个参数是指向类实例的指针,而第二个参数就是我们的配置结构体。在类的C源代码文件中,可以找到构造函数的实体:

#undef this
#define this        (*ptThis)
/*! \brief the constructor of the class: my_class1 */
my_class1_t * my_class1_init(my_class1_t *ptObj, my_class1_cfg_t *ptCFG)
{/* initialise "this" (i.e. ptThis) to access class members */class_internal(ptObj, ptThis, my_class1_t);ASSERT(NULL != ptObj && NULL != ptCFG);return ptObj;
}

此时,在构造函数中,我们可以通过 this.xxxx 的方式来访问类的成员,以便根据配置结构体中传进来的内容对类进行初始化。

也许你已经注意到了,我们的模板中并没有任何为类申请空间的代码。这是有意为之。原因如下:

  • 面向对象并非一定要使用动态内存分配,这是一种偏见

  • 我们只提供构造函数,而类的用户可以自由的决定如何为类的实例分配存储空间

  • 由于我们创造的类(比如 my_class1_t)本质上是一个完整的结构体类型,因此可以由用户像普通结构体那样:

    • 进行静态分配:即定义静态变量,或是全局变量

    • 使用池分配:直接为目标类构建一个专用池,有效避免碎片化。

    • 进行堆分配:使用普通的malloc()进行分配,类的大小可以通过sizeof() 获得,比如:

my_class1_cfg_t tCFG = {...
};
my_class1_t *ptNewItem = my_class1_init((my_class1_t *)malloc(sizeof(my_class1_t),&tCFG);
if (NULL == ptNewItem) {printf("Failed to new my_class1_t \r\n");
}...free(ptNewItem);

当然,如果你说我就是要那种形式主义,那你完全可以定义一个宏:

#define new_class(__name, ...)                \
({__name##_cfg_t tCFG = {                     \__VA_ARGS__                               \
};                                            \
__name##_init(                                \(__name##_t *)malloc(sizeof(__name##_t), \&tCFG);})

这可不就是一个根正苗红的 new()方法么,比如:

my_class1_t *ptItem = new_class(my_class, <构造用的参数列表>);
if (NULL == ptItem) {printf("Failed to new my_class1_t \r\n");
}...free(ptItem);

怎么样,是这个味道吧?析构函数类似,比如my_class1_depose()函数,同样不负责资源的释放——决定权还是在用户的手里,当然你也可以做完一套:

#define free_class(__name, __obj)               \do {                                        \__name##_depose((__name##_t *)(__obj)); \free(__obj);                            \} while(0)

形成组合拳,从分配资源、构造、析构到最后释放资源一气呵成:

my_class1_t *ptItem = new_class(my_class, <构造用的参数列表>);
if (NULL == ptItem) {printf("Failed to new my_class1_t \r\n");
}...free_class(my_class, ptItem);

第五步:如何设计构类的方法(method)

我们开篇说过,实践面向对象最重要的是功能,而非形式主义。假设有一个类的方法叫做 method1,理想中,大家一定觉得如下的使用方式是最“正统”的:

my_class1_t *ptItem = new_class(my_class, <构造用的参数列表>);
if (NULL == ptItem) {printf("Failed to new my_class1_t \r\n");
}ptItem.method1(<实参列表>);free_class(my_class, ptItem);

在C语言中,我们完全可以实现类似的效果——只要你在类的定义中加入函数指针就行了——其实很多OOC的模板都是这么做的(比如lw_oopc)。但你仔细思考一下,在类的结构体中加入函数指针究竟有何利弊:

先来说好处:

  • 可以用“优雅”的方式来完成方法的调用;

  • 支持运行时刻的重载(Override);

再来说缺点:

  • 在嵌入式应用中,大部分类的方法都不需要重载,更别说是运行时刻的重载了;

  • 函数指针会占用4个字节;

  • 通过函数指针来实现的间接调用,其效率低于普通的函数直接调用。

换句话说,对大部分类的大部分情况来说,我们都不需要考虑类的方法重载问题,就算有,很多时候也都是编译时刻的静态重载(plooc_example就展示了静态重载的实现方式),那么在不考虑运行时刻动态重载的应用场景下,直接用普通函数来实现类的方法就是务实的一个选择了。

基于这种考虑,上述例子实际上应该写为:

my_class1_t *ptItem = new_class(my_class, <构造用的参数列表>);
if (NULL == ptItem) {printf("Failed to new my_class1_t \r\n");
}my_class1_method1(ptItem,<实参列表>);free_class(my_class, ptItem);

这里,my_class1_method1()my_class1.h 提供声明、my_class1.c 提供实现的一个函数。前缀 my_class1_ 用于防止命名空间污染。

另外一个值得注意的细节是,OOPC中,任何类的方法,其函数的第一个参数一定是指向类实例的指针——也就是我们常说的 this 指针。以 my_class1_method1() 为例,它的形式为:

#undef this
#define this        (*ptThis)void my_class1_method(my_class1_t *ptObj, <形参列表>)
{/* initialise "this" (i.e. ptThis) to access class members */class_internal(ptObj, ptThis, my_class1_t);...    
}

这里,class_internal() 用于将 ptObj转变成我们所需的 this指针(这里的ptThis),借助宏的帮助,我们就可以实现  this.xxxx 这样无成本的形式主义了。

第六步:如何设计类的接口(Interface)

我们的模板还为每个类都提供了一个接口,并默认将构造和析构函数都包含在内,比如,我们可以较为优雅的对类进行构造和析构:

static my_class1_t s_tMyClass;
...
MY_CLASS.Init(&s_tMyClass, ...);
...
MY_CLASS.Depose(&s_tMyClass);

在 my_class1.h 中,我们可以找到这样的结构:

//! \name interface i_my_class1_t
//! @{
def_interface(i_my_class1_t)my_class1_t *  (*Init)       (my_class1_t *ptObj, my_class1_cfg_t *ptCFG);void           (*Depose)     (my_class1_t *ptObj);/* other methods */end_def_interface(i_my_class1_t) /*do not remove this for forward compatibility */
//! @}

假设我们要加入一个新的方法,则只需要在 i_my_class1_t 的接口定义中添加对应的函数指针即可,比如:

//! \name interface i_my_class1_t
//! @{
def_interface(i_my_class1_t)my_class1_t *  (*Init)       (my_class1_t *ptObj, my_class1_cfg_t *ptCFG);void           (*Depose)     (my_class1_t *ptObj);/* other methods */void           (*Method1)    (my_class1_t *ptObj, <形参列表>);
end_def_interface(i_my_class1_t) /*do not remove this for forward compatibility */
//! @}

接下来,我们要在 my_class1.h 中添加对应方法的函数声明:

extern
void my_class1_method1(my_class1_t *ptObj, <形参列表>);

这里,值得注意的是,习惯上函数的命名上与接口除大小写歪,还有一个简单的对应关系:即,所有的"."直接替换成"_",比如,使用上:

MY_CLASS1.Method1()

就对应为:

my_class1_method1()

与此同时,我们需要在 my_class1.c 中添加 my_class1_method1() 函数的实体:

void my_class1_method1(my_class1_t *ptObj, <形参列表>)
{class_internal(ptObj, ptThis, my_class1_t);...
}

并找到名为 MY_CLASS1 的接口实例:

const i_my_class1_t MY_CLASS1 = {.Init =             &my_class1_init,.Depose =           &my_class1_depose,/* other methods */
};

在其中初始化我们的新方法(新函数指针) Method1

const i_my_class1_t MY_CLASS1 = {.Init =             &my_class1_init,.Depose =           &my_class1_depose,/* other methods */.Method1 =          &my_class1_method1,
};

至此,我们就完成了类方法的添加和初始化。以后,在任何地方,都可以通过

<类名大写>.<接口中方法名>()

的形式来访问类的操作函数了——这也算某种程度上的优雅了吧。

第六步:如何设计派生类(Derived Class)

派生类的创建在基本步骤上与普通类基本一致,除了在模板选择阶段使用对应的模板外,还需要在“格式化”阶段额外添加以下两个替换步骤:

  • <BASE_CLASS_NAME> 替换为 基类的大写名称

  • <base_class_name>替换为基类的小写名称

在类的定义阶段,我们注意到:

//! \name class <class_name>_t
//! @{
declare_class(<class_name>_t)def_class(<class_name>_t,  which(implement(<base_class_name>_t))...
)end_def_class(<class_name>_t) /* do not remove this for forward compatibility  */
//! @}

派生类在原有的类定义基础上多出了的结构,以"," 与类的类型名隔开:

which(implement(<base_class_name>_t))

这里,which() 其实是一个列表,它允许我们实现多重继承。假设我们有多个基类,或是要继承多个接口,则可以写成如下的形式:

which(implement(<base_class_name1>_t)implement(<base_class_name2>_t)implement(<interface_name1>_t)implement(<interface_name2>_t)
)

需要注意的是,如果基类或是接口中存在名称冲突(重名)的成员,则可以将 implement() 替换为  inherit() 来避免这种冲突。比如 <interface_name2>_t 与 <base_class_name1>_t 都有一个叫做 wID 的成员,则可以通过将其中之一的implement() 替换为 inherit的方式在新的派生类中避免冲突:

which(inherit(<base_class_name1>_t)implement(<base_class_name2>_t)implement(<interface_name1>_t)implement(<interface_name2>_t)
)

就像这里所展示的那样,PLOOC支持多继承,这是C++和C#都不曾支持的——这也是 使用C语言来实现OO的魅力之一,具体方法,大家可以自行摸索,这里就不再赘述。

大家都知道,在面向对象中,有一类成员只有当前类和派生类能够访问——我们称之为受保护成员(protected member)。在类的定义中,可以通过 protected_member() 将这些成员囊括起来,比如:

//! \name class byte_queue_t
//! @{
declare_class(byte_queue_t)def_class(byte_queue_t,private_member(implement(mem_t)                    //!< queue buffervoid        *pTarget;               //!< user target)protected_member(uint16_t    hwHead;                 //!< head pointeruint16_t    hwTail;                 //!< tail pointeruint16_t    hwCount;                //!< byte count)
)end_def_class(byte_queue_t) /* do not remove this for forward compatibility  */
//! @}

这里,hwHead、hwTail和hwCount 都只有当前类和派生类能访问。

对于那些只允许派生类访问的方法(函数)来说,我们一般会使用预编译宏的形式将其有条件的保护起来:

#if defined(__BYTE_QUEUE_CLASS_IMPLEMENT) || defined(__BYTE_QUEUE_CLASS_INHERIT__)
extern mem_t byte_queue_buffer_get(byte_queue_t *ptObj);
#endif

这里,受到宏 __BYTE_QUEUE_CLASS_IMPLEMENT__BYTE_QUEUE_CLASS_INHERIT 的保护,函数 byte_queue_buffer_get() 仅能够允许类 byte_queue_t 自身极其派生类才能访问了。

在我们前面创建的 my_class1.h 中我们也有一个类似的例子:

#if defined(__MY_CLASS1_CLASS_IMPLEMENT) || defined(__MY_CLASS1_CLASS_INHERIT__)
/*! \brief a method only visible for current class and derived class */
extern void my_class1_protected_method_example(my_class1_t *ptObj);
#endif

函数 my_class1_protected_method_example() 就是一个仅供 my_class1 极其派生类访问的 受保护的方法。

在派生类中,如果要访问基类的受保护成员,则可以借助 protected_internal() 的帮助,例如:

#undef this
#define this        (*ptThis)#undef base
#define base        (*ptBase)void enhanced_byte_queue_peek_reset(enhanced_byte_queue_t *ptObj)
{/* initialise "this" (i.e. ptThis) to access class members */class_internal(ptObj, ptThis, enhanced_byte_queue_t);/* initialise "base" (i.e. ptBase) to access protected members */protected_internal(&this.use_as__byte_queue_t, ptBase, byte_queue_t);ASSERT(NULL != ptObj);/* ------------------atomicity sensitive start---------------- */this.hwPeek = base.hwTail;this.hwPeekCount = base.hwCount;/* ------------------atomicity sensitive end---------------- */
}

这里,派生类借助 this.use_as__byte_queue_t 获得了对基类的“引用”,并借助  protected_internal() 将其转化为了名为 ptBase 的指针。在 base 宏的帮助下,我们得以通过  base.xxxx 来访问基类的成员。在例子中,我们看到,base.hwTailbase.hwCount 正是前面所展示过的 byte_queue_t 的受保护成员。

【说在后面的话】


无论使用何种模板,OOPC来发的一个核心理念应该是“务实”,即:以最小的成本(最好是零成本),占最大的便宜(来自OO所带来的好处)

此前,我曾经在文章《真刀真枪模块化(2.5)—— 君子协定》详细介绍过PLOOC的原理和手动部署技术。借助CMSIS-Pack和MDK中RTE的帮助,原本繁琐的手动部署和类的创建过程得到了空前的简化,使用OOPC进行开发从未如此简单过——几乎与直接使用C++相差无几了

不知不觉间,从2年前第一次将其公开算起,PLOOC已经斩获了一百多个Star——算是我仓库中的明星工程了。从日志上来看,PLOOC相当稳定。距离我上一次“觉得其有必要更新”还是整整一年多前的事情,而加入CMSIS-Pack只是一件锦上添花的事情。

d7dadccbae9654f2864a3ba774420841.png

最后,感谢大家的支持——是你们的Star支撑着我一路对项目的持续更新。谢谢!

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。

这篇关于OOPC开发从未如此简单!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/262941

相关文章

一份LLM资源清单围观技术大佬的日常;手把手教你在美国搭建「百万卡」AI数据中心;为啥大模型做不好简单的数学计算? | ShowMeAI日报

👀日报&周刊合集 | 🎡ShowMeAI官网 | 🧡 点赞关注评论拜托啦! 1. 为啥大模型做不好简单的数学计算?从大模型高考数学成绩不及格说起 司南评测体系 OpenCompass 选取 7 个大模型 (6 个开源模型+ GPT-4o),组织参与了 2024 年高考「新课标I卷」的语文、数学、英语考试,然后由经验丰富的判卷老师评判得分。 结果如上图所

Eclipse+ADT与Android Studio开发的区别

下文的EA指Eclipse+ADT,AS就是指Android Studio。 就编写界面布局来说AS可以边开发边预览(所见即所得,以及多个屏幕预览),这个优势比较大。AS运行时占的内存比EA的要小。AS创建项目时要创建gradle项目框架,so,创建项目时AS比较慢。android studio基于gradle构建项目,你无法同时集中管理和维护多个项目的源码,而eclipse ADT可以同时打开

回调的简单理解

之前一直不太明白回调的用法,现在简单的理解下 就按这张slidingmenu来说,主界面为Activity界面,而旁边的菜单为fragment界面。1.现在通过主界面的slidingmenu按钮来点开旁边的菜单功能并且选中”区县“选项(到这里就可以理解为A类调用B类里面的c方法)。2.通过触发“区县”的选项使得主界面跳转到“区县”相关的新闻列表界面中(到这里就可以理解为B类调用A类中的d方法

自制的浏览器主页,可以是最简单的桌面应用,可以把它当成备忘录桌面应用

自制的浏览器主页,可以是最简单的桌面应用,可以把它当成备忘录桌面应用。如果你看不懂,请留言。 完整代码: <!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><ti

Python应用开发——30天学习Streamlit Python包进行APP的构建(9)

st.area_chart 显示区域图。 这是围绕 st.altair_chart 的语法糖。主要区别在于该命令使用数据自身的列和指数来计算图表的 Altair 规格。因此,在许多 "只需绘制此图 "的情况下,该命令更易于使用,但可定制性较差。 如果 st.area_chart 无法正确猜测数据规格,请尝试使用 st.altair_chart 指定所需的图表。 Function signa

python实现最简单循环神经网络(RNNs)

Recurrent Neural Networks(RNNs) 的模型: 上图中红色部分是输入向量。文本、单词、数据都是输入,在网络里都以向量的形式进行表示。 绿色部分是隐藏向量。是加工处理过程。 蓝色部分是输出向量。 python代码表示如下: rnn = RNN()y = rnn.step(x) # x为输入向量,y为输出向量 RNNs神经网络由神经元组成, python

WDF驱动开发-WDF总线枚举(一)

支持在总线驱动程序中进行 PnP 和电源管理 某些设备永久插入系统,而其他设备可以在系统运行时插入和拔出电源。 总线驱动 必须识别并报告连接到其总线的设备,并且他们必须发现并报告系统中设备的到达和离开情况。 总线驱动程序标识和报告的设备称为总线的 子设备。 标识和报告子设备的过程称为 总线枚举。 在总线枚举期间,总线驱动程序会为其子 设备创建设备对象 。  总线驱动程序本质上是同时处理总线枚

宝塔面板部署青龙面板教程【简单易上手】

首先,你得有一台部署了宝塔面板的服务器(自己用本地电脑也可以)。 宝塔面板部署自行百度一下,很简单,这里就不走流程了,官网版本就可以,无需开心版。 首先,打开宝塔面板的软件商店,找到下图这个软件(Docker管理器)安装,青龙面板还是安装在docker里,这里依赖宝塔面板安装和管理docker。 安装完成后,进入SSH终端管理,输入代码安装青龙面板。ssh可以直接宝塔里操作,也可以安装ssh连接

JavaWeb系列六: 动态WEB开发核心(Servlet) 上

韩老师学生 官网文档为什么会出现Servlet什么是ServletServlet在JavaWeb项目位置Servlet基本使用Servlet开发方式说明快速入门- 手动开发 servlet浏览器请求Servlet UML分析Servlet生命周期GET和POST请求分发处理通过继承HttpServlet开发ServletIDEA配置ServletServlet注意事项和细节 Servlet注

手把手教你入门vue+springboot开发(五)--docker部署

文章目录 前言一、前端打包二、后端打包三、docker运行总结 前言 前面我们重点介绍了vue+springboot前后端分离开发的过程,本篇我们结合docker容器来研究一下打包部署过程。 一、前端打包 在VSCode的命令行中输入npm run build可以打包前端代码,出现下图提示表示打包完成。 打包成功后会在前端工程目录生成dist目录,如下图所示: 把