这是一个非常重要且容易引起混淆的话题,因为 C 语言本身是一个过程式编程语言,而不是像 C++ 或 Java 那样的面向对象编程语言,C 语言没有内置的、原生的“对象”类型。

(图片来源网络,侵删)
理解 C 语言中的“对象”关键在于区分两个层面:
- C 语言标准定义下的“对象”:这是一个非常基础和底层的概念。
- C 程序员实践中的“对象”:这是一种编程思想,通过 C 语言的特性来模拟面向对象编程。
C 语言标准定义下的“对象”
在 C 语言标准(以及 C++ 标准)中,“对象”是存储区域,这个定义非常抽象,但它包含了所有可以存储数据的东西。
任何在内存中占据一块空间、并可以保存数据的东西都是对象。
对象的类型:

(图片来源网络,侵删)
- 变量:这是最常见的对象。
- 基本数据类型对象:
int a = 10;,float b = 3.14;,char c = 'A';,变量a,b,c都是对象。 - 数组对象:
int arr[5] = {1, 2, 3, 4, 5};,整个数组arr是一个对象,它包含了 5 个int类型的子对象。 - 结构体/联合体对象:
struct Point { int x; int y; }; struct Point p = {10, 20};,结构体变量p是一个对象。
- 基本数据类型对象:
- 函数参数:函数的参数也是对象。
- 函数返回值:函数的返回值在它被返回的瞬间也是一个对象。
关键特性:
- 有存储地址:每个对象在内存中都有一个确定的地址,可以用
&操作符获取。 - 有类型:每个对象都有一个类型,这决定了它的大小、在内存中的布局以及可以执行的操作。
- 有值:对象在任意时刻都保存着一个值。
从标准角度看,C 语言中充满了“对象”。 int, float, struct, union, array 等都是对象的类型,这个定义是纯粹基于内存的。
C 程序员实践中的“对象”(模拟 OOP)
当我们说“在 C 语言中实现面向对象”时,我们指的是利用 C 语言的特性(主要是结构体和函数指针)来模拟 OOP 的三大核心特性:封装、继承、多态。
下面我们通过一个经典的例子——实现一个简单的“形状”系统——来展示这个过程。

