找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3822

积分

0

好友

506

主题
发表于 昨天 21:45 | 查看: 6| 回复: 0

C语言里没有类、没有虚函数、也没有继承。但嵌入式开发中经常碰到这种需求:为一组传感器设计统一的驱动接口——温度传感器、压力传感器各自实现,上层调用时完全不用管底层差别。

C++是怎么做多态的

先看懂 C++ 的机制,才能用 C 复刻出来。

在 C++ 中,含有虚函数的类,编译器会自动生成一张虚函数表(vtable,并在每个对象内部安插一个虚表指针(vptr)指向这张表。调用虚函数时,程序先通过对象内的 vptr 找到 vtable,再从 vtable 里取出对应的函数地址,进行间接调用。

对象内存布局:
+--------+
| vptr ---+---> vtable: [ &draw, &area, &destroy ]
| x      |
| y      |
+--------+

base_ptr->draw() 在编译阶段并不知道具体要调用哪个函数,运行时通过 vptr 查表才能确定——这就是动态多态。

C 语言可没有编译器帮忙干这些,得自己手动造 vtable、自己维护 vptr。

函数指针嵌入结构体

最直观的办法:把函数指针直接塞进结构体里。

typedef struct {
    int x;
    int y;
    void (*draw)(void *self);
    float (*area)(void *self);
    void (*destroy)(void *self);
} shape_t;

每个“对象”自带函数指针,创建对象时对应绑定上去:

void rect_draw(void *self);
float rect_area(void *self);
void rect_destroy(void *self);

shape_t *rect_create(int x, int y, int w, int h)
{
    rect_t *r = malloc(sizeof(rect_t));
    r->base.x = x;
    r->base.y = y;
    r->base.draw = rect_draw;
    r->base.area = rect_area;
    r->base.destroy = rect_destroy;
    r->width = w;
    r->height = h;
    return (shape_t *)r;
}

调用时看起来挺自然:

shape_t *s = rect_create(0, 0, 10, 20);
s->draw(s);
float a = s->area(s);
s->destroy(s);

不好的地方:每个对象都保存一份函数指针,浪费内存。100 个矩形对象就会有 100 份 draw 指针,虽然它们全都指向同一个函数。在 RAM 紧张的 MCU 上,这种冗余是不可接受的。

手动 vtable

把函数指针从对象里剥离出来,放到一个单独的 vtable 结构中,同类型的所有对象共享一份 vtable。对象里只保留一个指向 vtable 的指针就行。

typedef struct shape_vtable {
    void  (*draw)(void *self);
    float (*area)(void *self);
    void  (*destroy)(void *self);
} shape_vtable_t;

typedef struct {
    const shape_vtable_t *vtable;
    int x;
    int y;
} shape_t;

通过 vtable 间接寻址时,可以用宏来简化书写:

#define SHAPE_DRAW(s)   ((s)->vtable->draw(s))
#define SHAPE_AREA(s)   ((s)->vtable->area(s))
#define SHAPE_DESTROY(s) ((s)->vtable->destroy(s))

矩形和圆形各定义一份 vtable,编译期就确定下来,可以放在 Flash 里:

static void rect_draw(void *self);
static float rect_area(void *self);
static void rect_destroy(void *self);

static const shape_vtable_t rect_vtable = {
    .draw    = rect_draw,
    .area    = rect_area,
    .destroy = rect_destroy,
};
static void circle_draw(void *self);
static float circle_area(void *self);
static void circle_destroy(void *self);

static const shape_vtable_t circle_vtable = {
    .draw    = circle_draw,
    .area    = circle_area,
    .destroy = circle_destroy,
};

创建对象时绑定对应的 vtable 指针:

typedef struct {
    shape_t base;
    int width;
    int height;
} rect_t;

shape_t *rect_create(int x, int y, int w, int h)
{
    rect_t *r = malloc(sizeof(rect_t));
    r->base.vtable = &rect_vtable;
    r->base.x = x;
    r->base.y = y;
    r->width = w;
    r->height = h;
    return &r->base;
}
typedef struct {
    shape_t base;
    int radius;
} circle_t;

shape_t *circle_create(int x, int y, int r)
{
    circle_t *c = malloc(sizeof(circle_t));
    c->base.vtable = &circle_vtable;
    c->base.x = x;
    c->base.y = y;
    c->radius = r;
    return &c->base;
}

这样使用时完全统一,多态的效果自然就出来了:

shape_t *shapes[3];
shapes[0] = rect_create(0, 0, 10, 20);
shapes[1] = circle_create(5, 5, 8);
shapes[2] = rect_create(1, 1, 30, 40);

for (int i = 0; i < 3; i++) {
    SHAPE_DRAW(shapes[i]);
    printf("area = %.2f\n", SHAPE_AREA(shapes[i]));
}

继承:结构体嵌套

C 语言里模拟继承,靠的是结构体嵌套,并且必须把“基类”结构体放在“派生类”结构体的第一个成员位置:

typedef struct {
    const sensor_vtable_t *vtable;
    const char *name;
    uint8_t addr;
} sensor_t;          // 基类

typedef struct {
    sensor_t base;    // 基类放第一个成员
    int32_t t_fine;   // 派生类自有属性
    uint8_t calib[42];
} bme280_t;          // 派生类

为什么非得第一个?因为 C 标准保证:结构体首个成员的地址等于结构体本身的地址。这样一来,&bme280->base(sensor_t *)bme280 就是同一个地址,可以安全地向上转型。

bme280_t bme;
sensor_t *s = &bme.base;   // OK,地址相同
sensor_t *s2 = (sensor_t *)&bme;  // 也OK,因为base是第一个成员

如果 base 不是第一个成员,强转之后 vtable 指针的位置就对不上了,轻则读错数据,重则 HardFault 或函数指针飞进未知区域。

类型安全:container_of 宏

从基类指针反推出派生类指针,Linux 内核中常用 container_of 宏:

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

在虚函数里可以这样用:

static int bme280_read(void *self)
{
    // self实际指向sensor_t,需要反推出bme280_t
    bme280_t *dev = container_of(self, bme280_t, base);
    // 现在可以访问dev->t_fine、dev->calib
    return 0;
}

当然,由于 base 是第一个成员,也可以直接强转,效果一样。但 container_of 更安全——万一有人把 base 移到了第二个成员的位置,强转会引入 bug,而 container_of 通过宏计算偏移量,不会受影响。

以上这些技巧在嵌入式 C 开发中很常见,如果你想进一步探讨 vtable、指针等 C/C++ 底层机制,也欢迎去云栈社区一起交流。




上一篇:Linux 多线程条件变量为什么必须用 CLOCK_MONOTONIC ?一个容易被忽略的系统时间陷阱
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-22 02:46 , Processed in 0.891721 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表