译:应用笔记 在C中的简单面向对象编程

**在学习QP架构的过程中,看到这篇OOP简易设计将挺好,英文文档看着累,于是就把它翻译一下,用作以后的参考。
PS:英语也不是很好和翻译软件一起翻的= =,中间有的翻译不是很准确,如有疑问可以参照原文。**
另外:好像不知道怎么放原文??


封面

这里写图片描述

————————————————————————————————————————————
**

目录

1.介绍
2.封装性
3.继承性
4.多态性(虚函数)
4.1 虚函数表(vtbl)和虚拟函数指针(vptr)
4.2 在构造函数中设置vptr
4.3 在子类中继承vtbl和重写vptr
4.4 虚拟调用(迟绑定)
4.5 虚函数使用实例
5.总结
6.参考文献
7.联系方式**


1.介绍

面向对象编程(OOP)不是一种特殊语言的使用或工具,而是一种基于三种基础设计元模式的设计方法。
封装性:将封装数据和函数到类中。
继承性:基于一个现有的类来定义一个新的类,从而获得代码结构的重用。
多态性:在程序运行时,子类对象能有接口匹配的能力。

虽然这些元模式通常会和像Smalltalk,C++,Java等的面对对象语言的联系起来,但你也几乎可以用任何编程语言包括标准C来实现它们。

—————————————————————
如果你是用C开发终端用户程序,但你也想用面向对象的形式进行编程,那么你很可能会用C++代替C。相比于C++,C的面向对象编程庞大而繁琐并且容易出错,很少有性能上的优势。
但是,如果你要搭建或使用一个框架,像QP/C和QP-nano这种主动对象类的框架,面向对象的思想作为专业自定义的首要结构,拓展框架到应用软件就变得非常有用。在那种情况下,用C做面对对象的编程最困难的部分被限制为了框架,并且能有效地向应用程序开发人员隐藏细节。该应用笔记中有主要的使用案例。
—————————————————————

该篇文档描述了面对对象编程在QP/C和QP-nano主动对象框架中如何实现。作为一个这些框架的使用者来说,你需要理解这种技术,因为你也会需要在你的应用级代码中应用它们。但是这些技术也不仅限制于开发QP/C或者QP-nano的应用程序也可广泛适用于C语言编程。


2.封装性

封装性是用来将数据和函数到类中的。这种观念实际上需要被所有的C语言程序员所熟悉,因为封装的概念使用相当广泛,甚至是在传统C语言中。举例来说,在标准C的运行环境库中,一系列的函数像fopen(),fclose(),fread()和fwrite()用来操作FILE类型的对象。FILE结构就具有这样的封装性,因为客户端编程人员不必访问FILE结构的内部属性,而不是整个文件接口仅存在上述功能。你可以想到FILE结构体和操作FILE类的相关C函数。下面的项目符号项总结了C运行环境库是如何运行FILE类的:
1.所以类的属性在一个C结构体(FILE结构体)中被定义。
2.对类的操作被定义为C函数。每个函数都有一个指针指向这个属性结构(FILE *)作为函数参数。类操作典型的遵从通用命名约定(eg.,所有FILE类方法都用f的前缀开头)。
3.有特殊的函数来进行初始化和清除属性结构(fopen()和fclose())。这些函数分别起着构造和析构的作用。

你能非常容易的运用这些设计原则到你自己的类中。比如,假设你有一个应用程序,用来画一个二维的几何形状(可能是在嵌入式液晶上显示)。这种基本的Shape“类”在C中可以被声明为一下:
列表1 shape类在C中的声明

/* Shape的属性 */
typedef struct {
int16_t x; /* Shape的x坐标 */
int16_t y; /* Shape的y坐标*/
} Shape;

/* Shape的操作(Shape的接口)*/
void Shape_ctor(Shape * const me, int16_t x, int16_t y,
Color outline, Color fill);
uint32_t Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);

虽然有时候你可能会选择把声明放到同一个文件范围内(.c file),但”类”的声明通常被放在头文件中(像shape.h)。

类的一个很好的方面是它可以画在图中,并且显示类名,属性操作和类之间的相互关系。下述的图片展示了Shape类UML类图。

图1 Shape的UML类表
这里写图片描述

并且这里面定义了Shape的操作(必定在一个.c文件中):
列表2 Shape在C中的定义

/* 构造函数 */
void Shape_ctor(Shape * const me, int16_t x, int16_t y) {
me->x = x;
me->y = y;
}
/* move-by 操作*/
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) {
me->x += dx;
me->y += dy;
}

你能创建任意数量的Shape类作为它的实例属性。你需要用构造函数Shape_ctor()初始化每个实例。你只能通过第一个参数是指针“me”的提供的函数来操作Shape这个类。