(图片来源网络,侵删)
核心思想
- 封装:使用
struct将数据(属性)捆绑在一起,将操作数据的函数(方法)通过函数指针也放在struct中,从而将数据和行为封装成一个“对象”。 - 继承:在 C 语言中,没有继承语法,我们通常使用结构体嵌套来模拟,一个“派生”的结构体会包含一个“基”结构体作为它的第一个成员。
- 多态:通过函数指针和结构体嵌套来实现,基结构体中包含一个函数指针表(通常称为 vtable - virtual table),调用函数时,通过基结构体的指针来调用,具体执行哪个函数的版本,取决于这个指针实际指向的是哪个派生结构体的函数。
完整示例:模拟形状系统
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// =========================================================================
// 1. 定义“对象”的基类 - Shape
// =========================================================================
// 这是所有形状的公共接口,我们将其视为一个抽象基类。
typedef struct {
// 虚函数表指针 - 这是实现多态的关键
// 它指向一个包含函数指针的结构体
void (*draw)(void* self);
void (*area)(void* self);
void (*destroy)(void* self);
} ShapeVtbl;
// Shape 对象本身
typedef struct {
ShapeVtbl* vtable; // 指向虚函数表的指针
// 可以在这里添加所有形状共有的属性
char* name;
} Shape;
// =========================================================================
// 2. 定义具体的“对象” - Circle
// =========================================================================
// Circle 的属性
typedef struct {
Shape base; // 继承:Circle 包含一个 Shape 作为其第一个成员
double radius;
} Circle;
// Circle 的方法实现
void Circle_draw(void* self) {
Circle* c = (Circle*)self;
printf("Drawing a Circle with radius: %f\n", c->radius);
}
void Circle_area(void* self) {
Circle* c = (Circle*)self;
double a = M_PI * c->radius * c->radius;
printf("Circle area: %f\n", a);
}
void Circle_destroy(void* self) {
Circle* c = (Circle*)self;
free(c->name); // 释放 name 分配的内存
free(c); // 释放 Circle 对象本身
}
// Circle 的虚函数表
static ShapeVtbl CircleVtbl = {
.draw = Circle_draw,
.area = Circle_area,
.destroy = Circle_destroy
};
// Circle 的“构造函数”
Circle* Circle_create(double radius) {
Circle* c = (Circle*)malloc(sizeof(Circle));
if (!c) return NULL;
c->base.vtable = &CircleVtbl; // 关联到自己的虚函数表
c->base.name = "Circle";
c->radius = radius;
return c;
}
// =========================================================================
// 3. 定义另一个具体的“对象” - Rectangle
// =========================================================================
typedef struct {
Shape base; // 继承
double width;
double height;
} Rectangle;
void Rectangle_draw(void* self) {
Rectangle* r = (Rectangle*)self;
printf("Drawing a Rectangle with width: %f, height: %f\n", r->width, r->height);
}
void Rectangle_area(void* self) {
Rectangle* r = (Rectangle*)self;
double a = r->width * r->height;
printf("Rectangle area: %f\n", a);
}
void Rectangle_destroy(void* self) {
Rectangle* r = (Rectangle*)self;
free(r->name);
free(r);
}
static ShapeVtbl RectangleVtbl = {
.draw = Rectangle_draw,
.area = Rectangle_area,
.destroy = Rectangle_destroy
};
Rectangle* Rectangle_create(double width, double height) {
Rectangle* r = (Rectangle*)malloc(sizeof(Rectangle));
if (!r) return NULL;
r->base.vtable = &RectangleVtbl;
r->base.name = "Rectangle";
r->width = width;
r->height = height;
return r;
}
// =========================================================================
// 4. 使用“对象”
// =========================================================================
void render_shape(Shape* shape) {
// 这就是多态!
// 我们只知道它是一个 Shape,不知道具体是 Circle 还是 Rectangle。
// 调用 shape->vtable->draw(shape) 时,程序会根据 shape->vtable
// 实际指向的是 CircleVtbl 还是 RectangleVtbl 来决定调用哪个函数。
shape->vtable->draw(shape);
}
void calculate_shape_area(Shape* shape) {
shape->vtable->area(shape);
}
int main() {
// 创建 Circle 对象
Shape* shapes[2];
shapes[0] = (Shape*)Circle_create(5.0);
shapes[1] = (Shape*)Rectangle_create(4.0, 6.0);
// 使用基类指针数组来处理不同的对象
for (int i = 0; i < 2; i++) {
render_shape(shapes[i]);
calculate_shape_area(shapes[i]);
}
// 销毁对象
for (int i = 0; i < 2; i++) {
shapes[i]->vtable->destroy(shapes[i]);
}
return 0;
}
代码解析
Shape结构体:扮演了“基类”的角色,它包含一个vtable指针,这个指针指向一个包含draw,area,destroy等函数指针的结构体。Circle和Rectangle结构体:扮演了“派生类”的角色,它们通过将Shape作为第一个成员来模拟继承,这非常重要,因为它保证了Shape*指针可以安全地转换为Circle*或Rectangle*(这被称为“异构容器”或“向上转型”)。CircleVtbl和RectangleVtbl:这是“虚函数表”,每个具体的“类”都有一张自己的表,里面存放了它自己实现的函数地址。Circle_create和Rectangle_create:模拟了构造函数,它们负责分配内存、初始化成员,并最关键的一步:将对象的vtable指针指向自己的函数表。render_shape函数:这是多态的体现,它接收一个Shape*基类指针,当调用shape->vtable->draw(shape)时,程序会查找shape当前指向的对象的vtable,并调用表中对应的draw函数。shape指向一个Circle,就调用Circle_draw;如果指向一个Rectangle,就调用Rectangle_draw。destroy方法:模拟了析构函数,负责释放内存,防止内存泄漏。
C 语言模拟 OOP 的优缺点
优点:
- 性能高:本质上是直接函数指针调用,几乎没有额外的运行时开销(比 C++ 的虚函数调用可能还要快一点点)。
- 灵活性强:程序员对内存布局和设计有完全的控制权。
- 可移植性好:不依赖任何特定的 OOP 编译器特性。
缺点:
- 语法繁琐:需要手动编写大量的结构体、函数指针和“构造/析构”函数,代码量巨大,容易出错。
- 缺乏语言层面的支持:没有
public,private,protected等访问控制修饰符,封装性完全靠程序员自觉和命名约定来维持。 - 调试困难:复杂的指针操作和类型转换使得调试比真正的 OOP 语言更困难。
- 类型安全性较低:C 语言的类型检查相对宽松,不正确的类型转换可能导致未定义行为。
在 C 语言中谈论“对象”时,你需要明确上下文:
- 从标准定义看:任何有内存地址的数据都是对象,如
int,struct,array等。 - 从编程实践看:C 程序员通过
struct+function pointers的强大组合来模拟面向对象编程的封装、继承和多态,从而构建出结构清晰、可维护的大型项目,虽然过程繁琐,但这是一种在 C 语言生态中非常成熟和强大的设计模式。
