C++这类面向对象语言的多态是建立在基类及派生类之间继承关系的基础上的。C不支持继承,所以我们得模拟结构体之间的继承。我们会定义和使用两个结构体来说明多态行为。Shape
结构体表示基“类”,而Rectangle
结构体表示从基类Shape
派生的类。
结构体的变量分配顺序对这种技术的工作原理影响很大。当我们创建一个派生类/结构体的实例时,会先分配基类/结构体的变量,然后分配派生类/结构体的变量。我们也需要考虑打算覆盖的函数。
注意 理解从类实例化来的对象如何分配内存是理解面向对象语言中继承和多态工作原理的关键。我们在C中使用这种技术时,这一点仍然适用。
让我们先从Shape
结构体的定义开始,如下所示。首先,我们分配一个结构体来为Shape
结构体持有函数指针,接着是表示x
和y
坐标的整数:
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;
这个结构体由一系列函数指针组成。fptrSet
和fptrGet
函数指针为整数类型数据定义了典型的getter
和setter
函数。在这种情况下,它们用来获取和设置Shape
或Rectangle
的x
和y
值。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
结构体一样,如下图所示。此外,我们还加入了两个新字段width
和height
,来表示长方形的属性。
跟Shape
类似,Rectangle
需要关联一些函数,声明如下。除了用的是Rectangle
的base
字段,它们跟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
函数。在循环中,我们可以轻易地把所有Shape
和Shape
派生的结构体的面积累加,而不需要先判断我们处理的是什么类型的Shape
。如果Shape
的getArea
实现足够了,那么我们就不需要为其他结构体增加函数了。这样很容易维护和扩展一个应用程序。