文章

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

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

前言

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

本文仅用于帮助理解 OOP 的原理,如果可以,应该尽可能使用 C++ 来实现 OOP,与 C++ 相比,C 中的 OOP 可能很麻烦且容易出错,并且几乎没有性能优势

面向对象特性

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

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

封装

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

要点:

  • 函数封装对对象内部变量的修改和获取,调用者只需关注函数参数和返回值

封装的写法

假设有如下结构体:

1
2
3
4
5
/* Shape's attributes... */
typedef struct {
    int16_t x; /* x-coordinate of Shape's position */
    int16_t y; /* y-coordinate of Shape's position */
} Shape;

要实现对它的构造:

1
2
Shape shape1{1, 2};
Shape shape1{.x = 1, .y = 2};

对它的访问和修改:

1
2
int16_t a = shape1.x;
shape1.y = 3;

对它的操作,比如移动其位置:

1
2
3
4
5
int16_t moveX = 5;
int16_t moveY = 6;
// 同时对Shape的x和y做修改
shape1.x += moveX;
shape1.y += moveY;

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.h(c++版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef SHAPE_H
#define SHAPE_H

class Shape {
private:
    int16_t x;
    int16_t y;
public:
    Shape(int16_t x, int16_t y):x(x),y(y){}
    void moveBy(int16_t dx, int16_t dy);
    int16_t getX();
    int16_t getY();
};

调用者只要引用头文件,而无需关心函数的实现以及 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;
}

C++中类的成员函数其实都包含了一个名为 this 的隐式指针参数,这里的 me 指针就是用于这个目的。

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++ 中一样,我们在 main.c 中构造了对象,使用对象的方法对其操作。

封装的意义

打包

把相关的数据放在一个结构体内,提高代码复用性。

1
2
3
4
5
6
7
int16_t shape1_x = 1;
int16_t shape1_y = 2;
int16_t shape2_x = 1;
int16_t shape3_y = 2;
int16_t shape2_y = 1;
// int16_t shape3_x = 2; // 漏定义
int16_t shape2_z = 2;    // 冗余定义

x,y 打包后:

1
2
3
4
5
6
7
typedef struct {
    int16_t x;
    int16_t y;
} Shape;
Shape shape1{1, 2};
Shape shape2{1, 2};
Shape shape3{2, 1};

隐藏复杂细节

在实际代码开发中,我们经常会把复杂的操作用一个函数封装起来,对使用者隐藏细节,使用者仅需提供简单的输入,就能得到所需的输出。

