嵌入式启航 嵌入式启航
首页 作品展示 个人主页
首页 作品展示 个人主页
binarybard
binarybard
嵌入式软件工程师,专注于MCU+RTOS技术栈,了解各种MCU底层。
binarybard
binarybard
嵌入式软件工程师,专注于MCU+RTOS技术栈,了解各种MCU底层。

分类

  • 默认分类 1
  • 开发环境 1
  • keil_C51 5
  • 嵌入式C语言 3
  • 常用算法 1

最新文章

  • CRC校验
    2026-02-07
  • C语言面向对象
    2026-01-31
  • 计数与控制结构
    2026-01-24
  • 控制外部世界
    2026-01-17
  • 51单片机函数调用与可重入函数
    2026-01-10

C语言面向对象

binarybard 2026年01月31日 嵌入式C语言 0 条评论

面向对象这个概念基本是编程人员绕不开的话题,各种软件设计模式都是建立在面向对象的基础上,但是提到这个概念通常人们会想到C++,java,python这些面向对象语言,但是实际上面向对象是一种思维方式而不是限定于一种语言,使用C语言也可以进行面向对象编程,最著名的例子就是linux系统,如果你进行过驱动开发很容易发现面向对象的痕迹,开发mcu经常使用的RTOS,以及HAL库,如果你观察过也都是无不体现面向对象的思想。

尽管说有些MCU开发时可以使用C++,也有一些开始转向Rust开发,但是就目前来讲大多数MCU开发的工作还是要使用C语言,知道怎么使用C语言进行面向对象既可以帮助你理解各种代码,也有助于自己写出漂亮的代码。

面向对象有三大基本概念封装,继承,多态,实现了这些C语言面向对象也就不是一句空谈了,封装就是把数据(属性)和函数(方法)打包到类中,C语言虽然没有类这个概念,但是也有自己的封装方法就是使用结构体。

封装

#ifndef SHAPE_H
#define SHAPE_H

#include <stdint.h>

/* Shape 的属性... */
typedef struct {
  int16_t x; /*Shape 位置的 x 坐标 */
  int16_t y; /* Shape 位置的 y 坐标*/
} Shape;

/* Shape 的方法... */
void Shape_ctor(Shape* const self, int16_t x0, int16_t y0);
void Shape_moveBy(Shape* const self, int16_t dx, int16_t dy);
#endif /* SHAPE_H */
/********************************************************************/
#include "shape.h" /* Shape class interface */
void Shape_ctor(Shape* const self, int16_t x0, int16_t y0) {
  self->x = x0;
  self->y = y0;
}
void Shape_moveBy(Shape* const self, int16_t dx, int16_t dy) {
  self->x += dx;
  self->y += dy;
}
int main(void) {
    Shape s1;
    Shape_ctor(&s1, 1, 2);
    Shape_moveBy(&s1, 7, 8); 
}
/*****************动态分配的写法*****************************************/
Shape* Shape_ctor(int16_t x0, int16_t y0) {
  Shape* self = (Shape*)malloc(sizeof(Shape));
  if (self != NULL) {
    self->x = x0;
    self->y = y0;
  }
  return self;
}
/*****************动态分配的的写法********************************************/
/*****************结构体内封装函数指针*****************************************/
typedef struct {
  int16_t x; 
  int16_t y; 
  void (*moveBy)(Shape* const self,int16_t dx, int16_t dy); 
} Shape;
void Shape_ctor(Shape* const self, int16_t x0, int16_t y0) {
  self->x = x0;
  self->y = y0;
  self->moveBy = Shape_moveBy;
}
/*****************结构体内封装函数指针********************************************/

这里用结构体封装Shape的属性,你就可以创建任意数量的 Shape 对象作为 Shape 属性结构体的实例,不同的是因为C语言不是原生支持,所以需要声明后调用构造函数填充实例。

需要注意的有两点:我看到有一些在结构体内封装函数指针,然后在构造函数内将函数指针指向对应函数的,这样写起来函数调用的时候可以"s2.moveBy(&s2, 5, 6);",形式上就和面向对象语言更像了,不过问题是这样每创建一个对象实例,都会分配函数指针的空间,而微处理器资源珍贵应避免浪费RAM空间。这个问题见仁见智,如果你的微处理器资源丰富其实这样写也还可以接受。

另一个就比较严重了,有对象构造函数采用动态分配的写法,因为里面是动态分配的内存,构造时就可以采用"Shape *s1 = Shape_ctor(1,2);"的写法,这种看起来更优雅一些,不过malloc等动态分配函数可能会造成内存碎片,另一方面这些函数执行时间不确定,分配空间是否成功不确定,在一些对实时性稳定性要求高的场合就不能使用。

不光是对象构造函数,在开发MCU的其他地方你可能已经使用过了malloc函数而且发现没有什么问题,但是使用这个函数确实有风险,还是建议不要使用,你不知道它在哪次运行会出问题,不如一开始就养成习惯,而不是去赌不会出问题。(有一些自己构建内存池进行动态分配的方法这里不进行讨论)

继承

继承是指基于现有类定义新类,以便重用和组织代码的能力。在 C 语言中,只需将继承的类属性结构嵌入为派生类属性结构的第一个成员,即可轻松实现继承。

