参考:Application Note Object-Oriented Programming in C (AN_OOP_in_C)一书
书中代码下载地址:https://sourceforge.net/projects/qpc/files/
面向对象编程的特点:封装、继承、多态。
FILE struct
FILE *
fopen()
,fclose()
#ifndef SHAPE_H #define SHAPE_H #include <stdint.h> /* Shape's attributes... */ typedef struct { int16_t x; /* x-coordinate of Shape's position */ int16_t y; /* y-coordinate of Shape's position */ } Shape; /* Shape's operations (Shape's interface)... */ void Shape_ctor(Shape * const me, int16_t x, int16_t y); void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy); int16_t Shape_getX(Shape const * const me); int16_t Shape_getY(Shape const * const me); #endif /* SHAPE_H */
#include "shape.h" /* Shape class interface */ /* constructor implementation */ void Shape_ctor(Shape * const me, int16_t x, int16_t y) { me->x = x; me->y = y; } /* move-by operation implementation */ void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) { me->x += dx; me->y += dy; } /* "getter" operations implementation */ int16_t Shape_getX(Shape const * const me) { return me->x; } int16_t Shape_getY(Shape const * const me) { return me->y; }
me
作为第一个参数,起到this
指针的作用基于现有类定义新类的能力,以获得重用和代码组织
子类.h
文件引用基类.h
通过将继承的类属性结构嵌入为派生类属性结构的第一个成员
#ifndef RECT_H #define RECT_H #include "shape.h" /* the base class interface */ /* Rectangle's attributes... */ typedef struct { Shape super; /* <== inherits Shape */ /* attributes added by this subclass... */ uint16_t width; uint16_t height; } Rectangle; /* constructor prototype */ void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height); #endif /* RECT_H */
子类的构造函数,第一步需要调用基类的构造函数
#include "rect.h" /* constructor implementation */ void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { /* first call superclass ctor */ Shape_ctor(&me->super, x, y); /* next, you initialize the attributes added by this subclass... */ me->width = width; me->height = height; }
具体使用
实例化子类,调用构造函数
重用基类的函数,因虚调用父类的引用,可以使用以下两种方式:
因为内存中基类是位于子类的开始位置,所以可以将基类对象强制转为父类对象,进而使用父类的函数
Shape_moveBy((Shape *)&r1, -2, 3);
可以利用子类实例调用父类成员,进而使用父类的函数
Shape_moveBy(&r2.super, 2, -1);
int main() { Rectangle r1, r2; /* multiple instances of Rect */ /* instantiate rectangles... */ Rectangle_ctor(&r1, 0, 2, 10, 15); Rectangle_ctor(&r2, -1, 3, 5, 8); printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r1.super), Shape_getY(&r1.super), r1.width, r1.height); printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r2.super), Shape_getY(&r2.super), r2.width, r2.height); /* re-use inherited function from the superclass Shape... */ Shape_moveBy((Shape *)&r1, -2, 3); Shape_moveBy(&r2.super, 2, -1); printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r1.super), Shape_getY(&r1.super), r1.width, r1.height); printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r2.super), Shape_getY(&r2.super), r2.width, r2.height); return 0; }
概念:在运行时相互替换匹配接口对象的能力
c++中实现多态是利用虚函数
。在C 语言中,使用函数指针
+虚函数表
,构成虚函数
,实现虚函数的机制
作为抽象接口,其不同的子类有不同的函数实现方法,需要在运行时动态根据本身的类型进行选择,如下图所示Rectangle 和 Circle 作为 Shape 的子类,其计算面积和实际绘制时有不同的方法,但是可以抽象出相同的动作,即计算面积和绘图。
/* Shape.h */ /* Shape's attributes... */ struct ShapeVtbl; /* forward declaration */ typedef struct { struct ShapeVtbl const *vptr; /* <== Shape's Virtual Pointer */ int16_t x; /* x-coordinate of Shape's position */ int16_t y; /* y-coordinate of Shape's position */ } Shape; /* Shape's virtual table */ struct ShapeVtbl { uint32_t (*area)(Shape const * const me); void (*draw)(Shape const * const me); }; /* Shape's operations (Shape's interface)... */ ... ... static inline uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); } static inline void Shape_draw(Shape const * const me) { (*me->vptr->draw)(me); }
Shape_area
作为虚函数,对于 Shape 的子类会有很多不同的方法,这也意味着虚函数不能像普通函数那样在链接时被解析,因为要调用的实际函数需要依赖于实例的类型(Rectangle or Circle),因此,对虚函数的调用和实际实现之间的绑定必须在运行时发生,即后期绑定
(链接期间的绑定称为 早期绑定)
实际上,所有的c++ 编译器都是通过每个类的一个虚拟表(vtbl
)和每个对象的一个虚指针(vtpl
),这种方法在C语言中也能实现,C 语言中虚拟表由一个函数制作作为成员的Struct
构成
虚指针vtpr
指向类中虚函数表,该指针必须存在于每个实例(对象)中,因此它必须进入类的属性结构,如示例中,使用在顶部添加的vptr
成员来增强的 Shape 类的属性结构,值得注意的是,vtpr
被声明为const
,因为虚拟表不应该被更改,而且实际中在ROOM中被分配
虚指针vtpr
,能够被子类继承,因此Shape的子类自动拥有该属性
vtpr
虚指针vtpr
必须要初始化,以指向类的每个实例中对应的虚拟表(vtbl
),最为理想的地方是构造函数中,C++ 就是在构造函数中隐式设置,在C语言中必须显示地进行设置,如下示例在构造函数中进行显示的初始化vtpr
/* Shape's prototypes of its virtual functions */ static uint32_t Shape_area_(Shape const * const me); static void Shape_draw_(Shape const * const me); /* constructor */ void Shape_ctor(Shape * const me, int16_t x, int16_t y) { static struct ShapeVtbl const vtbl = { /* vtbl of the Shape class */ &Shape_area_, &Shape_draw_ }; me->vptr = &vtbl; /* "hook" the vptr to the vtbl */ me->x = x; me->y = y; } /* Shape class implementations of its virtual functions... */ static uint32_t Shape_area_(Shape const * const me) { assert(0); /* purely-virtual function should never be called */ return 0U; /* to avoid compiler warnings */ } static void Shape_draw_(Shape const * const me) { assert(0); /* purely-virtual function should never be called */ }
如上所示,vtbl
初始化为指向实现相应操作的函数,Shape_area_
以及Shape_draw_
如果一个类不能提供其一些虚拟函数的合理实现(因为这是一个抽象类,如Shape
一样),那么实现应该在内部断言,这样您至少知道,在运行的时候,调用了一个未实现的纯虚函数
vtbl
及在子类中重写vtpr
如果子类继承了超类,则自动拥有超类的所有的属性,通过super成员可以很好地进行。
但是,vtpr
通常需要重新分配给特定子类的vtbl
,对应的这个赋值需要在子类的构造函数中进行,如下是Rectangle
类的构造函数:
/* Rectangle's prototypes of its virtual functions */ /* NOTE: the "me" pointer has the type of the superclass to fit the vtable */ static uint32_t Rectangle_area_(Shape const * const me); static void Rectangle_draw_(Shape const * const me); /* constructor */ void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { static struct ShapeVtbl const vtbl = { /* vtbl of the Rectangle class */ &Rectangle_area_, &Rectangle_draw_ }; Shape_ctor(&me->super, x, y); /* call the superclass' ctor */ me->super.vptr = &vtbl; /* override the vptr */ me->width = width; me->height = height; } /* Rectangle's class implementations of its virtual functions... */ static uint32_t Rectangle_area_(Shape const * const me) { Rectangle const * const me_ = (Rectangle const *)me; /* explicit downcast */ return (uint32_t)me_->width * (uint32_t)me_->height; } static void Rectangle_draw_(Shape const * const me) { Rectangle const * const me_ = (Rectangle const *)me; /* explicit downcast */ printf("Rectangle_draw_(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(me), Shape_getY(me), me_->width, me_->height); }
需要注意的是,在子类的构造函数中,父类构造函数必须首先被调用,以此初始化继承的成员me->super
,这个构造器将(Shape_ctor)vptr
指向了Shape
的vtbl
,然而me->super.vptr = &vtbl;
将vtpr
重写了,使得其指向了Rectangle
的vtbl
子类虚函数实现必须与超类中定义的签名精确匹配(此处指声明中形参及返回类型),才能适应
vtbl
,例如,实现rectangle_area_()
采用的me
是Shape*
指针而不是Rectangle*
类的,子类在实现中必须显示的强制转换为对应的子类型
有了vtbl
和vtpr
的基本搭建,虚函数的调用便能够采用下面的方式实现:
uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); }
这个函数可以放在.c文件范围内,但是每个虚函数调用时,都会产生额外的函数调用开销,为了避免这种开销,可以使用static inline
直接在.h文件中定义减少开销:
static inline uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); }
或者采用宏定义的方式:
#define Shape_area(me_) ((*(me_)->vptr->area)((me_)))
无论如何,虚函数调用是首先解引用vtbl
来找到相应的vtbl
,然后只通过指向函数的指针从这个vtbl
调用适当的实现,下图给出该过程
多态的虚拟函数允许您在子类中编写非常干净具备子类特定实现的泛型代码。此外,该代码自动支持多个子类数量,可以在长期支持泛型代码开发。
#include "rect.h" /* Rectangle class interface */ #include "circle.h" /* Circle class interface */ #include <stdio.h> /* for printf() */ int main() { Rectangle r1, r2; /* multiple instances of Rectangle */ Circle c1, c2; /* multiple instances of Circle */ Shape const *shapes[] = { /* collection of shapes */ &c1.super, &r2.super, &c2.super, &r1.super }; Shape const *s; /* instantiate rectangles... */ Rectangle_ctor(&r1, 0, 2, 10, 15); Rectangle_ctor(&r2, -1, 3, 5, 8); /* instantiate circles... */ Circle_ctor(&c1, 1, -2, 12); Circle_ctor(&c2, 1, -3, 6); s = largestShape(shapes, sizeof(shapes)/sizeof(shapes[0])); printf("largetsShape s(x=%d,y=%d)\n", Shape_getX(s), Shape_getY(s)); drawAllShapes(shapes, sizeof(shapes)/sizeof(shapes[0])); return 0; }
OOP
属于设计方法而不是使用特定的语言或者工具,本文描述了如何在C语言中实现封装、继承、多态的概念。
封装和继承实现起来非常简单,而不增加任何额外的成本或者开销。
多态也能够在其中进行使用,但是想要大规模使用多态概念,还是迁移到C++中较好,但如果想构建实时库,c中oop的复杂性可以局限于库,并且可以有效对开发人员进行隐藏