C语言实现多态

C 语言实现 OOP 多态的核心就是:

  1. 基类中定义函数指针(虚函数表)
  2. 构造函数中绑定具体实现
  3. 通过基类指针实现多态调用

看代码示例:

#include <stdio.h>
#include <stdlib.h>

// 1. 定义“基类” Shape
// ------------------------------------
struct Shape; // 前向声明

// 定义“虚函数”指针类型
typedef void (*FuncDraw)(struct Shape*);
typedef double (*FuncArea)(struct Shape*);

// Shape 结构体,模拟“基类”
typedef struct Shape {
    int x;
    int y;
    FuncDraw draw;
    FuncArea area;
} Shape;

// 2. 定义“派生类” Circle 和 Rectangle
// ------------------------------------
// Circle 结构体,模拟“派生类”
typedef struct {
    Shape base; // 继承!将基类作为第一个成员
    int radius;
} Circle;

// Rectangle 结构体,模拟“派生类”
typedef struct {
    Shape base; // 继承!
    int width;
    int height;
} Rectangle;


// 3. 实现具体的“方法”
// ------------------------------------
void draw_circle(Shape* s) {
    Circle* c = (Circle*)s; // 向下转型
    printf("Drawing a circle at (%d, %d) with radius %d\n", c->base.x, c->base.y, c->radius);
}

double area_circle(Shape* s) {
    Circle* c = (Circle*)s;
    return 3.14159 * c->radius * c->radius;
}

void draw_rectangle(Shape* s) {
    Rectangle* r = (Rectangle*)s;
    printf("Drawing a rectangle at (%d, %d) with size %dx%d\n", r->base.x, r->base.y, r->width, r->height);
}

double area_rectangle(Shape* s) {
    Rectangle* r = (Rectangle*)s;
    return (double)(r->width * r->height);
}

// 4. 实现“构造函数”
// ------------------------------------
Circle* create_circle(int x, int y, int radius) {
    Circle* c = (Circle*)malloc(sizeof(Circle));
    c->base.x = x;
    c->base.y = y;
    // 将函数指针指向具体的实现!这是多态的关键
    c->base.draw = draw_circle;
    c->base.area = area_circle;
    c->radius = radius;
    return c;
}

Rectangle* create_rectangle(int x, int y, int w, int h) {
    Rectangle* r = (Rectangle*)malloc(sizeof(Rectangle));
    r->base.x = x;
    r->base.y = y;
    // 将函数指针指向具体的实现!
    r->base.draw = draw_rectangle;
    r->base.area = area_rectangle;
    r->width = w;
    r->height = h;
    return r;
}

// 5. 多态的演示
// ------------------------------------
int main() {
    // 创建一个 Circle 对象和 Ractangle 对象
    Circle* c = create_circle(10, 20, 5);
    Rectangle* r = create_rectangle(30, 40, 8, 4);

    // 使用一个通用的 Shape 指针数组来存放它们
    Shape* shapes[2];
    shapes[0] = (Shape*)c; // 向上转型
    shapes[1] = (Shape*)r;

    // 多态调用!同样的代码,根据对象的实际类型执行不同的函数
    for (int i = 0; i < 2; i++) {
        shapes[i]->draw(shapes[i]); // 调用 draw 方法
        printf("Area: %f\n", shapes[i]->area(shapes[i])); // 调用 area 方法
    }
    
    // 释放内存
    free(c);
    free(r);

    return 0;
}

多态的实现依赖于:

  1. 继承:将基类作为第一个成员(内存布局兼容)
  2. 虚函数表:用函数指针模拟(在构造时绑定)
  3. 动态绑定:通过函数指针在运行时调用正确的函数

1. 继承:将基类作为第一个成员

1.1 内存布局

CircleRectangle 都将 Shape base 作为第一个成员,所以:

Circle* c = create_circle(10, 20, 5);
// 内存布局:
// +----------------+
// | Shape base     |  <- 从偏移量 0 开始
// |   - x          |
// |   - y          |
// |   - draw       |  <- 指向 draw_circle
// |   - area       |  <- 指向 area_circle
// +----------------+
// | radius         |
// +----------------+

内存布局对比:

Circle 对象的内存布局:
地址 0x1000:  ┌──────────────┐
              │ x            │ ← Shape base 的开始
              │ y            │
              │ draw         │
              │ area         │
              ├──────────────┤
              │ radius       │
              └──────────────┘

Shape* 视角看到的:
地址 0x1000:  ┌──────────────┐
              │ x            │
              │ y            │
              │ draw         │ ← shapes[0]->draw 直接访问这里
              │ area         │
              └──────────────┘
              (后面的 radius 看不到,但不影响)
// 方式 1:通过 Shape* 直接访问(你的代码)
shapes[0]->draw(shapes[0]);

// 方式 2:显式访问 base(多此一举)
((Circle*)shapes[0])->base.draw(shapes[0]);

设计成第一个成员, 这种访问就成为了可能:

typedef struct {
    Shape base;  // 必须是第一个成员!
    int radius;
} Circle;

C 语言标准保证:

  • 结构体第一个成员的地址 = 结构体的地址
  • 即:&c->base == (Shape*)c

验证:

Circle* c = create_circle(10, 20, 5);
Shape* s = (Shape*)c;

// 这三个地址完全相同!
printf("%p\n", (void*)c);           // 0x1000
printf("%p\n", (void*)&c->base);    // 0x1000
printf("%p\n", (void*)s);           // 0x1000

