C语言如何实现面向对象的多态特性?

99ANYc3cd6
预计阅读时长 29 分钟
位置: 首页 C语言 正文

使用函数指针和结构体来模拟“类”和“虚函数表”(vtable)。

c语言 实现c 多态
(图片来源网络,侵删)

下面我将分步详细解释,并提供一个完整的、可运行的示例。


核心概念:C 语言如何模拟面向对象

在 C++ 中,多态通常是这样工作的:

  1. 基类:定义一个或多个虚函数。
  2. 派生类:继承基类,并重写这些虚函数。
  3. 虚函数表:编译器为每个有虚函数的类生成一个静态的函数指针表,存储了该类实际应该调用的函数地址。
  4. 虚指针:每个类的对象(实例)内部都有一个隐藏的指针(vptr),指向其所属类的虚函数表。

在 C 语言中,我们需要自己实现这几点:

  • :用 struct 来表示。struct 可以包含数据成员和函数指针成员。
  • 虚函数表:用一组 函数指针 来表示,通常是定义在 struct 内部的一组静态函数指针。
  • 对象struct 的一个实例。
  • 继承:通过将基类的 struct 作为派生类 struct 的第一个成员来实现,这确保了内存布局的兼容性,使得基类指针可以安全地指向派生类对象(类似 C++ 的 upcast)。

实现步骤:一个“动物”叫的例子

我们将创建一个基类 Animal 和两个派生类 DogCat,它们都有一个 make_sound 函数,但行为不同,我们想通过一个 Animal 类型的指针来调用正确的 make_sound 函数。

c语言 实现c 多态
(图片来源网络,侵删)

步骤 1: 定义函数指针类型

为我们的基类定义一个函数指针类型,这会让代码更清晰。

// 定义一个函数指针类型,它指向一个接受 void* 指针并返回 void 的函数。
// void* 用于模拟 C++ 中的 this 指针,指向对象实例。
typedef void (*SoundFunc)(void*);

