C语言实现面向对象三大特性 : 封装、继承、多态(2)

hegangben
2025-08-27 / 0 评论 / 0 阅读 / 正在检测是否收录...

http://bennyhe.cn/index.php/archives/1709/
这个公众号写的例子解释得不清楚,让deepseek帮忙写了一个例子,加上解释。

以下是一个使用C语言模拟面向对象编程的完整示例,通过结构体和函数指针实现封装、继承和多态三大特性:
代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
// 基类:Shape(图形)
typedef struct Shape {
    // 虚函数表
    struct ShapeVTable* vtable;
    
    // 公共属性
    float x;
    float y;
} Shape;
 
// 虚函数表定义 
typedef struct ShapeVTable {
    // 多态方法
    float (*area)(Shape* self);
    void (*move)(Shape* self, float dx, float dy);
    void (*draw)(Shape* self);
} ShapeVTable;
 
// 继承自Shape的子类:Circle(圆形)
typedef struct Circle {
    Shape base;  // 继承基类 
    float radius;  // 特有属性 
} Circle;
 
// 继承自Shape的子类:Rectangle(矩形)
typedef struct Rectangle {
    Shape base;  // 继承基类 
    float width;
    float height;
} Rectangle;
 
// ======= Shape的方法实现 =======
float shape_area(Shape* self) {
    printf("调用基类area虚方法\n");
    return 0.0f;
}
 
void shape_move(Shape* self, float dx, float dy) {
    self->x += dx;
    self->y += dy;
    printf("图形移动到(%.1f, %.1f)\n", self->x, self->y);
}
 
void shape_draw(Shape* self) {
    printf("在(%.1f, %.1f)绘制图形\n", self->x, self->y);
}
 
// 基类虚函数表实例 
static ShapeVTable shape_vtable = {
    .area = shape_area,
    .move = shape_move,
    .draw = shape_draw
};
 
// ======= Circle的方法实现 =======
float circle_area(Shape* self) {
    Circle* circle = (Circle*)self;
    return M_PI * circle->radius * circle->radius;
}
 
void circle_draw(Shape* self) {
    Circle* circle = (Circle*)self;
    printf("在(%.1f, %.1f)绘制圆形,半径: %.1f\n", 
           circle->base.x, circle->base.y, circle->radius);
}
 
// Circle虚函数表(覆盖基类方法)
static ShapeVTable circle_vtable = {
    .area = circle_area,
    .move = shape_move,  // 复用基类方法
    .draw = circle_draw
};
 
// Circle构造函数
Circle* create_circle(float x, float y, float radius) {
    Circle* circle = malloc(sizeof(Circle));
    circle->base.vtable  = &circle_vtable;
    circle->base.x = x;
    circle->base.y = y;
    circle->radius = radius;
    return circle;
}
 
// ======= Rectangle的方法实现 =======
float rectangle_area(Shape* self) {
    Rectangle* rect = (Rectangle*)self;
    return rect->width * rect->height;
}
 
void rectangle_draw(Shape* self) {
    Rectangle* rect = (Rectangle*)self;
    printf("在(%.1f, %.1f)绘制矩形,尺寸: %.1fx%.1f\n", 
           rect->base.x, rect->base.y, rect->width, rect->height);
}
 
// Rectangle虚函数表 
static ShapeVTable rectangle_vtable = {
    .area = rectangle_area,
    .move = shape_move,
    .draw = rectangle_draw
};
 
// Rectangle构造函数 
Rectangle* create_rectangle(float x, float y, float width, float height) {
    Rectangle* rect = malloc(sizeof(Rectangle));
    rect->base.vtable  = &rectangle_vtable;
    rect->base.x = x;
    rect->base.y = y;
    rect->width = width;
    rect->height = height;
    return rect;
}
 
// ======= 多态演示函数 =======
void process_shape(Shape* shape) {
    printf("\n处理图形对象:\n");
    shape->vtable->draw(shape);
    printf("面积: %.2f\n", shape->vtable->area(shape));
    shape->vtable->move(shape, 2.0f, 3.0f);
}
 
// ======= 主函数 =======
int main() {
    // 创建派生类对象 
    Circle* circle = create_circle(1.0f, 1.0f, 5.0f);
    Rectangle* rect = create_rectangle(3.0f, 4.0f, 6.0f, 8.0f);
    
    // 演示多态 
    process_shape((Shape*)circle);
    process_shape((Shape*)rect);
    
    // 释放内存 
    free(circle);
    free(rect);
    
    return 0;
}

面向对象特性实现解析:

1. 封装(Encapsulation)

结构体封装:将数据和方法绑定在Shape结构体中
私有性模拟:通过.c文件隔离实现细节(示例中为简化展示在单文件)
构造函数:create_circle()和create_rectangle()实现对象初始化