// 所以这两种访问完全等价
s->draw;           //  正确
c->base.draw;      //  也正确,但不需要
  • shapes[0]->draw正确且推荐的写法
  • shapes[0]->base.draw编译错误,因为 shapes[0] 的类型是 Shape*,没有 base 成员
  • 如果要用 base,必须先转型:((Circle*)shapes[0])->base.draw,但这完全没必要

1.2 向上转型不改变指针地址

shapes[0] = (Shape*)c;  // 向上转型

这个转型只是改变了类型,但指针指向的内存地址完全相同

  • c 指向的地址 = shapes[0] 指向的地址
  • 因为 Shape base 是第一个成员,所以 &c->base == c

1.3 函数指针已经在构造时设置好

create_circle 中:

c->base.draw = draw_circle;  // 关键!
c->base.area = area_circle;

create_rectangle 中:

r->base.draw = draw_rectangle;  // 关键!
r->base.area = area_rectangle;

1.4 多态调用的完整过程

shapes[i]->draw(shapes[i]);

这行代码的执行步骤:

当 i = 0 时(Circle 对象):

  1. shapes[0] 指向 Circle 对象的起始地址
  2. shapes[0]->draw 访问该地址的 draw 成员
  3. 因为构造时设置了 c->base.draw = draw_circle
  4. 所以 shapes[0]->draw 实际上是 draw_circle 函数的地址
  5. shapes[0]->draw(shapes[0]) 等价于 draw_circle(shapes[0])

当 i = 1 时(Rectangle 对象):

  1. shapes[1] 指向 Rectangle 对象的起始地址
  2. shapes[1]->draw 访问该地址的 draw 成员
  3. 因为构造时设置了 r->base.draw = draw_rectangle
  4. 所以 shapes[1]->draw 实际上是 draw_rectangle 函数的地址
  5. shapes[1]->draw(shapes[1]) 等价于 draw_rectangle(shapes[1])

图解说明

创建 Circle 时:
shapes[0] ──────┐
                ↓
        ┌──────────────┐
        │ x = 10       │
        │ y = 20       │
        │ draw ────────┼──→ draw_circle 函数
        │ area ────────┼──→ area_circle 函数
        ├──────────────┤
        │ radius = 5   │
        └──────────────┘
创建 Rectangle 时:
shapes[1] ──────┐
                ↓
        ┌──────────────┐
        │ x = 30       │
        │ y = 40       │
        │ draw ────────┼──→ draw_rectangle 函数
        │ area ────────┼──→ area_rectangle 函数
        ├──────────────┤
        │ width = 8    │
        │ height = 4   │
        └──────────────┘

2 虚函数表:用函数指针模拟(在构造时绑定)

3 动态绑定:通过函数指针在运行时调用正确的函数

4 其他函数方法需要实现多态,也需要添加到shape基类

例如:要添加FuncFly 4.1:

typedef void (*FuncFly)(struct Shape*);

4.2. 添加到 Shape 结构体

typedef struct Shape {
    int x;
    int y;
    FuncDraw draw;
    FuncArea area;
    FuncFly fly;     // 新增!
} Shape;

4.3. 在派生类的构造函数中绑定具体实现

// Circle 的 fly 实现
void fly_circle(Shape* s) {
    Circle* c = (Circle*)s;
    printf("Circle flying at (%d, %d)!\n", c->base.x, c->base.y);
}

// Rectangle 的 fly 实现
void fly_rectangle(Shape* s) {
    Rectangle* r = (Rectangle*)s;
    printf("Rectangle can't fly! It's at (%d, %d)\n", r->base.x, r->base.y);
}

// 修改构造函数
Circle* create_circle(int x, int y, int radius) {
    Circle* c = (Circle*)malloc(sizeof(Circle));
    c->base.x = x;
    c->base.y = y;
    c->base.draw = draw_circle;
    c->base.area = area_circle;
    c->base.fly = fly_circle;    // 绑定 fly 方法
    c->radius = radius;
    return c;
}

Rectangle* create_rectangle(int x, int y, int w, int h) {
    Rectangle* r = (Rectangle*)malloc(sizeof(Rectangle));
    r->base.x = x;
    r->base.y = y;
    r->base.draw = draw_rectangle;
    r->base.area = area_rectangle;
    r->base.fly = fly_rectangle;  // 绑定 fly 方法
    r->width = w;
    r->height = h;
    return r;
}

4.4. 多态调用

for (int i = 0; i < 2; i++) {
    shapes[i]->draw(shapes[i]);
    printf("Area: %f\n", shapes[i]->area(shapes[i]));
    shapes[i]->fly(shapes[i]);   // 新增的多态调用!
}

优点

  • 所有派生类都能使用统一的接口
  • 通过基类指针就能调用所有多态方法

缺点

  1. 所有派生类都必须实现所有方法
    • 即使 Rectangle 不能 fly,也要提供 fly_rectangle 实现
  2. 基类会变得臃肿
    • 每增加一个方法,Shape 就多一个函数指针
    • 所有对象的内存占用都会增加
  3. 缺乏灵活性
    • 不能只让某些派生类实现特定方法

改进方案:接口分离

如果只有部分派生类需要 fly 方法,可以考虑:

// 单独定义一个 Flyable 接口
typedef struct {
    FuncFly fly;
} Flyable;

// 只让需要飞的类继承这个接口
typedef struct {
    Shape base;
    Flyable flyable;  // 可选的飞行能力
    int radius;
} FlyingCircle;

但这会让设计变复杂,C 语言中通常还是用你原来的方式——在基类中添加所有需要的虚函数指针

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值