——————————————————————
注意:me指针直接对应着C++里的隐式指针this。This符号没有被使用,因为他是C++中的关键字并且这样的程序不能在C++编译器中编译。
——————————————————————

列表3 在C中使用Shape类的例子

Shape s1, s2; /* Shape 的多个实例*/
Shape_ctor(&s1, 0, 1);
Shape_ctor(&s2, -1, 2);
Shape_moveBy(&s1, 2, -4);
. . .

3.继承性

继承性是基于一个现有的类来定义一个新的类,从而获得代码结构的重用。你可以很容易的在C中正确嵌入要继承的类属性结构作为派生类属性结构的第一个成员来实现单继承。
举例来说,不希望重头创建一个Rectangle类,你可以从有很多相同并且已经存在的Shape类上继承,并且只需加入它和矩形类的不同点。这里展示了如何声明这个矩形类。

列表4 作为Shape类的子类Rectangle声明

typedef struct {
Shape super; /* <== 继承Shape */
/* 添加子类的属性 */
uint16_t width;
uint16_t height;
} Rectangle;
/* 构造函数 */
void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
uint16_t width, uint16_t height);

如你所见,你通过嵌入超类(Shape)作为子类(Rectangle)的第一个成员“super”可以实现继承。

如下列图所示,这种安排会导致内存对齐,它可以让你将任何指向Rectangle类的指针视为一个指向Shape类指针:

图2 在C中的单继承:(a)继承关系的类图表
(b)Rectangle和Shape对象的内存分布
这里写图片描述

————————————————————————
注意:Rectangle结构体的这种对齐方式和从Shape结构体上得到的继承属性是被标准C语言WG14/N1124所保证的。该标准的6.7.2.1.13章节上:“一个指向结构体对象的指针,适当的转换后可指向可指向他的最初的成员。结构体对象内可以有未命名的填充,但不能在它的开头。”
————————————————————————

在这种安排下,你可以安全的将指向Rectangle的指针传递给所需Shape指针的任何C函数。具体来说,所有来自Shape类(称为超类)的函数自动可用于Rectangle类(称为子类)。因此,不仅仅是所有属性,连来自子类的所有函数也被所有子类继承。

列表5 矩形类的构造函数

void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
uint16_t width, uint16_t height)
{
/* 首次调用父类的构造函数 */
Shape_ctor(&me->super, x, y);
/*接下来,初始化子类中增加的属性*/
me->width = width;
me->height = height;
}

为了保证C完全正确,你需要将一个指向子类的指针显性转换一个指向超类的指针。在面向对象编程中。这类别称为向上类型转换,并总是安全的。

列表6 使用Rectangle对象的例子

Rectangle r1, r2;
/* 初始化rectangles*/
Rectangle_ctor(&r1, 0, 2, 10, 15);
Rectangle_ctor(&r2, -1, 3, 5, 8);
/* 使用从超类Shape继承的功能... */
Shape_moveBy((Shape *)&r1, -2, 3);
Shape_moveBy(&r2->super, 2, -1); 

如你所见,调用继承函数,你需要向上显示转换第一个参数“me”到超类(Shape *)。
或者你可以避免转换,使用带成员“super”(&r2->super)的地址。

———————————————————————
注意:子类的实例演示中没有多余的开销花费在使用“继承”函数上。换句话说,一个子类对象调用函数的开销和父类对象调用相同的函数的开销是一样的。这种开销也很类似于(本质一样)在C++中的消耗。
———————————————————————


4.多态性(虚函数)

多态性是在程序运行时,子类对象能有接口匹配的能力。C++中的多态性是靠虚函数实现的。在C中,你也可以在若干方式实现虚函数。这里演示的解决方案(并且运用于QP/C和QP-nano主动对象框架)和虚函数在C++中有着非常类似的性能和内存开销。
举个怎样才能使虚函数得到运用的例子,这里再次用前面介绍的Shape类进行示范。这个类可以提供很多有用的操作,像area()(让Shape计算他自己的面积)和draw()(让Shape在屏幕上画在出自己的形状)等。但是,问题是Shape类不能提供实际的这类的解决措施,因为Shape是一个太抽象的概念,它并不知道怎么去计算,描述它自己的面积。而计算方式会因为对象是一个Rectangle 的子类(width *height)或者Circle 的子类(pi *radium)而有很大差异。
然而,这不意味着Shape不能提供操作的接口,像Shape_area()或Shape_draw(),如下:

uint32_t Shape_area(Shape * const me);
void Shape_draw(Shape * const me); 