步骤 2: 定义基类(Animal

Animal 结构体将包含:

  1. 一个虚函数表指针 vtable,它将指向一个包含所有虚函数(这里是 make_sound)地址的结构体。
  2. 数据成员,name
// 基类: Animal
// 1. 定义基类的虚函数表结构
typedef struct {
    SoundFunc make_sound; // 虚函数表中的第一个函数
} AnimalVTable;
// 2. 定义基类结构体
typedef struct {
    AnimalVTable* vtable; // 指向虚函数表的指针
    const char* name;     // 数据成员
} Animal;

步骤 3: 实现基类的虚函数

这些函数的第一个参数是 void* this,它们会通过这个指针来访问对象的数据,并调用虚函数表中正确的函数。

// 基类的 make_sound 实现
// 注意:第一个参数是 void*,代表当前对象实例
void animal_make_sound(void* this) {
    Animal* animal = (Animal*)this;
    printf("%s (generic animal) makes a sound.\n", animal->name);
}

步骤 4: 定义派生类(DogCat

派生类将包含:

c语言 实现c 多态
(图片来源网络,侵删)
  1. 基类 Animal 作为其第一个成员(这是实现“继承”的关键)。
  2. 自己特有的数据成员(如果有的话)。
// 派生类: Dog
typedef struct {
    Animal base; // 必须将基类作为第一个成员
    int breed;   // Dog 特有的属性
} Dog;
// 派生类: Cat
typedef struct {
    Animal base; // 必须将基类作为第一个成员
    bool is_indoor; // Cat 特有的属性
} Cat;

步骤 5: 实现派生类的虚函数

这些函数与基类函数签名相同,但实现不同。

// Dog 的 make_sound 实现
void dog_make_sound(void* this) {
    Dog* dog = (Dog*)this;
    printf("%s (Dog) says: Woof! Woof!\n", dog->base.name);
}
// Cat 的 make_sound 实现
void cat_make_sound(void* this) {
    Cat* cat = (Cat*)this;
    printf("%s (Cat) says: Meow!\n", cat->base.name);
}

步骤 6: 创建和初始化对象(最关键的一步)

这是手动实现 vtable 的地方,我们需要为每个派生类创建一个静态的 vtable,并在创建对象时,将对象的 vtable 指针指向正确的 vtable。

// 1. 为 Dog 创建一个静态的虚函数表
static AnimalVTable dog_vtable = {
    .make_sound = dog_make_sound // 将 dog_make_sound 函数地址存入 vtable
};
// 2. 为 Cat 创建一个静态的虚函数表
static AnimalVTable cat_vtable = {
    .make_sound = cat_make_sound // 将 cat_make_sound 函数地址存入 vtable
};
// 3. 创建 Dog 对象并初始化
void dog_ctor(Dog* this, const char* name, int breed) {
    this->base.vtable = &dog_vtable; // 关键:将对象的 vtable 指针指向 Dog 的 vtable
    this->base.name = name;
    this->breed = breed;
}
// 4. 创建 Cat 对象并初始化
void cat_ctor(Cat* this, const char* name, bool is_indoor) {
    this->base.vtable = &cat_vtable; // 关键:将对象的 vtable 指针指向 Cat 的 vtable
    this->base.name = name;
    this->is_indoor = is_indoor;
}

步骤 7: 使用多态

我们可以通过一个 Animal* 指针来操作不同类型的对象,而调用正确的函数。

// 一个通用的函数,它接受一个 Animal 指针
// 这个函数不需要知道具体是 Dog 还是 Cat
void let_animal_speak(Animal* animal) {
    // 调用虚函数
    // animal->vtable->make_sound(animal);
    // 这行代码就是多态的核心!
    // 1. animal->vtable 获取到对象对应的 vtable。
    // 2. .make_sound 从 vtable 中获取到正确的函数地址。
    // 3. (animal) 作为 this 指针传递给该函数。
    animal->vtable->make_sound(animal);
}

完整代码示例

将以上所有部分组合起来,形成一个完整的 C 程序。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// =================================================================
// 1. 定义函数指针类型
// =================================================================
typedef void (*SoundFunc)(void*);
// =================================================================
// 2. 定义基类 (Animal)
// =================================================================
// 基类的虚函数表结构
typedef struct {
    SoundFunc make_sound;
} AnimalVTable;
// 基类结构体
typedef struct {
    AnimalVTable* vtable;
    const char* name;
} Animal;
// 基类的虚函数实现
void animal_make_sound(void* this) {
    Animal* animal = (Animal*)this;
    printf("%s (generic animal) makes a sound.\n", animal->name);
}
// =================================================================
// 3. 定义派生类 (Dog, Cat)
// =================================================================
typedef struct {
    Animal base; // 基类作为第一个成员
    int breed;
} Dog;
typedef struct {
    Animal base; // 基类作为第一个成员
    bool is_indoor;
} Cat;
// 派生类的虚函数实现
void dog_make_sound(void* this) {
    Dog* dog = (Dog*)this;
    printf("%s (Dog) says: Woof! Woof! (Breed: %d)\n", dog->base.name, dog->breed);
}
void cat_make_sound(void* this) {
    Cat* cat = (Cat*)this;
    printf("%s (Cat) says: Meow! (Indoor: %s)\n", cat->base.name, cat->is_indoor ? "Yes" : "No");
}
// =================================================================
// 4. 创建和初始化对象 (构造函数)
// =================================================================
// 为 Dog 创建静态的 vtable
static AnimalVTable dog_vtable = {
    .make_sound = dog_make_sound
};
// 为 Cat 创建静态的 vtable
static AnimalVTable cat_vtable = {
    .make_sound = cat_make_sound
};
// Dog 的构造函数
void dog_ctor(Dog* this, const char* name, int breed) {
    this->base.vtable = &dog_vtable;
    this->base.name = strdup(name); // 使用 strdup 复制字符串,避免外部修改
    this->breed = breed;
}
// Cat 的构造函数
void cat_ctor(Cat* this, const char* name, bool is_indoor) {
    this->base.vtable = &cat_vtable;
    this->base.name = strdup(name);
    this->is_indoor = is_indoor;
}
// 析构函数 (为了内存安全)
void animal_dtor(Animal* animal) {
    if (animal && animal->name) {
        free((void*)animal->name);
        animal->name = NULL;
    }
}
// =================================================================
// 5. 多态接口
// =================================================================
void let_animal_speak(Animal* animal) {
    if (animal && animal->vtable) {
        animal->vtable->make_sound(animal);
    } else {
        printf("Error: Invalid animal object or vtable.\n");
    }
}
// =================================================================
// 6. 主函数 - 演示
// =================================================================
int main() {
    Dog my_dog;
    Cat my_cat;
    // 创建对象
    dog_ctor(&my_dog, "Buddy", 1); // 1 for Golden Retriever
    cat_ctor(&my_cat, "Whiskers", true);
    printf("--- Polymorphism in Action ---\n");
    // 使用基类指针数组来存储不同类型的对象
    Animal* animals[2];
    animals[0] = (Animal*)&my_dog;
    animals[1] = (Animal*)&my_cat;
    // 循环调用,体现多态
    for (int i = 0; i < 2; i++) {
        let_animal_speak(animals[i]);
    }
    printf("\n--- Direct Call (Non-Polymorphic) ---\n");
    // 也可以直接调用,但这不是多态
    dog_make_sound(&my_dog);
    cat_make_sound(&my_cat);
    // 清理资源
    animal_dtor((Animal*)&my_dog);
    animal_dtor((Animal*)&my_cat);
    return 0;
}

编译和运行

将上述代码保存为 polymorphism.c,然后使用 GCC 编译:

gcc polymorphism.c -o polymorphism

运行程序:

./polymorphism

预期输出:

--- Polymorphism in Action ---
Buddy (Dog) says: Woof! Woof! (Breed: 1)
Whiskers (Cat) says: Meow! (Indoor: Yes)
--- Direct Call (Non-Polymorphic) ---
Buddy (Dog) says: Woof! Woof! (Breed: 1)
Whiskers (Cat) says: Meow! (Indoor: Yes)

总结与优缺点

优点:

  1. 实现了多态:可以在 C 语言中获得类似 C++ 的多态行为,这对于编写可扩展的库和框架非常有用。
  2. 性能高效:函数指针的调用开销在现代 CPU 上非常小,几乎和直接调用一样快,这比使用 switch-case 或字符串比较来分发函数要高效得多。
  3. 灵活性:提供了强大的抽象能力,可以隐藏实现细节,让代码更模块化。

缺点:

  1. 代码冗长且繁琐:需要手动定义 vtable、结构体、构造/析构函数,代码量比 C++ 多很多,容易出错。
  2. 类型安全降低void* 指针的使用削弱了 C 语言的类型检查,如果将错误的类型 void* 传递给函数,会导致未定义行为。
  3. 没有语言级支持:编译器不会自动检查 vtable 是否正确初始化,所有错误都需要在运行时由程序员自己处理。
  4. 维护困难:对于不熟悉这种模式的开发者来说,代码的意图可能不清晰,增加了维护成本。

尽管如此,在 C 语言项目中,尤其是在嵌入式系统、高性能库(如 OpenGL, SDL)等领域,这种手动实现多态的技术仍然非常普遍和重要。

-- 展开阅读全文 --
头像
dede站内搜索代码如何直接使用?
« 上一篇 04-10
织梦pubdate是什么?
下一篇 » 04-10

相关文章

取消
微信二维码
支付宝二维码

目录[+]