我不我不知道有多少人已经了解了语言发展的历史。早期C语言的语法功能其实比较简单。随着应用需求和场景的变化,C语言的语法功能也在不断升级和变化。

虽然我们的教材上有这样的结论:C语言是面向过程的语言,C语言是面向对象的编程语言,但是面向对象的概念是在C语言阶段产生的,并且已经应用到很多地方,比如一些操作系统内核,通信协议等等。

面向对象编程,即OOP(Object Oriented Programming)不是一种特定的语言或工具,而是一种设计方法和思想。它的三个基本特征是封装性、继承性和多态性。

为什么有些读者在使用C语言实现面向对象阅读之前一定要问这个问题:我们有C语言为什么还需要使用C语言实现面向对象阅读?

c语言这种非面向对象的语言,也可以用面向对象的思想来写程序。用面向对象的C语言更容易实现面向对象编程,但C语言的效率是其他面向对象编程语言无法比拟的。

当然,用C语言实现面向对象的开发相对来说比较难理解,这也是为什么大多数人学过C语言却能看不懂Linux内核源代码。

所以这个问题很好理解。对C语言编程有一定经验的读者应该能理解,与面向对象的C语言相比,面向过程的C语言在代码运行效率和代码量上有很大的差异。在性能差、资源少的单片机中使用C语言进行面向对象编程显得尤为重要。

如果想用C语言实现面向对象,首先需要有一些基础知识。例如:结构、函数、指针、函数指针(C语言中)、基类、派生、多态、继承等。(在C语言中)。

首先,它it’不仅仅要知道这些基础知识,还要有一定的编程经验,因为上面说面向对象是一种设计方法和设计思想。如果你只是停留在字面上理解,你可以没有这个设计理念是不行的。

所以不建议初学者使用C语言实现面向对象,尤其是在真实项目中。建议把基本功练好再用。

C语言实现面向对象的方法有很多种。这里描述最基本的封装、继承和多态。

封装是将数据和函数打包到一个类中。其实大部分C语言程序员都已经接近了。

C标准库中fopen()、fclose()、fread()、fwrite()等函数的操作对象都是FILE。内容是文件,数据的读写操作是fread()和fwrite()。fopen()类似于构造函数,fclose()是析构函数。

这似乎很容易理解,所以让实现基本的封装特性。

# ifndefshape _ h #的属性typedef struct { int 16 _ t x } define shape _ h # include//shape;int 16 _ t y;}形状;//shape的操作函数,接口函数void shape _ ctor (shape * constme,int16 _ t x,int 16 _ t y);void Shape_moveBy(Shape * const me,int16_t dx,int 16 _ t dy);int 16 _ t Shape _ getX(Shape const * const me);int 16 _ t Shape _ getY(Shape const * const me);#endif 这是SHAPE类的声明,非常简单易懂。一般来说,语句会放在头文件Shape.h 。让让我们看看Shape类的定义,它在形状. 当然了。#包括hape.h //构造函数void shape _ ctor (shape * const me,int16 _ t x,int 16 _ t y){ me-x=x;me-y=y;} void Shape _ move by(Shape * const me,int16_t dx,int 16 _ t dy){ me-x=dx;me-y=dy;}//获取属性值函数int 16 _ t shape _ getx(shape const * const me){ return me-x;} int 16 _ t Shape _ getY(Shape const * const me){ return me-y;}看看main.c # include shape.h # includeint main(){ shapes 1,S2;Shape _ ctor(S1,0,1)的多个实例;Shape_ctor(s2,-1,2);printf(形状s1(x=%d,y=% d)Shape_getX(s1),Shape _ getY(S1));printf(形状s2(x=%d,y=% d)Shape_getX(s2),Shape _ getY(S2));Shape_moveBy(s1,2,-4);Shape_moveBy(s2,1,-2);printf(形状s1(x=%d,y=% d)Shape_getX(s1),Shape _ getY(S1));printf(形状s2(x=%d,y=% d)Shape_getX(s2),Shape _ getY(S2));返回0;}编译后看执行结果:shapes1 (x=0,y=1) shapes2 (x=-1,y=2) shapes1 (x=2,y=-3) shapes2 (x=0,y=0)。整个例子非常简单易懂。以后写代码的时候,要多想想标准库的文件IO操作,这样才能有意识地培养面向对象编程的思维。

继承是在现有类的基础上定义一个新的类,有助于重用代码,更好地组织代码。在C语言中,实现单继承也很简单,只需将基类放在被继承类的第一个数据成员的位置即可。

比如我们现在要创建一个Rectangle类,只需要继承Shape类已有的属性和操作,然后在Rectangle中加入不同于Shape的属性和操作。