实际上,这样的接口非常有用。因为它允许你去写一个通用代码去通用的操作形状。举个例子,提供一个这样的接口,你可以写一个泛型函数在屏幕上绘制任何形状或者找到最大的形状(在最大的面积中)。这点上可能听起来有点理论,但是当你看到等下在下面的章节中看到实际代码时,这个观点会变得更加清晰。

图3 添加虚函数area()和draw()到Shape类和他的子类中
这里写图片描述

4.1 虚函数表(vtbl)和虚拟函数指针(vptr)

现在应该很清楚认识到一个单继承的虚函数,像Shape_area(),能在Shape的子类中提供很多不同的执行。比如,Shape的子类Rectangle和Circle有着不同的计算面积的方式。
这意味着虚函数的调用不是在链接时决定的,它是由C中的函数回调实现的,因为实际调用函数的版本取决于对象的类型(Rectangle, Circle等)。因此,相反的,将虚函数的调用和实际执行绑定在一起必须要发生在实时运行的时候,这就是所谓迟绑定(和链接时绑定相反,即早期绑定)。
几乎和所有C++编译器通过每个类有一个虚函数表(vtbl)和每个对象的虚拟函数指针(vptr)实现迟绑定。这种方式同样可以运用在C中。
虚函数表是一个函数指针表和在类中定义的虚函数一致。在C中,一个虚函数表能被函数指针的结构体所模拟,如下:

表7 Shape类的虚函数表 (详见图3)

typedef struct {
    uint32_t (*area)(Shape const *me);
    void (*draw)(Shape const *me);
} ShapeVtbl 

虚拟函数指针(vptr)是指向类的虚函数表的指针。这个指针必须存在于类的每个实例(对象)中,因此它必须进入到类的属性结构中。例如,这里要在Shape类的结构体属性的最顶部增加虚拟函数指针成员。

列表8 增加虚拟函数指针(vptr)到Shape类中

/* Shapede的属性 */
typedef struct {
    ShapeVtbl const *vptr; /* <== Shape的虚拟函数指针 */
    int16_t x; /* Shape位置的x坐标 */
    int16_t y; /* Shape位置的y坐标*/
} Shape; 

虚拟函数指针被声明为常量指针(参照const关键字前加*的用法),因为虚函数表不能被改变,并且事实上它是被分配到ROM中的。
虚拟函数指针(vptr)被所有子类所继承,因此Shape的vptr类会自动获得他的子类,像Rectangle, Circle等。

4.2 在构造函数中设置vptr

虚拟函数指针(vptr)在每个类的实例(对象)中必须被初始化指向相关的虚函数表(vtbl)。这种像初始化一样的理想布置并执行是通过类的构造函数。事实上,准确的说,这正是C++编译器隐式生成虚拟函数指针的初始化代码。
在C里,你必须显示初始化vptr。这里有一个在Shape的构造函数中建立虚函数表并初始化虚拟函数指针的例子:

列表9 在构造函数中定义虚函数表并初始化虚拟函数指针(vptr)

/* Shape 类实现它的虚函数*/
static uint32_t Shape_area_(Shape * const me);
static void Shape_draw_(Shape * const me);
/* 构造函数 */
void Shape_ctor(Shape * const me, int16_t x, int16_t y) {
    static ShapeVtbl const vtbl = { /*Shape类的虚函数表*/
        &Shape_area_,
&Shape_draw_
};
me->vptr = &vtbl; /* 虚拟函数指针指向虚拟函数表的钩子 */
me->x = x;
me->y = y;
} 

如你所见虚函数表被static和const所修饰,因为你需要每个类只有一个vtbl实例并且vbtl应该运行在ROM中(在嵌入式系统中)。
vtbl被初始化指向要执行的相应函数。在这个例子中,具体的实施对象是Shape_area_() and Shape_draw_() (注意结尾的下划线)。
如果一个类不能提供其中的虚函数的合理实现(因为这是一个抽象类,就像Shape一样),实现应该在内部进行声明。这样,至少在实时运行中应该知道,那个未实现的函数(纯虚函数)被调用了:

列表10 在Shape类中定义纯虚函数

/* Shape 类的虚函数实现 */
static uint32_t Shape_area_(Shape * const me) {
    ASSERT(0); /* 纯虚函数应该永远不会被调用 */
    return 0U; /* 为了避免编译警告 */
}
static void Shape_draw_(Shape * const me) {
ASSERT(0); /* 纯虚函数应该永远不会被调用*/
} 

4.3 在子类中继承vtbl和重写vptr

