C 语言本身没有 class、extends 或 virtual 等关键字,但可以通过结构体、函数指针和统一接口表达面向对象的设计思想:
- 封装:把数据和操作数据的函数组织在一起,通过接口访问对象;
- 继承:子类复用父类的属性和接口,并按需扩展或重写行为;
- 多态:使用相同接口操作不同对象,由对象决定实际执行的方法;
- 抽象:提取共同能力,让调用者依赖统一接口而不是具体类型。
本项目使用 Animal、Cat 和 Dog 演示这四个特性。重点不是把 C 语言写成 C++,而是理解面向接口、复用代码和隔离变化的方法。
OOP-By-Viys/
├─ CMakeLists.txt
└─ project
├─ animal
│ ├─ CMakeLists.txt
│ ├─ animal.c // Animal、Cat、Dog 的实现
│ └─ animal.h // 属性、接口和构造/析构函数声明
└─ app
└─ main.c // 创建对象并演示四大特性
把对象的数据与操作方法组织在一起,外部代码通过接口访问数据。
ANIMAL_CLASS 同时包含接口表 api 和属性 attr:
typedef struct {
ANIMAL_CLASS_IMPLEMENTS api;
Animal_Attr attr;
} ANIMAL_CLASS;名称由 animal_get_name() 统一读取:
static int animal_get_name(void* t, char* name) {
ANIMAL_CLASS* this = (ANIMAL_CLASS*)t;
memcpy(name, this->attr.name, strlen(this->attr.name) + 1);
return 0;
}在 main.c 中,不直接访问 attr.name,而是通过接口获取:
char name[10] = {0};
animal_cat->get_name(animal_cat, name);
printf("animal name is %s\n", name);这个例子体现的是“接口层面的封装”。由于教学需要,结构体定义仍然放在头文件中;实际项目若需要更严格的数据隐藏,可以进一步使用不透明结构体。
Cat 和 Dog 复用 Animal 已有的接口与属性,只重写自己不同的行为。
派生类与父类保持兼容布局:第一个成员都是 ANIMAL_CLASS_IMPLEMENTS api,属性都包含 name 和 sound。构造 Cat 时,先复制 Animal 的接口与属性:
memcpy(&this->api, t, sizeof(ANIMAL_CLASS_IMPLEMENTS));
memcpy(&this->attr, &parent->attr, sizeof(Cat_Attr));然后只重写 Cat 的 speak:
this->api.speak = cat_speak;Dog 的构造过程相同:
DOG_CLASS* DOG_CLASS_CTOR(ANIMAL_CLASS_IMPLEMENTS* t);因此,Cat 和 Dog 可以继续使用从 Animal 复用的 init、get_name 接口与属性,同时拥有自己的 speak 实现。
需要注意:C 语言没有原生继承。这里依靠结构体布局和接口复制模拟“继承与方法重写”,这些布局约定必须由开发者共同维护。
同一个调用入口,根据传入对象执行不同实现。
animal_sound() 也按照类方法的格式定义在 ANIMAL_CLASS_IMPLEMENTS 中,并由 ANIMAL_CLASS_CTOR() 完成绑定:
this->api.sound = animal_sound;它只依赖当前对象的统一接口:
static int animal_sound(void* t) {
ANIMAL_CLASS_IMPLEMENTS* this = (ANIMAL_CLASS_IMPLEMENTS*)t;
return this->speak(this);
}Cat 和 Dog 复制 Animal 接口时会继承 sound,同时分别重写 speak。调用代码保持相同的类方法格式:
cat->sound(cat);
dog->sound(dog);但运行结果不同:
animal cat say: Meow!
animal dog say: Woof!
原因是公共的 sound 方法会继续调用当前对象的 speak,而 Cat 和 Dog 的 speak 函数指针分别指向 cat_speak 和 dog_speak。这就是通过函数指针实现的运行时多态。
提取不同动物都具备的能力,为调用者提供统一的行为约定。
ANIMAL_CLASS_IMPLEMENTS 定义了所有动物对象可以提供的公共接口:
typedef struct {
int (*init)(void* t, Animal_Attr attr);
int (*get_name)(void* t, char* name);
int (*speak)(void* t);
int (*sound)(void* t);
} ANIMAL_CLASS_IMPLEMENTS;调用者只需要持有 ANIMAL_CLASS_IMPLEMENTS*:
ANIMAL_CLASS_IMPLEMENTS* animal_cat = NULL;
ANIMAL_CLASS_IMPLEMENTS* animal_dog = NULL;这样,main.c 关注的是“初始化动物、读取名称、让动物发声”,不需要了解每个函数内部如何实现。接口描述“对象能做什么”,具体函数负责“对象怎么做”。
本项目使用 CMake 构建,不依赖平台专用 API,可在 Windows、Linux 和 macOS 上编译。需要安装:
- CMake 3.15 或更高版本;
- 支持 C99 的 C 编译器,例如 GCC、Clang、Apple Clang、MinGW 或 MSVC。
使用 Ninja 只需要两条命令:
cmake -S . -B build -G Ninja
cmake --build build如果使用 MinGW,把第一条命令改为:
cmake -S . -B build -G "MinGW Makefiles"同一个 build 目录不能混用不同生成器。如果之前使用过 Visual Studio,需要先删除一次 build 目录,再重新执行上面的配置命令。
Windows:
.\build\oopc.exeLinux / macOS:
./build/oopc代码统一使用标准 C99 接口,并使用 \n 输出换行。Windows C 运行库会将其转换为 CRLF,Linux 和 macOS 则保持 LF,无需在源码中进行平台判断。
>
animal name is cat
animal name is dog
animal cat say: Meow!
animal dog say: Woof!
animal cat say: Meow!
animal dog say: Woof!
<
animal name is ...:通过get_name()演示封装;- 第一组
say:Cat 和 Dog 复用 Animal 接口并重写speak,演示继承; - 第二组
say:通过继承的sound()调用各自的speak(),演示多态; - 整个调用过程只依赖
ANIMAL_CLASS_IMPLEMENTS,演示抽象。
可以按照 Cat 和 Dog 的原有格式增加一个 Bird:
- 在
animal.h中定义Bird_Attr和BIRD_CLASS; - 在
animal.c中实现bird_speak(); - 在
BIRD_CLASS_CTOR()中复制 Animal 接口并重写speak; - 在
main.c中通过bird->sound(bird)验证多态。
核心代码可以保持与现有构造函数一致:
BIRD_CLASS* BIRD_CLASS_CTOR(ANIMAL_CLASS_IMPLEMENTS* t) {
BIRD_CLASS* this = (BIRD_CLASS*)malloc(sizeof(BIRD_CLASS));
ANIMAL_CLASS* parent = (ANIMAL_CLASS*)t;
memcpy(&this->api, t, sizeof(ANIMAL_CLASS_IMPLEMENTS));
memcpy(&this->attr, &parent->attr, sizeof(Bird_Attr));
this->api.speak = bird_speak;
return this;
}练习目标是:增加 Bird 后,继承得到的 sound 方法不需要增加 if/else 类型判断。
| 特性 | 在代码中的体现 |
|---|---|
| 封装 | 通过 get_name() 访问名称,由接口管理对象数据 |
| 继承 | Cat/Dog 构造函数复制 Animal 接口,并重写 speak |
| 多态 | sound() 调用对象重写后的 speak(),实际行为由函数指针决定 |
| 抽象 | ANIMAL_CLASS_IMPLEMENTS 定义所有动物的公共能力 |
本项目展示的是一种轻量级 C 面向对象写法。它适合需要统一接口和可替换实现的场景;如果业务流程简单,普通结构体和函数通常更加直接。