2. 继承(Inheritance)

结构体嵌套:派生类包含基类结构体作为第一个成员

typedef struct Circle {
    Shape base;  // 继承基类成员
    float radius; // 新增属性 
} Circle;

内存布局兼容:派生类指针可直接转为基类指针

3. 多态(Polymorphism)

虚函数表(VTable):通过ShapeVTable实现动态绑定

typedef struct ShapeVTable {
    float (*area)(Shape* self);
    void (*move)(Shape* self, float dx, float dy);
} ShapeVTable;

方法覆盖:派生类重写虚表函数指针

static ShapeVTable circle_vtable = {
    .area = circle_area,  // 覆盖基类方法 
    .move = shape_move    // 复用基类方法
};

4. 多态调用

// 统一接口处理不同派生类
void process_shape(Shape* shape) {
    shape->vtable->draw(shape);  // 动态绑定到具体实现 
    shape->vtable->move(shape, 2, 3);
}

执行结果示例:

处理图形对象:
在(1.0, 1.0)绘制圆形,半径: 5.0
面积: 78.54
图形移动到(3.0, 4.0)

处理图形对象:
在(3.0, 4.0)绘制矩形,尺寸: 6.0x8.0
面积: 48.00
图形移动到(5.0, 7.0)

扩展实现技巧:

内存管理:可添加destroy()虚函数实现对象析构
类型安全:增加type_id字段实现运行时类型检查
接口抽象:定义纯虚函数表作为接口规范
多重继承:通过多个基类结构体嵌套实现(需谨慎处理内存布局)
动态创建:使用注册表和字符串ID创建对象
💡 实际应用场景:Linux内核(task_struct)、GTK+ GUI库(GObject)、CPython解释器(PyObject)均使用类似技术实现面向对象编程

此方案展示了C语言实现OOP的核心模式,虽然需要手动管理虚表和内存,但提供了接近原生OOP语言的功能和性能。

问题1:针对上面例子,

typedef struct ShapeVTable {
float (*area)(Shape* self);
void (*move)(Shape* self, float dx, float dy);
} ShapeVTable;

中area 是指什么?是一个function 还是struct?
在C语言面向对象编程示例中,typedef struct ShapeVTable 定义中的 area 是一个函数指针,而非普通函数或结构体。以下是详细解析:

typedef struct ShapeVTable {
    float (*area)(Shape* self);      // ← 核心声明 
    void (*move)(Shape* self, float dx, float dy);
} ShapeVTable;

特性 说明
类型 函数指针 (Function Pointer)
作用 指向计算图形面积的具体实现函数
内存表示 存储函数入口地址的指针(通常4/8字节)
调用方式 通过指针间接调用:shape->vtable->area(shape)
多态关键 不同子类(Circle/Rectangle)可指向不同的实现函数
float:函数返回值类型

(area):声明名为 area 的指针( 表示指针)
(Shape* self):函数参数列表(接收Shape指针)
metj7p5r.png
关键价值:实现多态

// 统一接口处理不同图形
void print_area(Shape* shape) {
    // 动态调用具体实现
    float a = shape->vtable->area(shape); 
    printf("面积: %.2f\n", a);
}

圆形对象:实际调用 circle_area()
矩形对象:实际调用 rectangle_area()
新增图形:只需实现新的area函数并更新虚表

问题2:上面例子中

// Circle虚函数表(覆盖基类方法)
static ShapeVTable circle_vtable = {
.area = circle_area,
.move = shape_move, // 复用基类方法
.draw = circle_draw
}

中 虚函数表就是C语言的列表吗? .area 表示什么意思?
本质:虚函数表是包含函数指针成员的静态结构体实例,通过为不同子类创建不同的静态实例实现多态。

🔑 .area 成员详解
.area 的双重身份
作为结构体成员
在ShapeVTable定义中声明为成员变量:

typedef struct ShapeVTable {
    float (*area)(Shape* self);  // 成员声明 
} ShapeVTable;

作为函数指针容器
初始化时赋予具体函数地址

.area = circle_area  // 函数指针赋值

️ 运行时工作原理
多态调用流程

sequenceDiagram
    participant Main as main()
    participant Shape as Shape指针
    participant VTable as circle_vtable 
    participant Function as circle_area()
    
    Main ->> Shape: (Shape*)circle
    Shape ->> VTable: 访问vtable
    VTable ->> Function: 调用.area成员指向的函数
    Function -->> Shape: 计算圆形面积 
    Function -->> Main: 返回结果 

键技术点
静态初始化
static关键字确保虚函数表:

在编译期分配内存
在程序启动时初始化
生命周期贯穿整个程序
点运算符(.)作用
在初始化中使用.area是C99的指定初始化器(Designated Initializer):
函数指针转换
编译器自动处理函数类型转换:

// circle_area实际类型:float (*)(Circle*)
// 虚表要求类型:float (*)(Shape*)
.area = (float(*)(Shape*))circle_area // 编译器隐式转换 

核心价值体现

// 统一接口 
float calculate_area(Shape* shape) {
    return shape->vtable->area(shape); // 多态魔法发生处 
}

圆形对象:调用circle_area()
radius² × π
矩形对象:调用rectangle_area()
width × height
新增图形:只需实现新函数并更新虚表
⚠️ 重要注意事项:

函数签名必须严格匹配(返回类型+参数类型)
派生类函数需通过强制转换访问子类特有成员

问题3:上面例子中main 函数里面的process_shape((Shape*)circle);

process_shape((Shape)rect); 中 process_shape((Shape)circle); 就是shape->vtable->draw(shape); 这里draw 就是static ShapeVTable circle_vtable 中绑定的circle_draw 这个方法吗? process_shape((Shape*)rect); 做shape->vtable->draw(shape); 这里draw 就是static ShapeVTable rectangle_vtable 中绑定的rectangle_draw 这个方法吗? 是怎么做到在rectangle_vtable 和 circle_vtable 自动匹配的?

是的,您完全理解正确!下面我用清晰的逻辑解析这个多态调用的精妙机制:
对于圆形对象:
✅ process_shape((Shape*)circle) 中的 draw 调用
✅ 确实绑定到 circle_vtable 的 circle_draw
🔄 调用链:shape->vtable->draw → circle_vtable.draw → circle_draw
对于矩形对象:
✅ process_shape((Shape*)rect) 中的 draw 调用
✅ 确实绑定到 rectangle_vtable 的 rectangle_draw
🔄 调用链:shape->vtable->draw → rectangle_vtable.draw → rectangle_draw
自动匹配的实现原理
一、对象创建时的类型烙印

// 创建圆形对象时 
Circle* create_circle(...) {
    Circle* circle = malloc(sizeof(Circle));
    circle->base.vtable  = &circle_vtable;  // 关键烙印!
}

circle->base.vtable 被永久赋值为 &circle_vtable 的地址
即使转为基类指针 (Shape*)circle,内存中的 vtable 指针保持不变
调用时的动态解析
当执行 shape->vtable->draw(shape):

访问 vtable 指针

// 从对象内存中读取vtable地址 
ShapeVTable* vtable_ptr = shape->vtable; 

对 circle:获得 circle_vtable 地址
对 rect:获得 rectangle_vtable 地址
定位函数指针

// 从vtable结构体获取draw成员
void (*draw_func)(Shape*) = vtable_ptr->draw;

编译器自动计算成员偏移量(固定偏移)
间接函数调用

draw_func(shape); // 执行指针指向的函数 

CPU直接跳转到函数入口地址
circle_vtable.draw → circle_draw
rectangle_vtable.draw → rectangle_draw
⚙️ 关键技术保障

  1. 内存布局一致性
    对象类型 内存偏移0位置 意义
    Circle Shape base C标准保证基类位于起始地址
    Rectangle Shape base 使得 (Circle)→(Shape) 转换安全
  2. 静态绑定机制
    cpp
    复制
    // 编译时即确定的绑定关系(非运行时查找)
    static ShapeVTable circle_vtable = {
    .draw = circle_draw // 编译期硬编码绑定
    };
  3. 函数指针本质

    typedef void (*DrawFunc)(Shape*); // 统一函数签名 
     
    // 等价于:
    draw_func(shape);
    // 汇编实现:
    mov rax, [shape]       ; 取vtable地址
    mov rbx, [rax+8]       ; 取draw指针(假设偏移8字节)
    call rbx               ; 间接调用

💡 多态效果图解
mermaid
sequenceDiagram

participant Caller as process_shape()
participant Object as Shape对象
participant VTable as 虚函数表
participant Impl as 具体实现

Caller ->> Object: shape->vtable->draw()
Object ->> VTable: 查询我的vtable
alt 圆形对象 
    VTable ->> Impl: 调用circle_draw()
else 矩形对象
    VTable ->> Impl: 调用rectangle_draw()
end 
Impl -->> Caller: 执行结果 

关键结论:
自动匹配的核心在于每个对象创建时,其内部 vtable 指针已被永久绑定到专属虚函数表。类型转换只改变指针解释方式,不改变对象内存本质。当通过基类指针调用虚函数时,实际通过原始 vtable 指针找到最终实现。

这种设计完美模拟了C++的虚函数机制,Linux内核(如file_operations)、Windows驱动模型(WDF_OBJECT_ATTRIBUTES)等核心系统均采用此方案实现多态。

0

评论 (0)

取消