打开
【说在前面的话】
“零消耗”原则:即,我们所要实现的所有面向对象的特性都应该是“零资源消耗”或至少是“极小资源消耗”。这里的原理是:能在编译时刻(Compiletime)搞定的事情,绝不拖到运行时刻(Runtime)。
务实原则:即,我们不在形式上追求与C++类似,除非它的代价是零或者非常小。
“按需实现”原则:即,对任何类的实现来说,我们并不追求把所有的OO特性都实现出来——这完全没有必要——我们仅根据实际应用的需求来实现最小的、必要的面向对象技术。
“傻瓜化”原则:即,类的建立和使用都必须足够傻瓜化。最好所见即所得。
https://raw.githubusercontent.com/GorgonMeducer/PLOOC/master/cmsis-pack/GorgonMeducer.PLOOC.4.6.0.pack
一般来说,部署会非常顺利,但如果出现了安装错误,比如下面这种:
则很可能是您所使用的MDK版本太低导致的——是时候更新下MDK啦。关注【裸机思维】公众号后发送关键字"MDK",即可获得最新的MDK网盘链接。
【如何快速尝鲜】
类的定义方式
如何实现类的方法(Method)
如何为类定义接口(Interface)
如何定义派生类
如何重载接口
如何在派生类中访问基类受保护的成员(Protected Member)
……
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 enqueue
ENHANCED_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 dequeue
do {
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);
LOG_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));
此时我们仍然使用的是C语言,而不是C++;
在C99下,我们可以实现拥有不同参数个数的函数共享同一个名字;
在C11下,我们可以实现拥有相同参数个数但类型不同的函数共享同一个名字;
我们在运行时刻的开销是0,一切在编译时刻就已经尘埃落定了。我们并没有为这项特性牺牲任何代码空间。
则我们需要在 C/C++选项中:
将Language C设置为 gnu11(或者最低c99):
(推荐,而不是必须)在Misc Controls中添加对微软扩展的支持,并在 Define中添加一个宏定义 _MSC_VER。
-fms-extensions
类(class)
私有成员(private member)
公共成员(public member)
保护成员(protected member)
构造函数(constructor)
析构函数(destructor)
类的方法(method)
……
假设我们要创造一个新的类,叫做 my_class1
在工程管理器中,添加一个新的group,命名为 my_class1:
在弹出的对话框中选择 User Code Template:
展开 Language Extension,可以看到有两个 PLOOC模板,分别对应:
基类和普通类(Base Class Template) 派生类(Derived Class Template)
由于我们要创建的是一个普通类(未来也可以作为基类),因此选择“Base Class Template”。单击Location右边的 "..." 按钮,选择一个保存代码文件的路径后,单击“Add”。
此时我们可以看到,class_name.c 被添加到了 my_class1中,且MDK自动在编辑器中为我们打开了两个模板文件:class_name.h和class_name.c。
在编辑器中打开或者选中 class_name.c。通过快捷键CTRL+H打开 替换窗口:
在Look in中选择Current Document 去掉Find Opitons属性框中的 Match whold word前的勾选(这一步骤很重要)
接下来,依次:
将小写的 <class_name> 替换为 my_class1 将大写的 <CLASS_NAME> 替换为 MY_CLASS1
将小写的 <class_name> 替换为 my_class1 将大写的 <CLASS_NAME> 替换为 MY_CLASS1
在工程管理器中展开 my_class1,并将其中的 class_name.c 删除:
打开class_name.c 所在文件目录:
打开 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 {
//! 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源代码文件中,可以找到构造函数的实体:
/*! \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);
当然,如果你说我就是要那种形式主义,那你完全可以定义一个宏:
({__name
__VA_ARGS__ \
}; \
__name
(__name
&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()函数,同样不负责资源的释放——决定权还是在用户的手里,当然你也可以做完一套:
do { \
__name
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)
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);
可以用“优雅”的方式来完成方法的调用;
支持运行时刻的重载(Override);
在嵌入式应用中,大部分类的方法都不需要重载,更别说是运行时刻的重载了;
函数指针会占用4个字节;
通过函数指针来实现的间接调用,其效率低于普通的函数直接调用。
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);
#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);
...
}
我们的模板还为每个类都提供了一个接口,并默认将构造和析构函数都包含在内,比如,我们可以较为优雅的对类进行构造和析构:
static my_class1_t s_tMyClass;
...
MY_CLASS.Init(&s_tMyClass, ...);
...
MY_CLASS.Depose(&s_tMyClass);
//! \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 */
//! @}
//! \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 */
//! @}
extern
void my_class1_method1(my_class1_t *ptObj, <形参列表>);
MY_CLASS1.Method1()
my_class1_method1()
void my_class1_method1(my_class1_t *ptObj, <形参列表>)
{
class_internal(ptObj, ptThis, my_class1_t);
...
}
const i_my_class1_t MY_CLASS1 = {
.Init = &my_class1_init,
.Depose = &my_class1_depose,
/* other methods */
};
const i_my_class1_t MY_CLASS1 = {
.Init = &my_class1_init,
.Depose = &my_class1_depose,
/* other methods */
.Method1 = &my_class1_method1,
};
<类名大写>.<接口中方法名>()
派生类的创建在基本步骤上与普通类基本一致,除了在模板选择阶段使用对应的模板外,还需要在“格式化”阶段额外添加以下两个替换步骤:
将 <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(
implement(<base_class_name1>_t)
implement(<base_class_name2>_t)
implement(<interface_name1>_t)
implement(<interface_name2>_t)
)
which(
inherit(<base_class_name1>_t)
implement(<base_class_name2>_t)
implement(<interface_name1>_t)
implement(<interface_name2>_t)
)
大家都知道,在面向对象中,有一类成员只有当前类和派生类能够访问——我们称之为受保护成员(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 buffer
void *pTarget; //!< user target
)
protected_member(
uint16_t hwHead; //!< head pointer
uint16_t hwTail; //!< tail pointer
uint16_t hwCount; //!< byte count
)
)
end_def_class(byte_queue_t) /* do not remove this for forward compatibility */
//! @}
extern mem_t byte_queue_buffer_get(byte_queue_t *ptObj);
/*! \brief a method only visible for current class and derived class */
extern void my_class1_protected_method_example(my_class1_t *ptObj);
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---------------- */
}
无论使用何种模板,OOPC来发的一个核心理念应该是“务实”,即:以最小的成本(最好是零成本),占最大的便宜(来自OO所带来的好处)。
此前,我曾经在文章《真刀真枪模块化(2.5)—— 君子协定》详细介绍过PLOOC的原理和手动部署技术。借助CMSIS-Pack和MDK中RTE的帮助,原本繁琐的手动部署和类的创建过程得到了空前的简化,使用OOPC进行开发从未如此简单过——几乎与直接使用C++相差无几了。
不知不觉间,从2年前第一次将其公开算起,PLOOC已经斩获了一百多个Star——算是我仓库中的明星工程了。从日志上来看,PLOOC相当稳定。距离我上一次“觉得其有必要更新”还是整整一年多前的事情,而加入CMSIS-Pack只是一件锦上添花的事情。
如果你喜欢我的思维、觉得我的文章对你有所启发,
请务必 “点赞、收藏、转发” 三连,这对我很重要!谢谢!
欢迎订阅 裸机思维