你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

为什么要用C语言实现面向对象?

[复制链接]
gaosmile 发布时间:2020-11-6 22:25
不知道有多少人去了解过语言的发展史,早期C语言的语法功能其实比较简单。随着应用需求和场景的变化,C语言的语法功能在不断升级变化。

虽然我们的教材有这么一个结论:C语言是面向过程的语言,C++是面向对象的编程语言,但面向对象的概念是在C语言阶段就有了,而且应用到了很多地方,比如某些操作系统内核、通信协议等。

面向对象编程,也就是大家说的OOP(Object Oriented Programming)并不是一种特定的语言或者工具,它只是一种设计方法、设计思想,它表现出来的三个最基本的特性就是封装、继承与多态

1
为什么要用C语言实现面向对象

阅读文本之前肯定有读者会问这样的问题:我们有C++面向对象的语言,为什么还要用C语言实现面向对象呢?

C语言这种非面向对象的语言,同样也可以使用面向对象的思路来编写程序的。只是用面向对象的C++语言来实现面向对象编程会更简单一些,但是C语言的高效性是其他面向对象编程语言无法比拟的。

当然使用C语言来实现面向对象的开发相对不容易理解,这就是为什么大多数人学过C语言却看不懂Linux内核源码。

所以这个问题其实很好理解,只要有一定C语言编程经验的读者都应该能明白:面向过程的C语言和面向对象的C++语言相比,代码运行效率、代码量都有很大差异。在性能不是很好、资源不是很多的MCU中使用C语言面向对象编程就显得尤为重要。

2
具备条件

要想使用C语言实现面向对象,首先需要具备一些基础知识。比如:(C语言中的)结构体、函数、指针,以及函数指针等,(C++中的)基类、派生、多态、继承等。

首先,不仅仅是了解这些基础知识,而是有一定的编程经验,因为上面说了“面向对象是一种设计方法、设计思想”,如果只是停留在字面意思的理解,没有这种设计思想肯定不行。

因此,不建议初学者使用C语言实现面向对象,特别是在真正项目中。建议把基本功练好,再使用。

利用C语言实现面向对象的方法很多,下面就来描述最基本的封装、继承和多态。

3
封装

封装就是把数据和函数打包到一个类里面,其实大部分C语言编程者都已近接触过了。

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

这个看起来似乎很好理解,那下面我们实现一下基本的封装特性。

  1. #ifndef SHAPE_H
  2. #define SHAPE_H

  3. #include <stdint.h>

  4. // Shape 的属性
  5. typedef struct {
  6.     int16_t x;
  7.     int16_t y;
  8. } Shape;

  9. // Shape 的操作函数,接口函数
  10. void Shape_ctor(Shape * const me, int16_t x, int16_t y);
  11. void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
  12. int16_t Shape_getX(Shape const * const me);
  13. int16_t Shape_getY(Shape const * const me);

  14. #endif /* SHAPE_H */
复制代码

这是 Shape 类的声明,非常简单,很好理解。一般会把声明放到头文件里面 “Shape.h”。来看下 Shape 类相关的定义,当然是在 “Shape.c” 里面。

  1. #include "shape.h"

  2. // 构造函数
  3. void Shape_ctor(Shape * const me, int16_t x, int16_t y)
  4. {
  5.     me->x = x;
  6.     me->y = y;
  7. }

  8. void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy)
  9. {
  10.     me->x += dx;
  11.     me->y += dy;
  12. }

  13. // 获取属性值函数
  14. int16_t Shape_getX(Shape const * const me)
  15. {
  16.     return me->x;
  17. }
  18. int16_t Shape_getY(Shape const * const me)
  19. {
  20.     return me->y;
  21. }
复制代码

再看下 main.c

  1. #include "shape.h"  /* Shape class interface */
  2. #include <stdio.h>  /* for printf() */

  3. int main()
  4. {
  5.     Shape s1, s2; /* multiple instances of Shape */

  6.     Shape_ctor(&s1, 0, 1);
  7.     Shape_ctor(&s2, -1, 2);

  8.     printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1));
  9.     printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2));

  10.     Shape_moveBy(&s1, 2, -4);
  11.     Shape_moveBy(&s2, 1, -2);

  12.     printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1));
  13.     printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2));

  14.     return 0;
  15. }
