C语言于嵌入式项目里所占比例颇高,然而维护老旧代码的那种痛苦,众多程序员皆是深有感受的。一项功能发生改动,常常是一处变动会引发全局变动。这般情况恰好正是由于纯面向过程的编写方式,当代码规模有所增大之后,其耦合程度过高所致。要是能够运用C语言去达成面向对象的思路,那么便能够在留有C语言执行效率的当儿,大幅削减后续的维护成本。
C语言虽不存在class关键字,然而结构体理所当然是用以进行封装的。我们能够将相关的数据,像一个LED灯的引脚号码,以及状态,统统置于一个结构体当中。如此便达成了数据的聚合,此乃封装的首个步骤。
再进一步而言,我们能够将用于操作这个LED灯的函数指针这一事物,也放置到同一个结构体当中。比如说,去定义一个名为“open”以及“close”的函数指针。如此一来,这个结构体便同时具备了属性以及行为,从外界的视角来看,它已然极为类似于一个“类”了。
在C语言当中去谈论继承这个事儿,听起来好像真好似有些天方夜谭模样了,然而呢我们能够采用组合这种方式去模拟出呈现出“is-a”这样一种关系。其核心技巧便在于将那个“基类”的结构体放置于那个“子类”结构体的最顶端位置处。比如说去规定设定一个Shape结构体,此结构体之中涵盖包含坐标方面的信息。
那当我们去定义那个Circle结构体的时候,头一个成员变量放置一个属于Shape类型的变量。如此这般,在内存布局方面,Circle对象的起始地址跟它的Shape部分的起始地址是全然一致的。任何对Shape进行操作的函数,都能够直接去操作Circle对象当中的这一部分。
// Code List(1)
ABSTRACT struct Shape
{
PUBLIC ABSTRACT double (*getArea)(void* const shape);
PUBLIC ABSTRACT double (*print)(void* const shape);
};
struct Circle EXTERNS(Shape)
{
PUBLIC struct Shape shape;
PRIVATE double Radius;
PUBLIC double (*getRadius)(struct Circle* const cicle);
};
struct Rectangle EXTERNS(Shape)
{
struct Shape shape;
PRIVATE double Length;
PRIVATE double Width;
PUBLIC double (*getLength)(struct Rectangle* const rect);
PUBLIC double (*getWidth)(struct Rectangle* const rect);
};
#define PUBLIC
#define PRIVATE
#define ABSTRACT
#define EXTERNS(super)
多态堪称面向对象的核心要义,意思为“同一接口,多样实现”。C 语言并没有虚函数表,然而函数指针赋予了我们动态绑定的本领。我们能够于“基类”Shape的结构体里头,规定一个函数指针,就像void (draw)(Shape)。
要初始化那个名为Circle的对象,得使这个函数指针指向一个专门用于绘制圆形的函数,便是drawCircle。一旦外部代码借助Shape的指针调用draw方法,实际上执行的乃是drawCircle。如此一来便达成了运行时多态,也就是在面向对象领域中提及的“一个接口,多种形态”。
让模拟面向对象的代码看上去更直观些,我们能够借助宏定义去“伪装”成关键字,比如,定义#define CLASS typedef struct ,定义#define PUBLIC当作公共成员的标记,尽管本质仍是C语法,不过可读性大幅增强了。
在头文件里头,我们能够借助这些宏去声明一个类,举例来说,声明一个名为Rectangle的类,它从Shape那里“继承”而来,而且具备自身独特的宽度以及高度属性。于此同时,在结构体当中声明了draw函数指针,这恰似声明了一个虚函数。这样的一种约定能够使得团队成员一眼就明晰代码的设计意图。
#define NEW
Rectangle* rec = NEW rectangle(12,25);
模拟了类之后,那么自然而然就要去考量对象究竟该如何进行创建。C语言并不具备构造函数,针对这种状况我们能够定义一个跟类名同名称的函数(全部字母小写)去实施模拟,就像void shape_init(Shape self, int x, int y)这样,借助它来对对象的成员以及函数指针予以初始化。
ShapeFactory.createRectangle(12,25);
ShapeFactory.createCircle(7.12);
ShapeFactory.createSquareRectangle(11.3);
建立对象之际,我们能够去定义一个宏,其为#define NEW(type) (type*)malloc(sizeof(type)) ,以此来模仿new关键字;紧接着呢要手动去调用对应的展开初始化操作的函数;像这般就达成了“一开始先行去分配用的内存,此后又调用用来构建对象样子的程序”这般一个完整的过程顺序,虽说相较于C++而言会更加繁杂一些,不过其逻辑可是百分之百自身连贯且前后一致的。
typedef struct Rectangle* IRectangle;
常面向对象设计凭借接口界定规范,于C语言内里,能够定义含函数指针的结构体施行接口模拟,就像定义Drawable结构体,其中仅有draw函数指针,而缺少任何类型的数据成员。
IRectangle rec;
任何“一类”若要达成这个接口,仅需于自身结构体之中涵盖一个为Drawable类型的成员,并且将自身的绘图函数赋予此成员。此模式于Linux内核的驱动开发里极为常见,它精妙地展现出“面向接口编程”的理念,达成了高内聚低耦合。
/* circle
*
*/
static void installCircleMethods(ICircle c);
static ICircle createCircle(double radius)
{
ICircle c = (ICircle)malloc(sizeof(struct Circle));
c->Radius = radius;
installCircleMethods(c);
return c;
}
static double getCircleArea(ICircle c)
{
return 3.14 * c->Radius * c->Radius;
}
static void printCircle(ICircle c)
{
printf("Circle:radius = %g/n", c->Radius);
}
static double getCircleRadius(ICircle c)
{
return c->Radius;
}
static void installCircleMethods(ICircle c)
{
((IShape)c)->getArea = getCircleArea;
((IShape)c)->print = printCircle;
c->getRadius = getCircleRadius;
}
C语言里能找到面向对象三大特性得以落地的办法之处,这不只是一种编程的技巧,更是一种系统架构思维方面的转变。于实际项目当中,你有没有试着运用结构体以及函数指针去组织代码呢?碰到过哪些方面的坑呢?欢迎在评论区域分享你的相关经验,点赞并且收藏此文,方便往后进行查阅。
c = ShapeFactory.createCircle(3.5);
printf("area of c=%g/n", c->shape.getArea(c));
((IShape)c)->print(c);
printf("Radius of c=%g/n",c->getRadius(c));