Ответ 1
Существует три различных способа достижения полиморфизма в C:
-
Измените код
В функциях базового класса простоswitch
для идентификатора типа класса для вызова специализированных версий. Пример неполного кода:typedef enum classType { CLASS_A, CLASS_B } classType; typedef struct base { classType type; } base; typedef struct A { base super; ... } A; typedef struct B { base super; ... } B; void A_construct(A* me) { base_construct(&me->super); super->type = CLASS_A; } int base_foo(base* me) { switch(me->type) { case CLASS_A: return A_foo(me); case CLASS_B: return B_foo(me); default: assert(0), abort(); } }
Конечно, это утомительно для больших классов.
-
Сохранить указатели функций в объекте
Вы можете избежать операторов switch, используя указатель на функцию для каждой функции-члена. Опять же, это неполный код:typedef struct base { int (*foo)(base* me); } base; //class definitions for A and B as above int A_foo(base* me); void A_construct(A* me) { base_construct(&me->super); me->super.foo = A_foo; }
Теперь код вызова может просто сделать
base* anObject = ...; (*anObject->foo)(anObject);
В качестве альтернативы вы можете использовать макрос препроцессора по строкам:
#define base_foo(me) (*me->foo)(me)
Обратите внимание, что это дважды оценивает выражение
me
, так что это действительно плохая идея. Это может быть исправлено, но это выходит за рамки этого ответа. -
Использовать vtable
Поскольку все объекты класса имеют один и тот же набор функций-членов, все они могут использовать одни и те же указатели. Это очень близко к тому, что делает С++ под капотом:typedef struct base_vtable { int (*foo)(base* me); ... } base_vtable; typedef struct base { base_vtable* vtable; ... } base; typedef struct A_vtable { base_vtable super; ... } A_vtable; //within A.c int A_foo(base* super); static A_vtable gVtable = { .foo = A_foo, ... }; void A_construct(A* me) { base_construct(&me->super); me->super.vtable = &gVtable; };
Опять же, это позволяет коду пользователя выполнять отправку (с одной дополнительной косвенностью):
base* anObject = ...; (*anObject->vtable->foo)(anObject);
Какой метод вы должны использовать, зависит от задачи. Подход switch
легко взломать для двух или трех небольших классов, но громоздкий для больших классов и иерархий. Второй подход масштабируется намного лучше, но у него много места накладных расходов из-за дублирования указателей на функции. Подход vtable требует довольно немного дополнительной структуры и вводит еще больше косвенности (что делает код более трудным для чтения), но, безусловно, это путь для сложных иерархий классов.