复制代码

编译之后,看看执行结果:

  1. Shape s1(x=0,y=1)
  2. Shape s2(x=-1,y=2)
  3. Shape s1(x=2,y=-3)
  4. Shape s2(x=0,y=0)
复制代码

整个例子,非常简单,非常好理解。以后写代码时候,要多去想想标准库的文件IO操作,这样也有意识的去培养面向对象编程的思维。

4
继承

继承就是基于现有的一个类去定义一个新类,这样有助于重用代码,更好的组织代码。在 C 语言里面,去实现单继承也非常简单,只要把基类放到继承类的第一个数据成员的位置就行了。

例如,我们现在要创建一个 Rectangle 类,我们只要继承 Shape 类已经存在的属性和操作,再添加不同于 Shape 的属性和操作到 Rectangle 中。

下面是 Rectangle 的声明与定义:

  1. #ifndef RECT_H
  2. #define RECT_H

  3. #include "shape.h" // 基类接口

  4. // 矩形的属性
  5. typedef struct {
  6.     Shape super; // 继承 Shape

  7.     // 自己的属性
  8.     uint16_t width;
  9.     uint16_t height;
  10. } Rectangle;

  11. // 构造函数
  12. void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
  13.                     uint16_t width, uint16_t height);

  14. #endif /* RECT_H */
复制代码

  1. #include "rect.h"

  2. // 构造函数
  3. void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
  4.                     uint16_t width, uint16_t height)
  5. {
  6.     /* first call superclass’ ctor */
  7.     Shape_ctor(&me->super, x, y);

  8.     /* next, you initialize the attributes added by this subclass... */
  9.     me->width = width;
  10.     me->height = height;
  11. }
复制代码

我们来看一下 Rectangle 的继承关系和内存布局:
微信图片_20201106224255.png

因为有这样的内存布局,所以你可以很安全的传一个指向 Rectangle 对象的指针到一个期望传入 Shape 对象的指针的函数中,就是一个函数的参数是 “Shape *”,你可以传入 “Rectangle *”,并且这是非常安全的。这样的话,基类的所有属性和方法都可以被继承类继承!

  1. #include "rect.h"  
  2. #include <stdio.h>

  3. int main()
  4. {
  5.     Rectangle r1, r2;

  6.     // 实例化对象
  7.     Rectangle_ctor(&r1, 0, 2, 10, 15);
  8.     Rectangle_ctor(&r2, -1, 3, 5, 8);

  9.     printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n",
  10.            Shape_getX(&r1.super), Shape_getY(&r1.super),
  11.            r1.width, r1.height);
  12.     printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n",
  13.            Shape_getX(&r2.super), Shape_getY(&r2.super),
  14.            r2.width, r2.height);

  15.     // 注意,这里有两种方式,一是强转类型,二是直接使用成员地址
  16.     Shape_moveBy((Shape *)&r1, -2, 3);
  17.     Shape_moveBy(&r2.super, 2, -1);

  18.     printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n",
  19.            Shape_getX(&r1.super), Shape_getY(&r1.super),
  20.            r1.width, r1.height);
  21.     printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n",
  22.            Shape_getX(&r2.super), Shape_getY(&r2.super),
  23.            r2.width, r2.height);

  24.     return 0;
  25. }
复制代码

输出结果:

  1. Rect r1(x=0,y=2,width=10,height=15)
  2. Rect r2(x=-1,y=3,width=5,height=8)
  3. Rect r1(x=-2,y=5,width=10,height=15)
  4. Rect r2(x=1,y=2,width=5,height=8)
复制代码

5
多态