typedef struct {
    Shape super; /* 继承的 Shape */
    /* 特定于此类的属性 */
    uint16_t width;
    uint16_t height;
} Rectangle;
void Rectangle_ctor(Rectangle * const self,
                    int16_t x0, int16_t y0,
                    uint16_t w0, uint16_t h0)
{
    Shape_ctor(&self->super, x0, y0); /* 基类构造函数 */
    /*此类中的属性 */
    self->width = w0;
    self->height = h0;
}
int main() {
  Rectangle r1;
  Rectangle_ctor(&r1, 0, 2, 10, 15);
  printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", r1.super.x, r1.super.y,
         r1.width, r1.height);
  Shape_moveBy((Shape *)&r1, -2, 3);//父类方法传入子类对象
  printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", r1.super.x, r1.super.y,
         r1.width, r1.height);
  return 0;
}

这个地方我在创建了矩形实例r1之后,给父类方法传入的是子类的对象,观察执行前后可以看出正确执行了。其实这个地方还是比较容易理解的,当你把“&r1”转换成父类的指针时指向的就是结构体中父类属性的那部分内存,事实上C++内存排布也是子类特有数据成员紧接着父类数据成员。

typedef struct {
    /* 特定于此类的属性 */
    uint16_t width;
    uint16_t height;
    Shape super1; /* 继承的 Shape */
    Shape super2; /* 继承的 Shape */
} Rectangle;

不过必须注意的的时父类成员必须在子类中第一个,如果不是放在第一个你当然也可以使用成员“super”( “&r2->super” )的地址进行访问,但是这个时候更像是一个普通的数据成员,理论上你也可以有super1,super2·······,只有放在首位才可以将子类地址强制转换为父类指针使用。

多态

最后多态的实现完全可以模仿C++的实现方法实现,就是采用虚函数表, 虚函数表指针照例放在父类首位,这里用了两个函数进行演示,计算面积和画出图形,其实就是不同类在不同文件中各自实现自己的函数,在构造函数中将各自类中的虚函数表指针指向各自类对应的虚函数表。

struct ShapeVtable {
  void (*draw)(void const *const _self);
  uint32_t (*area)(void const *const _self);
};
typedef struct {
  struct ShapeVtable const *vptr; /* 虚拟指针 */
  int16_t x;                      /* Shape 位置的 x 坐标 */
  int16_t y;                      /* Shape 位置的 y 坐标 */
} Shape;
static void Shape_draw(void const * const _self) {
    Shape const * const self = _self;
    (void)self; /* unused parameter */
}
static uint32_t Shape_area(void const * const _self) {
    Shape const * const self = _self;
    (void)self; /* unused parameter */
    return 0; /* default area is zero */
}
void Shape_ctor(Shape * const self, int16_t x0, int16_t y0) {
    static const struct ShapeVtable vtable = {
        &Shape_draw,
        &Shape_area
    };
    self->vptr = &vtable;
    self->x = x0;
    self->y = y0;
}
void Rectangle_draw(void const* const _self) {
  Rectangle const* const self = _self;
  /* draw a rectangle at (self->super.x, self->super.y) */
  /* with width self->width and height self->height */
  /* ... */
  (void)self; /* unused parameter */
}
uint32_t Rectangle_area(void const* const _self) {
  Rectangle const* const self = _self;
  return self->width * self->height;
}
void Rectangle_ctor(Rectangle* const self, int16_t x0, int16_t y0, uint16_t w0,
                    uint16_t h0) {
  static const struct ShapeVtable vtable = {
      &Rectangle_draw, /* override base class draw() */
      &Rectangle_area  /* override base class area() */
  };
  Shape_ctor(&self->super, x0, y0); /* base class ctor */
  self->super.vptr = &vtable;
  /* init attributes added in this class */
  self->width = w0;
  self->height = h0;
}

矩形类构造函数中,先运行的是父类的构造函数,这时候虚函数表指针指向的是Shape类的虚函数表,之后在把指针指向Rectangle类的虚函数表,来达到多态的效果,在C++中如果你在构造函数中分析每一步其实也是这么一个过程。

在main函数中创建一个Shape类和两个Rectangle类,构造完成后暂停,观察局部变量,s1中虚函数指针指向Shape类的虚函数表,r1和r2的虚函数指针指向的是Rectangle类的虚函数表(两个实例指向同一个表),因为有“static const”修饰所以每个类的虚函数表实际上是存储在ROM空间的,且只有一份,避免了资源浪费。

static inline void Shape_draw_vcall(void const *const self) {
  (*((Shape const *const)self)->vptr->draw)(self);
}

static inline uint32_t Shape_area_vcall(void const *const self) {
  return (*((Shape const *const)self)->vptr->area)(self);
}

#define SHAPE_DRAW_VCALL(self) (*((Shape const *const)self)->vptr->draw)((self))
#define SHAPE_AREA_VCALL(self) (*((Shape const *const)self)->vptr->area)((self))
/****************************************************************************
//   printf("Shape area: %d\n", Shape_area_vcall(&s1));
//   printf("Rectangle area: %d\n", Shape_area_vcall(&r1));
****************************************************************/

最后你可以采用内联函数或者宏定义的方法,尝试传入不同类型对象,分别打印s1和r1的面积,此时你传入什么类型的对象就会调用这个类对应的函数而不是父类函数,也就实现了多态。

上一篇
计数与控制结构
下一篇
CRC校验

评论已关闭

© 2025-2026 嵌入式启航.
豫ICP备2025159703号    备案图标 豫公网安备41172102000273号