想用C来实现OOP,关键在于结构体。struct和OOP中的class最大区别为默认的继承访问权限:struct是public的,大家都能看到,class是private的,只有指定的对象看得到。
码农翻身里面有一篇文章讲到过用c语言来实现OOP,今天参照着它撸了一下oop的三大概念:封装、继承、多态。
1 封装:意思就是把信息隐藏起来。
举个例子,先创建一个shape结构体,然后实现create和init等方法,之后将源代码封装成库。这样一来,外部程序只能看见头文件的接口原型,而看不到其内部结构。
shape.h
//声明结构体
struct Shape;
//api原型
struct Shape *Shape_create(int x,int y);
void Shape_init(struct Shape *self, int x,int y);
void Shape_move(struct Shape *self, int x,int y);
float Shape_area(struct Shape *self);
...
shape.c
struct Shape{
int x;
int y;
};
struct Shape *Shape_create(int x, int y){
struct Shape *s = malloc(sizeof(struct Shape));
s->x= x;
s->y= y;
return s;
}
void Shape_move(struct Shape *self, int x,int y)
{
self->x= x;
self->y= y;
}
float Shape_area(struct Shape *self){
return (*self->vptr->area)(self); //vptr成员后面会新增进去
}
main.c中调用api
int main(int argc, *argv[]){
...
struct Shape *s = Shape_create(0,0);
Shape_move(s,1,2);
...
}
虽然结构体成员和create、move等方法是分开写的,看起来没class那么融合,可也算是实现了简单的封装。
2 继承
先看下面代码:
//Rectangle子类
struct Rectangle{
struct Shape base;
int length;
int width;
};
struct Rectangle *Rectangle_create(int x,int y,int l, int w){
struct Rectangle *r = malloc(sizeof(struct Rectangle)); //创建对象
Shape_init((struct Shape *)r,x,y); //继承了Shape基类的成员函数:shape_init
r->length = l;
r->width = w;
return r;
}
...
int main(){
struct Rectangle *r = Rectangle_create(1,2,30,40);
Shape_move((struct Shape*)r,10,20);
...
}
上述代码创建了另一个结构体Rectangle。因其包含了Shape,所以称之为子类。其在内存中是这样的:
继承关系如下:
有两个地方需要注意:
- 在继承Shape_init这个api时,传进去的对象为 r,此时需要类型转换为Shape基类。
- 在Rectangle子类中Shape基类的位置需放在最前。下面举个例子说明一下:
struct rectangle{
int length;
int width;
struct shape base; //此处放在最后,会有问题
};
...
struct rectangle *r = malloc(sizeof(struct rectangle));
struct shape *tmp = (struct shape *)r; //强制类型转换
tmp->x=110;
上述代码中 将Shape类放在了最后。本来是将110赋值给tmp 的 x成员,实际却是给了length成员,调试结果如下:
(r和tmp的地址一样,只是里头的数据代表的类型不同)
3 多态
多态最难搞,关键在于理解函数指针的使用。先上代码:
//新建虚函数表
struct shapeVtbl{
float (*area)(struct shape *self);
void (*draw)(struct shape *self);
}
//将虚函数表添加到Shape基类中
struct shape{
struct shapevtbl *vptr;
int x;
int y;
};
新结构体shapeVtbl 包含两个函数指针,目的是为了获取对象的面积及画图,我们称其为虚函数表。之后不管创建的子类是Rectangle 还是其它的,只要是继承自Shape基类,都会有个虚函数表。
下面是Rectangle 和Square对象。前者的函数指针area指向的是自家的 Rectangle_area();而Square对象的函数指针area指向的是Square_area()。
开头部分的shape.h中有个api为Shape_area(...),在子类继承之后,调用它就可以直接指向自家的api地址。如下:
float Area;
...
struct Rectangle *r = Rectangle_create(1,2,30,40);
Area = Shape_area((struct Shape*)r);
注意:上图中的虚函数表需要你手动将自家的函数入口地址填进去,比如 &Rectangle_area 和 &Rectangle_draw,而C++则可以自动完成。