核心思路:
- 对象 (Object): 用
struct
来表示。结构体包含对象的数据成员(属性)。 - 方法 (Method): 用普通的 C 函数来表示。这些函数通常接受一个指向对象结构体的指针作为第一个参数(约定俗成地命名为
self
或this
),用来操作对象的数据。 - 封装 (Encapsulation): 通过信息隐藏技术(如 Opaque Pointers 或在 .c 文件中定义结构体)来限制对对象内部数据的直接访问。
- 继承 (Inheritance): 通过在子结构体中包含父结构体(通常作为第一个成员)来模拟。
- 多态 (Polymorphism): 通过在结构体中使用函数指针(通常组织成一个“虚函数表” vtable)来模拟。
1. 模拟封装 (Encapsulation)
封装是将数据和操作数据的方法捆绑在一起,并隐藏对象的内部实现细节。
- 数据: 使用
struct
定义对象的状态。 - 方法: 创建操作该
struct
指针的函数。 - 隐藏:
- 方法一 (推荐 - Opaque Pointer): 在公共头文件 (
.h
) 中只提供结构体的前向声明或typedef
一个指针类型,但不给出完整的结构体定义。这样用户只能通过你提供的函数来操作对象,无法直接访问其成员。 - 方法二 (简单): 在
.c
文件中定义完整的struct
,在.h
文件中只提供函数声明。这种方式下,同一模块内的其他代码理论上仍可访问,但模块外的代码不行。
- 方法一 (推荐 - Opaque Pointer): 在公共头文件 (
示例:一个简单的计数器对象
// counter.h (公共接口 - 使用 Opaque Pointer)
#ifndef COUNTER_H
#define COUNTER_H
// 1. 前向声明结构体,但不定义它,创建了一个不完整类型
typedef struct Counter Counter;
// 2. 或者直接 typedef 一个指针类型 (更常用)
// typedef struct Counter* CounterHandle;
// 下面的函数参数就要用 CounterHandle
// 3. 提供“构造函数”和“析构函数” (模拟)
Counter* Counter_create(int initial_value);
void Counter_destroy(Counter* self);
// 4. 提供公共方法
void Counter_increment(Counter* self);
int Counter_get_value(const Counter* self); // 如果方法不修改对象,用 const
#endif // COUNTER_H
// ---
// counter.c (私有实现)
#include "counter.h"
#include <stdlib.h> // for malloc, free
#include <stdio.h> // for printf in methods if needed
// 5. 在 .c 文件中给出完整的结构体定义
struct Counter {
int value;
// 可以有其他私有成员
};
// 6. 实现构造函数
Counter* Counter_create(int initial_value) {
Counter* self = (Counter*)malloc(sizeof(Counter));
if (self == NULL) {
perror("Failed to allocate Counter");
return NULL;
}
self->value = initial_value;
printf("Counter created with value %d\n", self->value);
return self;
}
// 7. 实现析构函数
void Counter_destroy(Counter* self) {
if (self) {
printf("Destroying Counter with value %d\n", self->value);
free(self);
}
}
// 8. 实现方法
void Counter_increment(Counter* self) {
if (!self) return; // 总是指针检查
self->value++;
printf("Counter incremented to %d\n", self->value);
}
int Counter_get_value(const Counter* self) {
if (!self) return -1; // 或者其他错误码
return self->value;
}
// ---
// main.c (使用 Counter 对象)
// #include "counter.h"
//
// int main() {
// Counter* c1 = Counter_create(10);
// Counter* c2 = Counter_create(0);
//
// if (c1) {
// Counter_increment(c1); // 输出: Counter incremented to 11
// int val = Counter_get_value(c1); // val = 11
// // c1->value = 99; // 错误!无法访问,因为 Counter 定义不完整
// }
//
// if (c2) {
// Counter_increment(c2); // 输出: Counter incremented to 1
// }
//
// Counter_destroy(c1); // 输出: Destroying Counter with value 11
// Counter_destroy(c2); // 输出: Destroying Counter with value 1
//
// return 0;
// }
2. 模拟继承 (Inheritance)
C 语言没有原生的继承。模拟继承通常有两种方式:
-
结构体嵌套 (Embedding for “is-a” relationship):
- 将“父类”结构体作为“子类”结构体的第一个成员。
- 这样,“子类”对象的指针可以安全地转换为“父类”对象的指针(因为内存布局开头相同)。
- 可以调用“父类”的方法,只需将子类指针强制转换为父类指针即可。
-
组合 (Composition for “has-a” relationship):
- 在一个结构体中包含指向其他结构体的指针。
- 通过调用包含对象的方法来实现功能。
- 这通常比模拟继承更灵活、更简单,也是 C 中更常用的方式。
示例:结构体嵌套模拟继承 (Shape -> Circle)
// shape.h
#ifndef SHAPE_H
#define SHAPE_H
typedef struct Shape Shape; // Opaque pointer
struct Shape { // 在 .c 中定义
float x, y;
};
Shape* Shape_create(float x, float y);
void Shape_destroy(Shape* self);
void Shape_move(Shape* self, float dx, float dy);
void Shape_print_pos(const Shape* self);
#endif // SHAPE_H
// ---
// circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "shape.h" // 包含父类头文件
typedef struct Circle Circle; // Opaque pointer
// 公共方法
Circle* Circle_create(float x, float y, float radius);
void Circle_destroy(Circle* self);
float Circle_get_radius(const Circle* self);
void Circle_draw(const Circle* self); // Circle 特有的方法
// 可以复用 Shape 的方法,需要提供一个获取 Shape 指针的函数或直接转换
Shape* Circle_as_Shape(Circle* self);
#endif // CIRCLE_H
// ---
// circle.c
#include "circle.h"
#include "shape.h" // Shape 的实现也需要,或者只用其接口
#include <stdlib.h>
#include <stdio.h>
// 子类结构体定义
struct Circle {
struct Shape base; // 父类结构体作为第一个成员!
float radius; // 子类特有成员
};
// 返回基类指针的辅助函数
Shape* Circle_as_Shape(Circle* self) {
return (Shape*)self;
}
Circle* Circle_create(float x, float y, float radius) {
Circle* self = (Circle*)malloc(sizeof(Circle));
if (!self) return NULL;
// 初始化父类部分 - 可以直接访问,因为在同一个 .c 文件中
// 或者调用 Shape_create (但 Shape_create 可能不知道 Circle 的大小)
// 更好的方式是提供 Shape_init(Shape* self, ...) 函数
self->base.x = x;
self->base.y = y;
// 初始化子类部分
self->radius = radius;
printf("Circle created at (%.1f, %.1f) with radius %.1f\n", x, y, radius);
return self;
}
void Circle_destroy(Circle* self) {
if (self) {
printf("Destroying Circle\n");
// 如果父类有需要清理的资源,应该先调用父类的清理函数
// Shape_cleanup(&self->base); // 假设有这样一个函数
free(self);
}
}
float Circle_get_radius(const Circle* self) {
return self ? self->radius : -1.0f;
}
void Circle_draw(const Circle* self) {
if (!self) return;
printf("Drawing Circle at (%.1f, %.1f), radius %.1f\n",
self->base.x, self->base.y, self->radius);
}
// ---
// main.c usage
// #include "circle.h"
// int main() {
// Circle* c = Circle_create(1, 2, 5.0);
// if (c) {
// Circle_draw(c);
// // 调用继承自 Shape 的方法 (通过类型转换)
// Shape_move((Shape*)c, 10, 10); // 将 Circle* 转为 Shape*
// Shape_print_pos((Shape*)c); // 输出新的位置
// // 或者使用辅助函数
// // Shape_move(Circle_as_Shape(c), 10, 10);
// // Shape_print_pos(Circle_as_Shape(c));
// Circle_destroy(c);
// }
// return 0;
// }
注意: 这种继承模拟比较脆弱,主要依赖于内存布局。没有 C++ 中继承的很多特性(如访问控制、构造函数链式调用等)。
3. 模拟多态 (Polymorphism)
多态允许我们以统一的方式处理不同类型的对象。在 C 中,主要通过函数指针实现。
- 在“父类”结构体中定义一个包含函数指针的结构体(通常称为 vtable 或 vptr,即虚函数表指针)。
- 每个“子类”都提供自己实现的函数,并将这些函数的地址填充到自己的 vtable 中。
- 创建子类对象时,将其 vtable 指针指向正确的 vtable。
- 通过调用 vtable 中的函数指针来执行操作,这样就会调用到对应子类的实现。
示例:使用 vtable 实现 Shape 的多态 draw()
和 area()
// shape_poly.h
#ifndef SHAPE_POLY_H
#define SHAPE_POLY_H
// 前向声明
typedef struct Shape Shape;
typedef struct ShapeVTable ShapeVTable;
// 虚函数表结构体
struct ShapeVTable {
void (*draw)(const Shape* self); // 函数指针 draw
float (*area)(const Shape* self); // 函数指针 area
void (*destroy)(Shape* self); // 函数指针 destroy
};
// 父类结构体,包含 vtable 指针
struct Shape {
const ShapeVTable* vtable; // 指向虚函数表的指针 (通常 const)
float x, y; // 父类数据成员
};
// 辅助函数,通过 vtable 调用方法 (通常 inline)
static inline void Shape_draw(const Shape* self) {
if (self && self->vtable && self->vtable->draw) {
self->vtable->draw(self);
}
}
static inline float Shape_area(const Shape* self) {
if (self && self->vtable && self->vtable->area) {
return self->vtable->area(self);
}
return 0.0f; // Or error
}
static inline void Shape_destroy(Shape* self) {
if (self && self->vtable && self->vtable->destroy) {
self->vtable->destroy(self);
}
}
// 父类初始化函数 (可选,但推荐)
void Shape_init(Shape* self, const ShapeVTable* vtable, float x, float y);
#endif // SHAPE_POLY_H
// ---
// circle_poly.h
#ifndef CIRCLE_POLY_H
#define CIRCLE_POLY_H
#include "shape_poly.h" // 包含父类
typedef struct Circle {
Shape base; // 父类作为第一个成员
float radius; // 子类成员
} Circle;
// 子类构造函数
Circle* Circle_create(float x, float y, float radius);
#endif // CIRCLE_POLY_H
// ---
// circle_poly.c
#include "circle_poly.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h> // for M_PI (might need -lm link flag)
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// --- Circle 类方法的具体实现 ---
static void Circle_draw_impl(const Shape* self) {
const Circle* circle = (const Circle*)self; // 类型转换回来
printf("Drawing Circle (poly) at (%.1f, %.1f) with radius %.1f\n",
circle->base.x, circle->base.y, circle->radius);
}
static float Circle_area_impl(const Shape* self) {
const Circle* circle = (const Circle*)self;
return (float)M_PI * circle->radius * circle->radius;
}
static void Circle_destroy_impl(Shape* self) {
Circle* circle = (Circle*)self;
printf("Destroying Circle (poly) with radius %.1f\n", circle->radius);
// 如果 Circle 内部有动态分配的资源,在这里释放
free(circle); // 最后释放对象本身
}
// --- Circle 类的虚函数表 (通常是 static const) ---
static const ShapeVTable g_circle_vtable = {
.draw = Circle_draw_impl,
.area = Circle_area_impl,
.destroy = Circle_destroy_impl
};
// --- Circle 类的构造函数 ---
Circle* Circle_create(float x, float y, float radius) {
Circle* self = (Circle*)malloc(sizeof(Circle));
if (!self) return NULL;
// 调用父类初始化,并传入 Circle 的 vtable
Shape_init((Shape*)self, &g_circle_vtable, x, y);
// 初始化子类成员
self->radius = radius;
printf("Circle (poly) created.\n");
return self;
}
// --- Shape 的初始化函数实现 (可以放在 shape_poly.c) ---
void Shape_init(Shape* self, const ShapeVTable* vtable, float x, float y) {
if (!self) return;
self->vtable = vtable;
self->x = x;
self->y = y;
}
// ---
// main.c (使用多态)
// #include "shape_poly.h"
// #include "circle_poly.h"
// // #include "rectangle_poly.h" // 假设也定义了 Rectangle 类
//
// int main() {
// Shape* shapes[2]; // 创建一个父类指针数组
//
// // 创建不同类型的对象,但都赋值给父类指针
// shapes[0] = (Shape*)Circle_create(1, 1, 5.0);
// // shapes[1] = (Shape*)Rectangle_create(2, 2, 10.0, 5.0); // 假设有 Rectangle
//
// // 统一调用方法,会自动调用到子类的实现
// for (int i = 0; i < 1; ++i) { // 只用了 Circle
// if (shapes[i]) {
// Shape_draw(shapes[i]); // 调用 Circle_draw_impl
// float area = Shape_area(shapes[i]); // 调用 Circle_area_impl
// printf(" Area = %.2f\n", area);
// }
// }
//
// // 统一销毁对象
// for (int i = 0; i < 1; ++i) {
// Shape_destroy(shapes[i]); // 调用 Circle_destroy_impl
// }
//
// return 0;
// }
总结与局限性
- 优点: 可以在 C 中实现更好的代码组织、封装和一定程度的多态,提高代码的可维护性和复用性。
- 局限性:
- 手动管理: 构造、析构、内存管理都需要手动完成,容易出错。
- 语法繁琐: 需要编写更多的模板代码(如 vtable 定义、类型转换)。
- 类型安全: 类型转换 (
(Shape*)c
) 依赖程序员保证正确性,编译器无法检查。 - 无 RTTI: 没有运行时的类型信息。
- 继承限制: 模拟的继承不如原生 OOP 语言强大和灵活。