像前面提到的,如果一个父类包括vptr,则它会自动地被派生的子类在所有继承层次上继承。因此,属性继承的技术(通过super成员)自动为多态类工作。
然而,vptl通常需要被重新分配到特殊子类的虚函数表中。此外,重新分配必须发生在子类的构造函数。比如,这里有个Shape类的子类Rectangle类的构造函数:

列表11 在Shape父类的Rectangle子类中重写vbtl和vptr

/* Rectangle类实现纯虚函数*/
static uint32_t Rectangle_area_(Shape * const me);
static void Rectangle_draw_(Shape * const me);
/* 构造函数 */
void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
uint16_t width, uint16_t height)
{
    static ShapeVtbl const vtbl = { /* Rectangle类的虚函数表*/
    &Rectangle_area_,
    &Rectangle_draw_
    };
    Shape_ctor(&me->super, x, y); /* 调用父类的构造函数*/
    me->super.vptr = &vtbl; /* 重写vptr */
    me->width = width;
    me->height = height;
} 

请注意父类的构造函数(Shape_ctor())被调用最先初始化从Shape中继承的me->super成员。构造函数设置vptr来指向Shape的虚函数表。然而,vptr再下一条分配Rectangle的虚函数表中的语句中被重新了。
请同样注意子类虚函数的执行必须严格匹配父类中定义的签名,以适应vtbl。例如,用带着Shape *类的me指针的Rectangle_area_()函数的执行,替代他自己的Rectangle类。来自子类的实际实现必须执行显示对me指针进行向下转换,具体如下:

列表12 在子类实现中对me指针进行显示向下转换

static uint32_t Rectangle_area_(Shape * const me) {
    Rectangle * const me_ = (Rectangle *)me; /* 向下显示转换 */
    return (uint32_t)me_->width * (uint32_t)me_->height;
} 

————————————————————————
注意:为了方便讨论,列表11展示了一个Rectangle类在不添加的自身的虚函数的例子。在这个例子中,Rectangle会重用Shape的Vtbl。然而,Rectangle能它自己继承于Shape虚函数表的Rectangle虚函数表的例子可以相当容易延伸到通常情况中实现。
————————————————————————

4.4 虚拟调用(迟绑定)

在虚函数表和虚拟函数指针的适当结构下,虚拟调用(迟绑定)可以像以下方式实现:

uint32_t Shape_area(Shape * const me) {
    return (*me->vptr->area)(me);
}

这个函数的定义同样也被放在.c文件内,但是缺点是你需要承担二外的调用开销。为了避免这种开销,如果你的编译器支持内联函数(标准C99),那么你可以在头文件中这么定义:

static inline uint32_t Shape_area(Shape * const me) {
    return (*me->vptr->area)(me);
} 

或者对于更老的编译器(C89)你可以使用类函数宏,像这样:

#define Shape_area(me_) ((*(me_)->vptr->area)((me_))) 

无论哪种方法,虚拟的工作原理是通过首先找到相符对象的虚函数表,只有这样才能通过虚函数表中的函数指针调用合适的实现。下图说明了这个过程:

图4 矩形和圆的虚拟调用机制
这里写图片描述

4.5 虚函数使用实例

就像章节之前提到的多态性一样,虚函数会让你写出很清晰的代码,子类的具体实现将会别独立出来。此外,该种代码还会自动支持不限数量的子类,可以在泛型代码长时间开发后被添加。
例如,下列代码的功能是找到并返回屏幕上最大的形状:

Shape const *largestShape(Shape const *shapes[], size_t nShapes) {
    Shape const *s = NULL;
    uint32_t max = 0U;
    size_t i;
    for (i = 0U; i < nShapes; ++i) {
        uint32_t area = Shape_area(shapes[i]); /* 虚拟调用*/
        if (area > max) {
        max = area;
        s = shape[i];
        }
    }
return s; /* 在数组shapes[]中最大的形状 */
} 

类似的,下列代码可以在屏幕上画任意形状:

void drawAllShapes(Shape const *shapes[], size_t nShapes) {
    size_t i;
    for (i = 0U; i < nShapes; ++i) {
        Shape_draw(shapes[i]); /* 虚拟调用 */
    }
} 

5.总结

OOP是一种设计方法,而不是特定的语言的使用或工具。本文描述了如何在简易C中的实现封装,单继承和多态性的概念。前两个概念(类和继承)证明了在没有增加任何额外开销的情况下是相当容易实现的。
多态性证明了过程是相当复杂的。如果你打算大范围地使用它,则切换到C++去会更好。然而,如果你是要搭建或使用应用框架,像QP/C和QP-nano主动对象框架,在C中使用OOP的复杂性能被现在在框架之内并且对应用开发来说能被有效的隐藏。


6.参考文献

这里写图片描述


7.联系信息

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值