比如 crc16 的计算,使用者无需知道 crc 算法,只需提供数据,即可得到 uint16_t 类型的 crc16 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdint.h>
#include <stdio.h>
uint16_t crc16_bitwise(const uint8_t *data, size_t len) {
    uint16_t crc = 0xFFFF; // MODBUS 初始值

    for (size_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

这就是面向过程的封装,对于面向对象来说还要将数据也封装在一起。

之前的 Shape 例子中,x 和 y 相对简单,如果他们是复杂的结构,比如是用两个整型表示一个浮点型的结构:

1
2
3
4
5
6
7
8
9
typedef struct {
  int16_t integer;   // 整数部分(可为负)
  uint16_t fraction; // 小数部分
} FloatValue;

typedef struct {
  FloatValue x;
  FloatValue y;
} Shape;

使用者不仅需要了解 Shape 的结构,还需要了解 FloatValue 结构,对它们的修改难度较大。实际上,使用者只想要按整数精度移动 Shape 的位置,无需知道相关细节,可以让 Shape 提供一个移动函数:

1
2
3
4
5
void Shape_move(Shape* me, int16_t dx, int16_t dy)
{
  me->x.integer += dx; // x的整数部分增加dx值,小数部分不变
  me->y.integer += dy;
}

保证数据合法性

如果直接修改对象内的数据,可能导致数据不合法。

1
shape1.x = 10000;

通过函数封装修改操作,可以增加如合法性检查的操作:

1
2
3
4
5
6
7
8
9
10
11
bool Shape_setX(Shape* me, uint16_t x)
{
    assert(x <= 10); // x 必须小于等于 10
    me->x = x;
    return true;
}

int main()
{
    Shape_setX(&shape1, 10000); // 不合法
}

业务需求变化时,也能方便的添加判断条件,不影响调用者:

1
2
3
4
5
6
7
8
9
10
11
12
13
bool Shape_setX(Shape* me, uint16_t x)
{
    if(y <= 10)
    {
        assert(x <= 10);
    }
    else
    {
        assert(x <= 100); // 添加扩展的判断条件:在特定情况下,x 可以大于 10,但必须小于等于 100
    }
    me->x = x;
    return true;
}

有时 x 和 y 具有联系,不能单独修改其中一个值,可以不提供 setX 函数,只能通过 moveBy 函数同时对 x 和 y 做修改。

继承

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

基础的实现

通过将基类属性结构嵌入为派生类属性结构,可以轻松地在 C 中实现单继承。

这样派生类也包含了基类的所有特性,但较难实现 C++语言层级的选择性继承多继承等特性。

Inheritance

如图所示,super 对象被指定为派生类对象中第一个成员,其起始地址与派生类对象地址相同,可以通过调用->super或通过基类Shape指针访问 Rectangle 对象中的继承部分(super),这一点是为了后面实现多态特性,如果不考虑多态,则 super 对象可以放在派生类中的任何位置,此时只能通过->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;
}

继承的意义

  • 代码复用:保留了父类的成员定义,可以直接使用,无需重复再写一遍。
  • 行为继承:通过继承,所有继承 Shape 的类都保留了 Shape_moveBy 的行为,通过直接调用该函数就能实现图形移动。

多态

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

多态要解决的问题

假设 Rectangle 想要覆盖基类 Shape 的 moveBy 函数实现:

1
2
3
4
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) {
    me->x += dx * 2; // 移动两倍的距离
    me->y += dy * 2;
}

因为行为不同,只能另写一个:

1
2
3
4
void Rectangle_moveBy(Shape * const me, int16_t dx, int16_t dy) {
    me->x += dx * 2; // 移动两倍的距离
    me->y += dy * 2;
}

如果还有一个名为 Circle 的派生类,还要再另写一个:

1
2
3
4
void Circle_moveBy(Shape * const me, int16_t dx, int16_t dy) {
    me->x += dx / 2; // 移动一半的距离
    me->y += dy / 2;
}

这样的问题是可维护性差,后续新增派生类或替换其他派生类时,相应调用 moveBy 的地方都要重新修改:

1
2
3
4
5
6
7
8
9
int main()
{
    Rectangle shape;
    // Circle shape;

    // 因为只有 Rectangle 一种情况,写死为 Rectangle
    // 后续新增 Circle 并替换时就适应不了了
    Rectangle_moveBy(shape.super);
}

能不能做到只用一个函数,就能根据派生类的不同,自动选择不同的实现?:

1
2
3
4
5
6
7
8
9
10
11
12
13
int virtual_moveBy(Shape * const shape)
{
    // 如果 shape 属于 Rectangle,则自动调用 Rectangle_moveBy
    // 如果 shape 属于 Circle,则自动调用 Circle_moveBy
    // 否则,自动调用 Shape_moveBy
}
// 改为
int main()
{
    Rectangle shape;
    // Circle shape;
    virtual_moveBy(shape.super);
}

这样,后面新增派生类的时候,该接口也无需修改。

多态的实现

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

Polymorphism

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

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

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

为了实现多态,我们需要将虚成员函数的函数指针放在表示类的结构体中,同时因为所有对象都共享这些成员函数,我们可以使用虚函数表(ShapeVtbl)的方式减少函数指针的空间占用(把多个函数指针压缩成一个虚表指针,防止每个派生类的对象都包含多个函数指针,其实对于同一个派生类,每个对象的函数指针都指向同一个地方(指针的值相同),这样会浪费空间):

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++的实现其实也使用了同样的方法,所有 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 指向该虚表

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

覆盖基类的实现

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

派生类 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);
}

虚函数调用(后绑定)

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

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-SA 4.0 进行授权

© Kai. 保留部分权利。

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