C++ 语言实现多态就是使用虚函数。在 C 语言里面,也可以实现多态。
现在,我们又要增加一个圆形,并且在 Shape 要扩展功能,我们要增加 area() 和 draw() 函数。但是 Shape 相当于抽象类,不知道怎么去计算自己的面积,更不知道怎么去画出来自己。而且,矩形和圆形的面积计算方式和几何图像也是不一样的。
下面让我们重新声明一下 Shape 类:

  1. #ifndef SHAPE_H
  2. #define SHAPE_H

  3. #include <stdint.h>

  4. struct ShapeVtbl;
  5. // Shape 的属性
  6. typedef struct {
  7.     struct ShapeVtbl const *vptr;
  8.     int16_t x;
  9.     int16_t y;
  10. } Shape;

  11. // Shape 的虚表
  12. struct ShapeVtbl {
  13.     uint32_t (*area)(Shape const * const me);
  14.     void (*draw)(Shape const * const me);
  15. };

  16. // Shape 的操作函数,接口函数
  17. void Shape_ctor(Shape * const me, int16_t x, int16_t y);
  18. void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
  19. int16_t Shape_getX(Shape const * const me);
  20. int16_t Shape_getY(Shape const * const me);

  21. static inline uint32_t Shape_area(Shape const * const me)
  22. {
  23.     return (*me->vptr->area)(me);
  24. }

  25. static inline void Shape_draw(Shape const * const me)
  26. {
  27.     (*me->vptr->draw)(me);
  28. }


  29. Shape const *largestShape(Shape const *shapes[], uint32_t nShapes);
  30. void drawAllShapes(Shape const *shapes[], uint32_t nShapes);

  31. #endif /* SHAPE_H */
复制代码

看下加上虚函数之后的类关系图:
微信图片_20201106224259.png

5.1 虚表和虚指针虚表(Virtual Table)是这个类所有虚函数的函数指针的集合。
虚指针(Virtual Pointer)是一个指向虚表的指针。这个虚指针必须存在于每个对象实例中,会被所有子类继承。
在《Inside The C++ Object Model》的第一章内容中,有这些介绍。
5.2 在构造函数中设置vptr在每一个对象实例中,vptr 必须被初始化指向其 vtbl。最好的初始化位置就是在类的构造函数中。事实上,在构造函数中,C++ 编译器隐式的创建了一个初始化的vptr。在 C 语言里面, 我们必须显示的初始化vptr。
下面就展示一下,在 Shape 的构造函数里面,如何去初始化这个 vptr。

  1. #include "shape.h"
  2. #include <assert.h>

  3. // Shape 的虚函数
  4. static uint32_t Shape_area_(Shape const * const me);
  5. static void Shape_draw_(Shape const * const me);

  6. // 构造函数
  7. void Shape_ctor(Shape * const me, int16_t x, int16_t y)
  8. {
  9.     // Shape 类的虚表
  10.     static struct ShapeVtbl const vtbl =
  11.     {
  12.        &Shape_area_,
  13.        &Shape_draw_
  14.     };
  15.     me->vptr = &vtbl;
  16.     me->x = x;
  17.     me->y = y;
  18. }


  19. void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy)
  20. {
  21.     me->x += dx;
  22.     me->y += dy;
  23. }


  24. int16_t Shape_getX(Shape const * const me)
  25. {
  26.     return me->x;
  27. }
  28. int16_t Shape_getY(Shape const * const me)
  29. {
  30.     return me->y;
  31. }

  32. // Shape 类的虚函数实现
  33. static uint32_t Shape_area_(Shape const * const me)
  34. {
  35.     assert(0); // 类似纯虚函数
  36.     return 0U; // 避免警告
  37. }

  38. static void Shape_draw_(Shape const * const me)
  39. {
  40.     assert(0); // 纯虚函数不能被调用
  41. }


  42. Shape const *largestShape(Shape const *shapes[], uint32_t nShapes)
  43. {
  44.     Shape const *s = (Shape *)0;
  45.     uint32_t max = 0U;
  46.     uint32_t i;
  47.     for (i = 0U; i < nShapes; ++i)
  48.     {
  49.         uint32_t area = Shape_area(shapes[i]);// 虚函数调用
  50.         if (area > max)
  51.         {
  52.             max = area;
  53.             s = shapes[i];
  54.         }
  55.     }
  56.     return s;
  57. }


  58. void drawAllShapes(Shape const *shapes[], uint32_t nShapes)
  59. {
  60.     uint32_t i;
  61.     for (i = 0U; i < nShapes; ++i)
  62.     {
  63.         Shape_draw(shapes[i]); // 虚函数调用
  64.     }
  65. }
复制代码