以下是矩形的陈述和定义:

# ifndef rect _ h # define rther _ h # include hape.h //基类接口//矩形typedef struct { Shape super的属性;//继承Shape //own属性uint16 _ t widthuint16_t高度;}长方形;//构造函数void rectangle _ ctor(rectangle * const me,int16 _ t x,int16 _ t y,uint16 _ t width,uint 16 _ t height);# endif# include rect.h //构造函数void rectangle _ ctor(rectangle * const me,int16 _ t x,int16 _ t y,uint16 _ t width,uint16_t height){ Shape_ctor(me-super,x,y);me-width=宽度;me-height=身高;}让让我们来看看Rectangle的继承关系和内存布局:

由于这种内存布局,您可以安全地将指向矩形对象的指针传递给期望传递指向Shape对象的指针的函数,也就是说,函数的参数是形状* ,你就可以进去矩形* ,非常安全。这样基类的所有属性和方法都可以被继承类继承!

#包括直肠。h # include int main(){矩形R1,R2;//实例化对象Rectangle_ctor(r1,0,2,10,15);Rectangle_ctor(r2,-1,3,5,8);printf(Rect r1(x=%d,y=%d,宽度=%d,高度=% d)Shape_getX(r1.super),Shape_getY(r1.super),r1.width,r1。身高);printf(Rect r2(x=%d,y=%d,宽度=%d,高度=% d)Shape_getX(r2.super),Shape_getY(r2.super),r2.width,r2。身高);//注意,这里有两种方式,一是强转类型,二是直接使用成员地址Shape_moveBy((Shape *)r1,-2,3);Shape_moveBy(r2.super,2,-1);printf(Rect r1(x=%d,y=%d,宽度=%d,高度=% d)Shape_getX(r1.super),Shape_getY(r1.super),r1.width,r1。身高);printf(Rect r2(x=%d,y=%d,宽度=%d,高度=% d)Shape_getX(r2.super),Shape_getY(r2.super),r2.width,r2。身高);返回0;}输出结果:

Rect r1(x=0,y=2,宽度=10,高度=15)Rect r2(x=-1,y=3,宽度=5,高度=8)Rect r1(x=-2,y=5宽度=10,高度=15)Rect r2(x=1,y=2,宽度=5,高度=8)多态C语言实现多态就是使用虚函数。在C语言里面,也可以实现多态。现在,我们又要增加一个圆形,并且在形状要扩展功能,我们要增加面积()和绘制()函数。但是形状相当于抽象类,不知道怎么去计算自己的面积,更不知道怎么去画出来自己。而且,矩形和圆形的面积计算方式和几何图像也是不一样的。下面让我们重新声明一下形状类:# ifndef SHAPE _ H # define SHAPE _ H # include struct SHAPE vtbl;//形状的属性typedef struct { struct shape vtbl const * vptr;int 16 _ t x;int 16 _ t y;}形状;//形状的虚表struct ShapeVtbl { uint 32 _ t(* area)(Shape const * const me);void(* draw)(Shape const * const me);};//形状的操作函数,接口函数void Shape_ctor(Shape * const me,int16_t x,int 16 _ t y);void Shape_moveBy(Shape * const me,int16_t dx,int 16 _ t dy);int 16 _ t Shape _ getX(Shape const * const me);int 16 _ t Shape _ getY(Shape const * const me);静态内联uint 32 _ t Shape _ area(Shape const * const me){ return(* me-vptr-area)(me);}静态内嵌void Shape _ draw(Shape const * const me){(* me-vptr-draw)(me);} Shape const * largestShape(Shape const * shapes[],uint 32 _ t n shapes);void drawAllShapes(Shape const * shapes[],uint 32 _ t n shapes);#endif看下加上虚函数之后的类关系图:

5.1 虚表和虚指针虚表(虚拟表)是这个类所有虚函数的函数指针的集合。虚指针(虚拟指针)是一个指向虚表的指针。这个虚指针必须存在于每个对象实例中,会被所有子类继承。在《Inside The C++ Object Model》 的第一章内容中,有这些介绍。5.2 在构造函数中设置vptr在每一个对象实例中,vptr必须被初始化指向其vtbl。最好的初始化位置就是在类的构造函数中。事实上,在构造函数中,C编译器隐式的创建了一个初始化的vptr。在C语言里面,我们必须显示的初始化vptr。下面就展示一下,在形状的构造函数里面,如何去初始化这个vptr。#包括形状。h #包含//形状的虚函数static uint 32 _ t Shape _ area _(Shape const * const me);静态void Shape _ draw _(Shape const * const me);//构造函数void Shape_ctor(Shape * const me,int16_t x,int16_t y) { //形状类的虚表静态结构Shape vtbl const vtbl={ Shape _ area _,Shape _ draw _ };me-vptr=vtbl;me-x=x;me-y=y;} void Shape _ move by(Shape * const me,int16_t dx,int 16 _ t dy){ me-x=dx;me-y=dy;} int 16 _ t Shape _ getX(Shape const * const me){ return me-x;} int 16 _ t Shape _ getY(Shape const * const me){ return me-y;}//形状类的虚函数实现static uint 32 _ t Shape _ area _(Shape const * const me){ assert(0);//类似纯虚函数return 0U//避免警告} static void Shape _ draw _(Shape const * const me){ assert(0);//纯虚函数不能被调用} Shape const * largestShape(Shape const * shapes[],uint 32 _ t n shapes){ Shape const * s=(Shape *)0;uint 32 _ t max=0 uuint 32 _ t I;for(I=0U;I nShapesI){ uint 32 _ t area=Shape _ area(shapes[I]);//虚函数调用if(area max){ max=area;s=形状[I];} }返回s;} void drawAllShapes(Shape const * shapes[],uint 32 _ t n shapes){ uint 32 _ t I;for(I=0U;I nShapesI){ Shape _ draw(shapes[I]);//虚函数调用}}5.3 继承vtbl和重载vptr上面已经提到过,基类包含vptr,子类会自动继承。但是,vptr需要被子类的虚表重新赋值。并且,这也必须发生在子类的构造函数中。下面是矩形的构造函数。包括# rect。h # include//矩形虚函数static uint 32 _ t Rectangle _ area _(Shape const * const me);静态void Rectangle _ draw _(Shape const * const me);//构造函数void Rectangle _ ctor(Rectangle * const me,int16_t x,int16_t y,uint16_t width,uint 16 _ t height){ static struct shape vtbl const vtbl={ Rectangle _ area _,Rectangle _ draw _ };Shape_ctor(me-super,x,y);//调用基类的构造函数我-超级。vptr=vtbl//重载vptr me-宽度=宽度;我-身高=身高;}//矩形的虚函数实现static uint 32 _ t Rectangle _ area _(Shape const * const me){ Rectangle const * const me _=(Rectangle const *)me;//显示的转换return(uint 32 _ t)me _-width *(uint 32 _ t)me _-height;} static void Rectangle _ draw _(Shape const * const me){ Rectangle const * const me _=(Rectangle const *)me;//显示的转换printf(Rectangle_draw_(x=%d,y=%d,宽度=%d,高度=% d)Shape_getX(me),Shape_getY(me),me_-width,me _-height);}5.4 虚函数调用有了前面虚表(虚拟表)和虚指针(虚拟指针)的基础实现,虚拟调用(后期绑定)就可以用下面代码实现了。uint 32 _ t Shape _ area(Shape const * const me){ return(* me-vptr-area)(me);}这个函数可以放到c文件里面,但是会带来一个缺点就是每个虚拟调用都有额外的调用开销。为了避免这个缺点,如果编译器支持内联函数(C99)。

我们可以把定义放在头文件里,像下面这样:静态内联uint 32 _ t shape _ area(shape const * const me){ return(* me-vptr-area)(me);}如果它一个比较老的编译器(C89),我们可以用宏函数来实现,像下面这样:# define shape _ area(me _)(*(me _)-vptr-area)((me _))。看看例子中的调用机制:5.5main.c # include rect.h 。圆c1,C2;Shape const *shapes[]={ c1.super,r2.super,c2.super,R1 . super };形状常数;//实例化rectangle对象Rectangle_ctor(r1,0,2,10,15);Rectangle_ctor(r2,-1,3,5,8);//实例化圆形对象Circle_ctor(c1,1,-2,12);Circle_ctor(c2,1,-3,6);s=largestShape(shapes,sizeof(shapes)/sizeof(shapes[0]);printf(largestsshape s(x=% d,y=% d)Shape_getX(s),Shape _ getY(s));drawAllShapes(shapes,sizeof(shapes)/sizeof(shapes[0]);返回0;}输出结果:largest shapes(x=1,y=-2) circle _ draw _ (x=1,y=-2,rad=12) rectangle _ draw _ (x=-1,y=3,width=5,height=8) circle _ draw Y=2,width=10,height=15)综上所述,面向对象编程是一种方法,并不局限于某一种编程语言。用C语言实现封装和单继承,理解和实现起来相对简单,但是多态会稍微复杂一点。如果打算大量使用多态,建议改用C语言。毕竟这种复杂性被这种语言封装了,所以你只需要简单地使用它。但是它没有并不意味着C语言可以不要实现多态性。参考材料:

https://blog.csdn.net/onlyshi/article/details/81672279