文章

面向对象编程(OOP)的C语言实现

前言

阅读本文章前首先要具备一定的面向对象编程基础

面向对象特性

面向对象编程 (Object-oriented programming,OOP) 是一种基于以下三个基本概念的设计方式:

封装(Encapsulation)
将数据和函数打包到类中的能力
继承(Inheritance)
基于现有类定义新类的能力,以获得重用和代码组织
多态(Polymorphism)
在运行时将匹配接口的对象相互替换的能力

封装

即对调用者隐藏实现和非必要的内部属性

要点:

  • 头文件(.h)与实现文件(.c)分离,调用者只需引入头文件
  • 函数封装对对象内部变量的修改和获取,调用者只需关注函数参数和返回值

shape.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef SHAPE_H
#define SHAPE_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 me);
int16_t Shape_getY(Shape * const me);

#endif /* SHAPE_H */

调用者只要引用头文件,而无需关心函数的实现以及Shape内的变量

shape.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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;
}

//用get函数封装了对x和y变量的值的获取
//(当然这样做不到C++语言级别的private关键字,
// 使用者还是能通过Shape对象直接访问这两个变量)
/* "getter" operations implementation */
int16_t Shape_getX(Shape * const me) {
    return me->x;
}
int16_t Shape_getY(Shape * const me) {
    return me->y;
}

me指针在C++中就是this隐式指针

main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "shape.h"  /* Shape class interface */
#include   /* for printf() */

int main() {
    Shape s1, s2; /* multiple instances of Shape */

    Shape_ctor(&s1, 0, 1);
    Shape_ctor(&s2, -1, 2);

    printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1));
    printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2));

    Shape_moveBy(&s1, 2, -4);
    Shape_moveBy(&s2, 1, -2);

    printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1));
    printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2));

    return 0;
}

继承

继承是基于现有类定义新类以重用和组织代码的能力

通过将基类属性结构嵌入为派生类属性结构,可以轻松地在 C 中实现单继承。这样派生类也包含了基类的所有特性,但较难实现C++语言层级的选择性继承多继承等特性。

如果只需实现继承,则 super 对象可以放在派生类中的任何位置,使用时调用->super即可访问基类部分。

Inheritance

如图所示,super 对象被指定为派生类对象中第一个成员,其起始地址与派生类对象地址相同,可以通过调用->super方式访问继承部分,也可以通过基类Shape指针即可访问Rectangle对象中的继承部分(super),这一点是为了后面实现多态特性

rect.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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 */

rect.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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;
}

main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "rect.h"  /* Rectangle class interface */
#include   /* for printf() */

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",
           r1.super.x, r1.super.y, r1.width, r1.height);
    printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n",
           r2.super.x, r2.super.y, 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",
           r1.super.x, r1.super.y, r1.width, r1.height);
    printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n",           r2.super.x, r2.super.y, r2.width, r2.height);

    return 0;
}

对于基类函数的’this’参数可以使用(Shape *)&r&r.super

多态

多态性是在运行时将匹配接口的对象相互替换的能力

C++使用虚函数实现多态性。

Polymorphism

在C语言中也可以为Shape类添加几个“虚函数”,并由派生类实现

核心是让派生类对象能通过统一的接口函数调用已被自己继承并实现的基类的函数。通过上一节继承中提到的利用基类指针指向基类部分的特点,可以让不同的派生类的对象都通过统一的强制转换指针操作实现对基类中函数指针的访问,利用不同的构造函数让不同的派生类的对象的该指针指向的函数不同,从而实现多态。

虚拟表 (vtbl) 和虚拟指针 (vptr)

虚拟表和多态关系不大,主要是为减少为了实现OOP引入的函数指针带来的空间代价

shape.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#ifndef SHAPE_H
#define SHAPE_H

#include

/* 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)... */
void Shape_ctor(Shape *const me, int16_t x, int16_t y);
void Shape_moveBy(Shape *const me, int16_t dx, int16_t dy);

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);
}

/* generic operations on collections of Shapes */
Shape const *largestShape(Shape const *shapes[], uint32_t nShapes);
void drawAllShapes(Shape const *shapes[], uint32_t nShapes);

#endif /* SHAPE_H */

虚函数可以直接定义为函数指针,这里是使用了虚函数表(ShapeVtbl),把多个函数指针压缩成一个虚表指针,防止每个派生类的对象都包含多个函数指针,其实对于同一个派生类,每个对象的函数指针都指向同一个地方(指针的值相同),这样会浪费空间

所有C++编译器都通过每个类一个虚拟表 (vtbl) 和每个对象一个虚拟指针 (vptr) 来实现后绑定

在构造函数中设置 vptr

让不同的派生类的继承函数指向差异化的实现,这是实现多态的重要一步

基类Shape实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "shape.h"
#include

/* 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;
}

/* move-by operation */
void Shape_moveBy(Shape *const me, int16_t dx, int16_t dy)
{
    me->x += dx;
    me->y += dy;
}

/* 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 */
}

/* the following code finds the largest-area shape in the collection */
Shape const *largestShape(Shape const *shapes[], uint32_t nShapes)
{
    Shape const *s = (Shape *)0;
    uint32_t max = 0U;
    uint32_t i;
    for (i = 0U; i < nShapes; ++i)
    {
        uint32_t area = Shape_area(shapes[i]); /* virtual call */
        if (area > max)
        {
            max = area;
            s = shapes[i];
        }
    }
    return s; /* the largest shape in the array shapes[] */
}

/* The following code will draw all Shapes on the screen */
void drawAllShapes(Shape const *shapes[], uint32_t nShapes)
{
    uint32_t i;
    for (i = 0U; i < nShapes; ++i)
    {
        Shape_draw(shapes[i]); /* virtual call */
    }
}

Shape类有自己的虚表vtbl,构造对象时需要将虚指针vptr指向该虚表

虚函数内使用断言的目的就是表示该函数不应该被调用,是一个纯虚函数

继承 vtbl 并覆盖子类中的 vptr

用自己的实现覆盖基类的实现(空间覆盖)

派生类Rectangle实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "rect.h" /* Rectangle class interface */
#include          /* for printf() */

/* 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",
           me_->super.x, me_->super.y, me_->width, me_->height);
}

派生类构造函数中应该覆盖基类中的虚指针vptr,指向自己的虚表(ShapeVtbl)对象vtbl

虚函数调用(后绑定)

通过统一接口实现不同派生类的虚函数实现的调用

1
2
3
4
5
6
7
8
/* C99 */
// 通过内联函数减少调用性能开销
static inline uint32_t Shape_area(Shape const * const me) {
    return (*me->vptr->area)(me);
}

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

vptr

虚函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "rect.h"   /* Rectangle class interface */
#include "circle.h" /* Circle class interface */
#include            /* 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;
}

将不同的派生类对象视为相同的类型,从而使用数组保存,并用相同的接口进行操作

总结

用C实现的多态较为复杂,最好还是使用语言层级支持OOP的C++

参考

本文由作者按照 CC BY 4.0 进行授权

© Kai. 保留部分权利。

浙ICP备20006745号-2,本站由 Jekyll 生成,采用 Chirpy 主题。