C语言中的多态

C++这类面向对象语言的多态是建立在基类及派生类之间继承关系的基础上的。C不支持继承,所以我们得模拟结构体之间的继承。我们会定义和使用两个结构体来说明多态行为。Shape结构体表示基“类”,而Rectangle结构体表示从基类Shape派生的类。

结构体的变量分配顺序对这种技术的工作原理影响很大。当我们创建一个派生类/结构体的实例时,会先分配基类/结构体的变量,然后分配派生类/结构体的变量。我们也需要考虑打算覆盖的函数。

注意 理解从类实例化来的对象如何分配内存是理解面向对象语言中继承和多态工作原理的关键。我们在C中使用这种技术时,这一点仍然适用。

让我们先从Shape结构体的定义开始,如下所示。首先,我们分配一个结构体来为Shape结构体持有函数指针,接着是表示xy坐标的整数:

typedef struct _shape {
    vFunctions functions;
    // 基类变量
    int x;
    int y;
} Shape;

我们将vFunctions结构体及其支持函数声明定义如下。当对一个类/结构体执行函数时,其行为取决于它所作用的对象是什么。比如说,对Shape调用打印函数就会显示一个Shape,对Rectangle调用打印函数就会显示Rectangle。在面向对象编程语言中这通常是通过虚表(或者VTable)实现的。vFunctions结构体就是用来实现这种功能的:

typedef void (*fptrSet)(void*,int);
typedef int (*fptrGet)(void*);
typedef void (*fptrDisplay)();

typedef struct _functions {
    // 函数
    fptrSet setX;
    fptrGet getX;
    fptrSet setY;
    fptrGet getY;
    fptrDisplay display;

} vFunctions;

这个结构体由一系列函数指针组成。fptrSetfptrGet函数指针为整数类型数据定义了典型的gettersetter函数。在这种情况下,它们用来获取和设置ShapeRectanglexy值。fptrDisplay函数指针定义了一个参数为空、返回值为空的函数,我们会用这个打印函数解释多态行为。

Shape结构体有四个函数跟它协同工作,如下所示。它们的实现很直观。为了让示例简单,在display函数中,我们只是打印出了字符串"Shape",我们会把Shape实例作为第一个参数传递给这些函数,这样可以让这些函数处理多个Shape实例:

void shapeDisplay(Shape *shape) { printf("Shape\n");}
void shapeSetX(Shape *shape, int x) {shape->x = x;}
void shapeSetY(Shape *shape, int y) {shape->y = y;}
int shapeGetX(Shape *shape) { return shape->x;}
int shapeGetY(Shape *shape) { return shape->y;}

为了辅助创建Shape实例,我们提供了getShapeInstance函数,它为对象分配内存,然后为其设置函数:

Shape* getShapeInstance() {
    Shape *shape = (Shape*)malloc(sizeof(Shape));
    shape->functions.display = shapeDisplay;
    shape->functions.setX = shapeSetX;
    shape->functions.getX = shapeGetX;
    shape->functions.setY = shapeSetY;
    shape->functions.getY = shapeGetY;
    shape->x = 100;
    shape->y = 100;
    return shape;
}

下面的代码说明了这些函数的使用方法:

Shape *sptr = getShapeInstance();
sptr->functions.setX(sptr,35);
sptr->functions.display();
printf("%d\n", sptr->functions.getX(sptr));

输出如下:

Shape
35

看起来为了实现Shape结构体这么做有点大费周章,但是一旦我们从Shape派生出一个Rectangle结构体,就会看到这么做的强大能力。这个结构体如下所示:

typedef struct _rectangle {
    Shape base;
    int width;
    int height;
} Rectangle;

Rectangle第一个字段的内存分配和Shape结构体一样,如下图所示。此外,我们还加入了两个新字段widthheight,来表示长方形的属性。

Shape和Rectangle的内存分配

Shape类似,Rectangle需要关联一些函数,声明如下。除了用的是Rectanglebase字段,它们跟Shape结构体关联的函数一样。

void rectangleSetX(Rectangle *rectangle, int x) {
    rectangle->base.x = x;
}

void rectangleSetY(Rectangle *rectangle, int y) {
    rectangle->base.y;
}

int rectangleGetX(Rectangle *rectangle) {
    return rectangle->base.x;
}

int rectangleGetY(Rectangle *rectangle) {
    return rectangle->base.y;
}

void rectangleDisplay() {
    printf("Rectangle\n");
}

getRectangleInstance函数返回一个Rectangle结构体的实例,如下所示:

Rectangle* getRectangleInstance() {
    Rectangle *rectangle = (Rectangle*)malloc(sizeof(Rectangle));
    rectangle->base.functions.display = rectangleDisplay;
    rectangle->base.functions.setX = rectangleSetX;
    rectangle->base.functions.getX = rectangleGetX;
    rectangle->base.functions.setY = rectangleSetY;
    rectangle->base.functions.getY = rectangleGetY;
    rectangle->base.x = 200;
    rectangle->base.y = 200;
    rectangle->height = 300;
    rectangle->width = 500;
    return rectangle;
}

下面说明这个结构体的用法:

Rectangle *rptr = getRectangleInstance();
rptr->base.functions.setX(rptr,35);
rptr->base.functions.display();
printf("%d\n", rptr->base.functions.getX(rptr));

输出如下:

Rectangle
35

现在创建一个Shape指针的数组,然后像下面这样初始化。当我们把Rectangle赋给shapes[1]时,其实没有必要非得把它转换成(Shape *),但是不这么做会产生警告:

Shape *shapes[3];
shapes[0] = getShapeInstance();
shapes[0]->functions.setX(shapes[0],35);
shapes[1] = getRectangleInstance();
shapes[1]->functions.setX(shapes[1],45);
shapes[2] = getShapeInstance();
shapes[2]->functions.setX(shapes[2],55);

for(int i=0; i<3; i++) {
    shapes[i]->functions.display();
    printf("%d\n", shapes[i]->functions.getX(shapes[i]));
}

执行这段代码后会得到如下输出:

Shape
35
Rectangle
45
Shape
55

创建Shape指针的数组过程中,我们创建了一个Rectangle实例并将其赋给数组的第二个元素。当我们在for循环中打印元素时,它会用Rectangle的函数行为而不是Shape的,这就是一种多态行为。display函数的行为取决于它所执行的对象。

我们是把它当成Shape来访问的,因此不应该试图用shapes[i]来访问其宽度和高度,原因是这个元素可能引用一个Rectangle,也可能不是。如果我们这么做,就可能访问shapes的其他内存,那些内存并不代表宽度和高度信息,会导致不可预期的结果。

现在我们也可以再从Shape中派生一个结构体,比如Circle,把它加入数组,而不需要大量修改代码。我们也需要为这个结构体创建函数。

如果我们给基结构体Shape增加一个函数,比如getArea,就可以为每一个类实现一个唯一的getArea函数。在循环中,我们可以轻易地把所有ShapeShape派生的结构体的面积累加,而不需要先判断我们处理的是什么类型的Shape。如果ShapegetArea实现足够了,那么我们就不需要为其他结构体增加函数了。这样很容易维护和扩展一个应用程序。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程