5.3 继承 vtbl 和 重载 vptr
上面已经提到过,基类包含 vptr,子类会自动继承。但是,vptr 需要被子类的虚表重新赋值。并且,这也必须发生在子类的构造函数中。下面是 Rectangle 的构造函数。

  1. #include "rect.h"  
  2. #include <stdio.h>

  3. // Rectangle 虚函数
  4. static uint32_t Rectangle_area_(Shape const * const me);
  5. static void Rectangle_draw_(Shape const * const me);

  6. // 构造函数
  7. void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
  8.                     uint16_t width, uint16_t height)
  9. {
  10.     static struct ShapeVtbl const vtbl =
  11.     {
  12.         &Rectangle_area_,
  13.         &Rectangle_draw_
  14.     };
  15.     Shape_ctor(&me->super, x, y); // 调用基类的构造函数
  16.     me->super.vptr = &vtbl;           // 重载 vptr
  17.     me->width = width;
  18.     me->height = height;
  19. }

  20. // Rectangle's 虚函数实现
  21. static uint32_t Rectangle_area_(Shape const * const me)
  22. {
  23.     Rectangle const * const me_ = (Rectangle const *)me; //显示的转换
  24.     return (uint32_t)me_->width * (uint32_t)me_->height;
  25. }

  26. static void Rectangle_draw_(Shape const * const me)
  27. {
  28.     Rectangle const * const me_ = (Rectangle const *)me; //显示的转换
  29.     printf("Rectangle_draw_(x=%d,y=%d,width=%d,height=%d)\n",
  30.            Shape_getX(me), Shape_getY(me), me_->width, me_->height);
  31. }
复制代码

5.4 虚函数调用
有了前面虚表(Virtual Tables)和虚指针(Virtual Pointers)的基础实现,虚拟调用(后期绑定)就可以用下面代码实现了。

  1. uint32_t Shape_area(Shape const * const me)
  2. {
  3.     return (*me->vptr->area)(me);
  4. }
复制代码

这个函数可以放到.c文件里面,但是会带来一个缺点就是每个虚拟调用都有额外的调用开销。为了避免这个缺点,如果编译器支持内联函数(C99)。我们可以把定义放到头文件里面,类似下面:

  1. static inline uint32_t Shape_area(Shape const * const me)
  2. {
  3.     return (*me->vptr->area)(me);
  4. }
复制代码

如果是老一点的编译器(C89),我们可以用宏函数来实现,类似下面这样:
  • #define Shape_area(me_) ((*(me_)->vptr->area)((me_)))
看一下例子中的调用机制: 微信图片_20201106224303.png
5.5 main.c

  1. #include "rect.h"  
  2. #include "circle.h"
  3. #include <stdio.h>

  4. int main()
  5. {
  6.     Rectangle r1, r2;
  7.     Circle    c1, c2;
  8.     Shape const *shapes[] =
  9.     {
  10.         &c1.super,
  11.         &r2.super,
  12.         &c2.super,
  13.         &r1.super
  14.     };
  15.     Shape const *s;

  16.     // 实例化矩形对象
  17.     Rectangle_ctor(&r1, 0, 2, 10, 15);
  18.     Rectangle_ctor(&r2, -1, 3, 5, 8);

  19.     // 实例化圆形对象
  20.     Circle_ctor(&c1, 1, -2, 12);
  21.     Circle_ctor(&c2, 1, -3, 6);

  22.     s = largestShape(shapes, sizeof(shapes)/sizeof(shapes[0]));
  23.     printf("largetsShape s(x=%d,y=%d)\n", Shape_getX(s), Shape_getY(s));

  24.     drawAllShapes(shapes, sizeof(shapes)/sizeof(shapes[0]));

  25.     return 0;
  26. }
复制代码
输出结果:

  1. largetsShape s(x=1,y=-2)
  2. Circle_draw_(x=1,y=-2,rad=12)
  3. Rectangle_draw_(x=-1,y=3,width=5,height=8)
  4. Circle_draw_(x=1,y=-3,rad=6)
  5. Rectangle_draw_(x=0,y=2,width=10,height=15)
复制代码

6
总结

还是那句话,面向对象编程是一种方法,并不局限于某一种编程语言。用 C 语言实现封装、单继承,理解和实现起来比较简单,多态反而会稍微复杂一点,如果打算广泛的使用多态,还是推荐转到 C++ 语言上,毕竟这层复杂性被这个语言给封装了,你只需要简单的使用就行了。但并不代表,C 语言实现不了多态这个特性。
收藏 2 评论0 发布时间:2020-11-6 22:25

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版