一、面向对象基本概念
面向对象编程基于三个核心概念:封装、继承和多态。
1. 封装
将数据和操作这些数据的方法封装在一起,形成一个独立的单元(类)。这种机制隐藏了内部实现细节,只暴露必要的接口。
2. 继承
允许基于已存在的类(父类)定义新类(子类),子类可以复用父类的属性和方法,并可以添加新的属性和方法。
3. 多态
允许不同类的对象对同一消息作出响应,实现方法的动态绑定,提高了代码的灵活性和可扩展性。
二、C语言模拟封装实现
封装是面向对象编程的基础。在C语言中,我们可以使用结构体来封装数据,并通过函数指针来封装方法。
0. 头文件枚举值定义
typedef enum {
HAL_OK = 0x00,
HAL_ERROR = 0x01,
} HAL_StatusTypeDef;
1. String 类的实现
1. 结构体定义
// 定义String结构体,模拟类
typedef struct String {
/* 成员函数 */
// HAL_StatusTypeDef(*_init_string)(String* self, const char* str); // 构造函数(无法在对象初始化前通过对象调用)
HAL_StatusTypeDef(*_destroy_string)(struct String* self); // 析构函数
HAL_StatusTypeDef(*_PrintString)(const struct String* const self); // 打印String
/* 成员变量 */
char* _str; // 字符串
size_t _size; // 长度
size_t _capacity; // 容量
} String;
2. 构造函数
/* String对象构造函数 */
HAL_StatusTypeDef init_string(String* self, const char* str)
{
// 初始化成员变量
self->_size = strlen(str);
self->_capacity = self->_size;
self->_str = (char*)malloc(sizeof(char) * self->_size + 1);
memcpy(self->_str, str, self->_size + 1);
// 注入成员函数
// self->_init_string = init_string;
self->_destroy_string = destroy_string;
self->_PrintString = PrintString;
return HAL_OK;
}
说明:
构造函数负责初始化对象和注入方法
需要手动调用,模拟构造函数行为
3. 析构函数
/* String对象析构函数 */
HAL_StatusTypeDef destroy_string(String* self)
{
free(self->_str);
self->_str = NULL;
return HAL_OK;
}
说明:
显式释放分配的内存
需要手动调用,模拟析构函数行为
4. 功能函数
/* String对象打印函数 */
HAL_StatusTypeDef PrintString(const String* const self)
{
printf("%s\n", self->_str);
return HAL_OK;
}
2. Person 类的实现
1. 结构体定义
// 定义Person结构体,模拟类
typedef struct Person {
/* 成员函数 */
// HAL_StatusTypeDef(*_init_person)(Person* self, char* name, int age); // 构造函数(无法在对象初始化前通过对象调用)
HAL_StatusTypeDef(*_destroy_person)(struct Person* self); // 析构函数
HAL_StatusTypeDef(*_speak)(const struct Person* const self); // 指向speak方法的指针
/* 成员变量 */
char* _name; // 姓名
int _age; // 年龄
} Person;
2. 构造函数
/* Person对象构造函数 */
HAL_StatusTypeDef init_person(Person* self, char* name, int age) {
// 初始化成员变量
self->_name = name;
self->_age = age;
self->_speak = speak_func; // 函数名就是函数的地址,与&speak_func等价
// self->_init_person = init_person; // 将构造函数的地址赋值给_init_person
self->_destroy_person = destroy_person; // 将析构函数的地址赋值给_destroy_person
return HAL_OK;
}
3. 析构函数
/* Person对象析构函数 */
HAL_StatusTypeDef destroy_person(Person* self) {
self->_name = NULL;
self->_age = 0;
return HAL_OK;
}
4. 功能函数
// 定义speak方法
HAL_StatusTypeDef speak_func(const Person* const self) {
printf("Hello, I am %s, %d years old.\n", self->_name, self->_age);
return HAL_OK;
}
3. main测试代码
int main() {
/* 封装类 */
String s1; // 创建String实例
init_string(&s1, "Hello world!"); // 构造函数
s1._PrintString(&s1); // 调用功能函数
s1._destroy_string(&s1); // 析构函数
Person p1; // 创建Person实例
init_person(&p1, "Alice", 30); // 构造函数
p1._speak(&p1); // 调用功能函数
p1._destroy_person(&p1); // 析构函数
return 0;
}
说明:
演示了对象的创建、使用和销毁过程
展示了如何调用对象的方法
三、C语言模拟继承实现
1. 子类 Student 结构体定义
// 定义Student结构体,模拟类
typedef struct Student {
/* 继承父类 */
Person _Person; // 继承Person类
// 放在子类结构体的第一个成员,这样可以通过(Person*)self获取父类的指针,传入父类的构造函数和析构函数
/* 成员函数 */
HAL_StatusTypeDef(*_print_score)(const struct Student* const self); // 打印分数
HAL_StatusTypeDef(*_destroy_Student)(struct Student* self); // 析构函数
/* 成员变量 */
int _math_score; // 数学分数
int _English_score; // 英语分数
int _Chinese_score; // 语文分数
} Student;
说明:
通过结构体嵌套实现继承
父类作为子类的第一个成员,便于类型转换
2. Student类构造函数
/* 构造函数 */
HAL_StatusTypeDef init_Student(Student* const self, const char* name, int age, int math_score, int English_score, int Chinese_score)
{
// 初始化父类结构体
init_person((Person*)self, name, age);
// 初始化成员变量
self->_Chinese_score = Chinese_score;
self->_English_score = English_score;
self->_math_score = math_score;
self->_print_score = print_score;
self->_destroy_Student = destroy_Student;
return HAL_OK;
}
说明:
先初始化父类部分,再初始化子类特有部分
通过类型转换将Student转换为Person
3. Student类析构函数
HAL_StatusTypeDef destroy_Student(Student* self) // 析构函数
{
// 先析构子类
self->_Chinese_score = 0;
self->_English_score = 0;
self->_math_score = 0;
// 再析构父类
self->_Person._destroy_person((Person*)self);
return HAL_OK;
}
说明:
析构顺序:先子类后父类
模拟了继承体系中的析构过程
4. 功能函数实现
HAL_StatusTypeDef print_score(const Student* const self) // 打印分数
{
printf("Math: %d\n", self->_math_score);
printf("English: %d\n", self->_English_score);
printf("Chinese: %d\n", self->_Chinese_score);
return HAL_OK;
}
5. main测试代码
int main() {
/* 继承 */
Student stu2; // 创建Student实例
init_Student(&stu2, "Bob", 20, 90, 85, 95); // 构造函数
stu2._print_score(&stu2); // 打印成绩
stu2._Person._speak(&stu2); // 说话函数,存在隐式类型转换,将子类结构体地址转化成父类结构体地址
stu2._destroy_Student(&stu2); // 析构函数
// 查看是否完成析构
printf("stu2 math_score:%d\n", stu2._math_score);
printf("stu2 name:%p\n", stu2._Person._name);
return 0;
}
三、C语言模拟多态实现
1. 多态实现完整代码
#include <stdio.h>
// 1. 定义“基类”Animal:包含通用属性(name)和“虚函数指针”(speak)
// 核心:用函数指针封装“共有的方法接口”,不同子类通过赋值不同函数实现差异化
typedef struct {
const char* name;
void (*speak)(const void* self); // 函数指针:指向“发声”方法(self指向具体对象)
} Animal;
// 2. 定义“子类”Dog:通过嵌套基类Animal实现“继承”
// 逻辑:子类结构体包含基类结构体作为第一个成员,模拟“子类继承基类的属性和方法”
typedef struct {
Animal base;
const char* breed;
} Dog;
// 3. 实现Dog的“发声”方法(对应基类的speak接口)
// 参数self:通用指针,指向具体的Dog对象(需强制转换)
void dog_speak(const void* self) {
// 将通用指针转为Dog*,才能访问其嵌套的base成员
const Dog* dog = (const Dog*)self;
printf("[Dog] %s (breed: %s) barks: Wang Wang!\n", dog->base.name, dog->breed);
}
// 4. 定义“子类”Cat:同样通过嵌套Animal实现“继承”
typedef struct {
Animal base;
const char* color;
} Cat;
// 5. 实现Cat的“发声”方法(对应基类的speak接口)
void cat_speak(const void* self) {
const Cat* cat = (const Cat*)self;
printf("[Cat] %s (color: %s) meows: Miao Miao!\n", cat->base.name, cat->color);
}
// 6. 封装“创建Dog对象”的函数(相当于构造函数)
// 功能:初始化Dog的属性,并将其base的speak指向Dog的实现
Dog* create_dog(const char* name, const char* breed) {
// 简化示例:用静态变量避免堆内存管理(实际开发可改用malloc)
static Dog dog;
//Dog* dog = malloc(szieof(Dog)); 这时引用dog的成员要使用 ->
dog.base.name = name;
dog.base.speak = dog_speak;
dog.breed = breed;
return &dog;
}
// 7. 封装“创建Cat对象”的函数(构造函数)
Cat* create_cat(const char* name, const char* color) {
static Cat cat;
//Cat* cat= malloc(szieof(Cat));
cat.base.name = name;
cat.base.speak = cat_speak;
cat.color = color;
return &cat;
}
// 8. 通用接口函数:接收“基类指针”(Animal*),调用speak方法
// 核心:通过基类指针调用函数指针,实现“多态”——同一接口,不同实现
void animal_speak(const Animal* animal) {
if (animal && animal->speak) {
// 调用函数指针,传入具体对象(animal指向的实际是Dog/Cat的base成员)
animal->speak(animal);
}
}
int main() {
Dog* dog = create_dog("Buddy", "Golden Retriever");
Cat* cat = create_cat("Luna", "White");
// 多态体现:调用同一接口animal_speak,根据实际对象类型执行不同实现
animal_speak(&dog->base); // 传入Dog的base成员(Animal类型),执行dog_speak
animal_speak(&cat->base); // 传入Cat的base成员(Animal类型),执行cat_speak
return 0;
}
核心原理解析(C 语言模拟多态的 3 个关键)
C 语言没有类、继承、虚函数等面向对象特性,上述代码通过3 个核心技术模拟多态,本质是 “接口统一,实现分离”:
用 “结构体嵌套” 模拟 “继承关系”
面向对象中,子类继承基类的属性和方法;C 语言中,通过 “子类结构体嵌套基类结构体” 实现类似效果。
例如
Dog
结构体第一个成员是Animal base
,这意味着:Dog
对象的内存布局中,开头部分与Animal
完全一致(可直接将&dog->base
视为Animal*
指针)。Dog
可以 “继承”Animal
的属性(name
)和方法接口(speak
函数指针)。
子类可额外添加特有属性(如
Dog
的breed
、Cat
的color
),实现 “扩展”。
用 “函数指针” 模拟 “虚函数”(多态的核心)
面向对象的多态依赖 “虚函数”(基类声明虚函数,子类重写);C 语言中,用基类结构体中的函数指针模拟 “虚函数接口”。
基类
Animal
中的void (*speak)(const void* self)
是 “通用方法接口”,所有子类(Dog、Cat)都需实现对应的具体函数(dog_speak
、cat_speak
)。创建子类对象时(如
create_dog
函数),将基类的speak
函数指针绑定到子类的具体实现(dog->base.speak = dog_speak
),这一步相当于 “子类重写虚函数”。
用 “基类指针” 实现 “动态绑定”
多态的关键是 “运行时根据实际对象类型,调用对应实现”(动态绑定);C 语言中,通过 “基类指针(Animal)指向子类对象的基类成员*” 实现。
在
main
函数中,animal_speak(&dog->base)
和animal_speak(&cat->base)
调用的是同一个接口animal_speak
,但传入的Animal*
指针实际指向:&dog->base
:指向Dog
对象中的Animal
成员,其speak
绑定的是dog_speak
。&cat->base
:指向Cat
对象中的Animal
成员,其speak
绑定的是cat_speak
。
运行时,
animal->speak(animal)
会根据speak
指针实际指向的函数(dog_speak
或cat_speak
)执行,从而实现 “同一接口,不同行为” 的多态效果。
main()
|
| 创建对象
+--> create_dog() --> Dog对象.base.speak = dog_speak
|
+--> create_cat() --> Cat对象.base.speak = cat_speak
|
| 多态调用
+--> animal_speak(&dog->base)
| |
| +--> animal->speak(animal) --> 实际调用 dog_speak(dog)
|
+--> animal_speak(&cat->base)
|
+--> animal->speak(animal) --> 实际调用 cat_speak(cat)
运行结果
[Dog] Buddy (breed: Golden Retriever) barks: Wang Wang!
[Cat] Luna (color: White) meows: Miao Miao!
核心总结
C 语言模拟多态的本质是 “用结构化思维手动实现面向对象的核心逻辑”,对比面向对象语言(如 C++):
面向对象概念(C++) | C语言模拟方式 |
基类(父类)/派生类(子类) | 结构体嵌套(子类包含基类结构体) |
虚函数 |
基类结构体中的函数指针 |
重写(覆盖)(Override) |
子类对象给基类函数指针赋值 |
动态绑定 |
基类指针指向子类的基类成员 |
这种方式的局限性:需要手动管理 “继承”(结构体嵌套)和 “虚函数绑定”(函数指针赋值),没有编译器自动支持;但通过统一接口封装不同实现,能让代码更灵活、可扩展。