C 语言实现 OOP 多态的核心就是:
- 在基类中定义函数指针(虚函数表)
- 在构造函数中绑定具体实现
- 通过基类指针实现多态调用
看代码示例:
#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. 继承:将基类作为第一个成员
1.1 内存布局
Circle 和 Rectangle 都将 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 对象):
shapes[0]指向 Circle 对象的起始地址shapes[0]->draw访问该地址的draw成员- 因为构造时设置了
c->base.draw = draw_circle - 所以
shapes[0]->draw实际上是draw_circle函数的地址 shapes[0]->draw(shapes[0])等价于draw_circle(shapes[0])
当 i = 1 时(Rectangle 对象):
shapes[1]指向 Rectangle 对象的起始地址shapes[1]->draw访问该地址的draw成员- 因为构造时设置了
r->base.draw = draw_rectangle - 所以
shapes[1]->draw实际上是draw_rectangle函数的地址 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]); // 新增的多态调用!
}
优点
- 所有派生类都能使用统一的接口
- 通过基类指针就能调用所有多态方法
缺点
- 所有派生类都必须实现所有方法
- 即使 Rectangle 不能 fly,也要提供
fly_rectangle实现
- 即使 Rectangle 不能 fly,也要提供
- 基类会变得臃肿
- 每增加一个方法,Shape 就多一个函数指针
- 所有对象的内存占用都会增加
- 缺乏灵活性
- 不能只让某些派生类实现特定方法
改进方案:接口分离
如果只有部分派生类需要 fly 方法,可以考虑:
// 单独定义一个 Flyable 接口
typedef struct {
FuncFly fly;
} Flyable;
// 只让需要飞的类继承这个接口
typedef struct {
Shape base;
Flyable flyable; // 可选的飞行能力
int radius;
} FlyingCircle;
但这会让设计变复杂,C 语言中通常还是用你原来的方式——在基类中添加所有需要的虚函数指针。
3998

被折叠的 条评论
为什么被折叠?



