Как я могу симулировать полиморфизм ОО-стиля в C?
Есть ли способ написать OO-подобный код в C
язык программирования?
Смотрите также:
Найден поиском по "[c] oo".
13 ответов
Первый компилятор C++ ("C с классами") фактически генерирует код на C, так что это определенно выполнимо.
По сути, ваш базовый класс является структурой; производные структуры должны включать базовую структуру в первой позиции, так что указатель на "производную" структуру также будет действительным указателем на базовую структуру.
typedef struct {
data member_x;
} base;
typedef struct {
struct base;
data member_y;
} derived;
void function_on_base(struct base * a); // here I can pass both pointers to derived and to base
void function_on_derived(struct derived * b); // here I must pass a pointer to the derived class
Функции могут быть частью структуры как указатели на функции, так что становится возможен синтаксис, такой как p->call(p), но вам все равно придется явно передавать указатель на структуру самой функции.
Общий подход заключается в определении структуры с указателями на функции. Это определяет "методы", которые могут быть вызваны для любого типа. Затем подтипы устанавливают свои собственные функции в этой общей структуре и возвращают ее.
Например, в ядре Linux есть структура:
struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
struct dentry * (*lookup) (struct inode *,struct dentry *,
struct nameidata *);
...
};
Каждый зарегистрированный тип файловой системы затем регистрирует свои собственные функции для create
, lookup
и остальные функции. Остальная часть кода может использовать общие inode_operations:
struct inode_operations *i_op;
i_op -> create(...);
C++ не так далеко от C.
Классы - это структуры со скрытым указателем на таблицу указателей функций с именем VTable. Сам Vtable является статичным. Когда типы указывают на Vtables с той же структурой, но указатели указывают на другую реализацию, вы получаете полиморфизм.
Рекомендуется инкапсулировать логику вызовов в функцию, которая принимает структуру в качестве параметра, чтобы избежать путаницы кода.
Вы должны также инкапсулировать создание и инициализацию структур в функциях (это эквивалентно конструктору C++) и удаление (деструктор в C++). В любом случае, это хорошая практика.
typedef struct
{
int (*SomeFunction)(TheClass* this, int i);
void (*OtherFunction)(TheClass* this, char* c);
} VTable;
typedef struct
{
VTable* pVTable;
int member;
} TheClass;
Чтобы вызвать метод:
int CallSomeFunction(TheClass* this, int i)
{
(this->pVTable->SomeFunction)(this, i);
}
Я посмотрел на ответы всех остальных и придумал это:
#include <stdio.h>
typedef struct
{
int (*get)(void* this);
void (*set)(void* this, int i);
int member;
} TheClass;
int Get(void* this)
{
TheClass* This = (TheClass*)this;
return This->member;
}
void Set(void* this, int i)
{
TheClass* This = (TheClass*)this;
This->member = i;
}
void init(TheClass* this)
{
this->get = &Get;
this->set = &Set;
}
int main(int argc, char **argv)
{
TheClass name;
init(&name);
(name.set)(&name, 10);
printf("%d\n", (name.get)(&name));
return 0;
}
Я надеюсь, что это отвечает на некоторые вопросы.
Приложение B статьи " Открытые многократно используемые объектные модели" Иана Пимарта и Алессандро Варта из VPRI представляет собой реализацию объектной модели в GNU C, около 140 строк кода. Это увлекательное чтение!
Вот некэшированная версия макроса, который отправляет сообщения объектам, используя расширение GNU для C (выражение оператора):
struct object;
typedef struct object *oop;
typedef oop *(*method_t)(oop receiver, ...);
//...
#define send(RCV, MSG, ARGS...) ({ \
oop r = (oop)(RCV); \
method_t method = _bind(r, (MSG)); \
method(r, ##ARGS); \
})
В том же документе, посмотрите на object
, vtable
, vtable_delegated
а также symbol
структуры, а _bind
а также vtable_lookup
функции.
Ура!
Что мне обычно нравится делать, так это обертывать структуры в другую, которая содержит метаинформацию об обернутом классе, а затем создавать списки функций, подобные посетителям, действующие на общую структуру. Преимущество этого подхода в том, что вам не нужно изменять существующие структуры, и вы можете создавать посетителей для любого подмножества структур.
Возьмем обычный пример:
typedef struct {
char call[7] = "MIAO!\n";
} Cat;
typedef struct {
char call[6] = "BAU!\n";
} Dog;
Мы можем обернуть 2 стойки в эту новую структуру:
typedef struct {
const void * animal;
AnimalType type;
} Animal;
Тип может быть простым int, но не будем лениться:
typedef enum {
ANIMAL_CAT = 0,
ANIMAL_DOG,
ANIMAL_COUNT
} AnimalType;
Было бы неплохо иметь несколько функций обертывания:
Animal catAsAnimal(const Cat * c) {
return (Animal){(void *)c, ANIMAL_CAT};
}
Animal dogAsAnimal(const Dog * d) {
return (Animal){(void *)d, ANIMAL_DOG};
}
Теперь мы можем определить нашего "посетителя":
void catCall ( Animal a ) {
Cat * c = (Cat *)a.animal;
printf(c->call);
}
void dogCall ( Animal a ) {
Dog * d = (Dog *)a.animal;
printf(d->call);
}
void (*animalCalls[ANIMAL_COUNT])(Animal)={&catCall, &dogCall};
Тогда фактическое использование будет:
Cat cat;
Dog dog;
Animal animals[2];
animals[0] = catAsAnimal(&cat);
animals[1] = dogAsAnimal(&dog);
for (int i = 0; i < 2; i++) {
Animal a = animals[i];
animalCalls[a.type](a);
}
Недостатком этого подхода является то, что вам нужно обертывать структуры каждый раз, когда вы хотите использовать их в качестве универсального типа.
#include <stdio.h>
typedef struct {
int x;
int z;
} base;
typedef struct {
base;
int y;
int x;
} derived;
void function_on_base( base * a) // here I can pass both pointers to derived and to base
{
printf("Class base [%d]\n",a->x);
printf("Class base [%d]\n",a->z);
}
void function_on_derived( derived * b) // here I must pass a pointer to the derived class
{
printf("Class derived [%d]\n",b->y);
printf("Class derived [%d]\n",b->x);
}
int main()
{
derived d;
base b;
printf("Teste de poliformismo\n");
b.x = 2;
d.y = 1;
b.z = 3;
d.x = 4;
function_on_base(&b);
function_on_base(&d);
function_on_derived(&b);
function_on_derived(&d);
return 0;
}
Выход был:
Class base [3]
Class base [1]
Class base [4]
Class derived [2]
Class derived [3]
Class derived [1]
Class derived [4]
так что это работает, это полиморфный код.
UncleZeiv объяснил об этом в начале.
Файловые функции fopen, fclose, fread являются примерами OO-кода на C. Вместо закрытых данных в классе они работают со структурой FILE, которая используется для инкапсуляции данных, а функции C действуют как функции класса-члена. http://www.amazon.com/File-Structures-Object-Oriented-Approach-C/dp/0201874016
Я успешно добился полиморфизма в C, поэтому мне захотелось поделиться своим кодом. У меня есть структура Pas, которая "наследуется" от структуры Zivotinja (Pas означает Dog, Zivotinja означает Animal BTW).
И в Zivotinja, и в Pas первым полем структуры является vTable. У Zivotinja есть vTable типа ZivotinjaVTable, у Pas есть vTable типа PasVTable. Итак, у нас есть
typedef struct ZivotinjaVTableStruct{
void (*ispisiPodatkeOZivotinji)(void *zivotinja);
int (*dajGodine) (void *zivotinja);
} ZivotinjaVTable;
typedef struct ZivotinjaStruct{
ZivotinjaVTable *vTable;
int godine;
} Zivotinja;
и у нас есть
typedef struct PasVTableStruct{
void (*ispisiPodatkeOZivotinji)(void *Pas);
int (*dajGodine) (void *Pas);
bool (*daLiJeVlasnikStariji) (void *Pas);
} PasVTable;
typedef struct PasStruct{
PasVTable *vTable;
int godine;
const char* vlasnik;
int godineVlasnika;
} Pas;
Не беспокойтесь об именах функций, это не имеет значения. Так или иначе, затем я написал функции для обеих этих vTable. Как я связал vTables с функциями, которые написал для них? Я создал глобальную структуру как для ZivotinjaVTable, так и для PasVTable. я создалvTableZivotinjaGlobal
иvTablePasGlobal
которые имеют указатели на функции, которые я написал. Затем я создал функцииPas_new()
иZivotinja_new()
которые инициализируют поля vTable так, чтобы они указывали на эти глобальные структуры vTable. Обратите внимание на важные детали в приведенном выше коде. Важно то, что vTables являются первыми полями в их структурах. Таким образом, когда мы пишем
Zivotinja *z = (Zivotinja*) Pas_new(/* init variables */);
z->vTable->someMethod(z);
компилятор знает, что vTable является первым полем в структуре Zivotinja, поэтому, когда компилятор читает, он будет идти по адресу памяти, по которому первые 8 байтов вашей структурыz
указать на (или первые 4 байта, если у вас 32-битный ПК, но это не имеет значения для того, о чем я говорю). Вот как я обманул компьютер, так как этот указатель z на самом деле указывает на структуру Pas и посколькуPasVTable *vTable
является первым полем структуры Pas, после того как мы фактически окажемся по адресу памяти pasVTableGlobal, а не по адресу памяти zivotinjaVTableGlobal.
Теперь еще одна очень важная деталь, она должна быть на одном месте и в ZivotinjaVTable, и в PasVTable. Я имею в виду - еслиsomeMethod
является вторым полем в ZivotinjaVTable, тогда оно должно быть вторым полем в PasVTable. Почему? Потому что допустим, что someMethod является вторым полем ZivotinjaVTable, когда компилятор читаетz->vTable->someMethod(z);
компьютер займет вторые 8 байт в адресе памятиz->vTable
и он поместит эти 8 байтов в указатель инструкций (или вторые 4 байта, если у вас 32-битный ПК, но опять же, это не имеет значения). Компьютер «думает», что он помещает вторые 8 байт ZivotinjaVTable в указатель инструкций, но на самом деле он помещает вторые 8 байтов PasVTable в указатель инструкций. Вот как работает трюк, потому что функция, которую мы хотим, чтобы компьютер выполнил, также является вторым полем (но PasVTable, а не ZivotinjaVTable), компьютер будет «думать», что он выполняет вторую функцию ZivotinjaVTable, но на самом деле он будет выполнять вторую функцию PasVTable.
Таким образом, подытоживая, vTables должны быть в одном и том же месте в ваших структурах, и ваши структуры должны иметь соответствующие методы в одних и тех же местах в своих vTables. То же самое касается и других полей ваших структур. Второе поле структуры Zivotinja соответствует второму полю структуры Pas, таким образом, когда вы пишете
animal_which_is_actually_a_dog->age = 10;
Вы будете обманывать компилятор в основном так же, как и с vTables (вы будете обманывать его так же, как я описал выше). Вот весь код, в основной функции можно написать следующее
Zivotinja *zivotinja = Zivotinja_new(10);
zivotinja->vTable->ispisiPodatkeOZivotinji(zivotinja);
Zivotinja *pas = Pas_new_sve(5, 50, "Milojko");
pas->vTable->ispisiPodatkeOZivotinji(pas);
int godine = pas->vTable->dajGodine(pas);
printf("The dog which was casted to an animal is %d years old.\n", godine);
Тогда это код для Zivotinja
typedef struct ZivotinjaVTableStruct{
void (*ispisiPodatkeOZivotinji)(void *zivotinja);
int (*dajGodine) (void *zivotinja);
} ZivotinjaVTable;
typedef struct ZivotinjaStruct{
ZivotinjaVTable *vTable;
int godine;
} Zivotinja;
void ispisiPodatkeOOvojZivotinji(Zivotinja* zivotinja){
printf("Ova zivotinja ima %d godina. \n", zivotinja->godine);
}
int dajGodineOveZivotinje(Zivotinja *z){
return z->godine;
}
struct ZivotinjaVTableStruct zivotinjaVTableGlobal = {ispisiPodatkeOOvojZivotinji, dajGodineOveZivotinje};
Zivotinja* Zivotinja_new(int godine){
ZivotinjaVTable *vTable = &zivotinjaVTableGlobal;
Zivotinja *z = (Zivotinja*) malloc(sizeof(Zivotinja));
z->vTable = vTable;
z->godine = godine;
}
И, наконец, код для Pas
typedef struct PasVTableStruct{
void (*ispisiPodatkeOZivotinji)(void *Pas);
int (*dajGodine) (void *Pas);
bool (*daLiJeVlasnikStariji) (void *Pas);
} PasVTable;
typedef struct PasStruct{
PasVTable *vTable;
int godine;
const char* vlasnik;
int godineVlasnika;
} Pas;
void ispisiPodatkeOPsu(void *pasVoid){
Pas *pas = (Pas*)pasVoid;
printf("Pas ima %d godina, vlasnik se zove %s, vlasnik ima %d godina. \n", pas->godine, pas->vlasnik, pas->godineVlasnika);
}
int dajGodinePsa(void *pasVoid){
Pas *pas = (Pas*) pasVoid;
return pas->godine;
}
bool daLiJeVlasnikStariji(Pas *pas){
return pas->godineVlasnika >= pas->godine;
}
struct PasVTableStruct pasVTableGlobal = {
ispisiPodatkeOPsu,
dajGodinePsa,
daLiJeVlasnikStariji
};
Pas* Pas_new(int godine){
Pas *z = (Pas*) malloc(sizeof(Pas));
z->vTable = (&pasVTableGlobal);
}
Pas *Pas_new_sve(int godine, int godineVlasnika, char* imeVlasnika){
Pas *pas = (Pas*) malloc(sizeof(Pas));
pas->godine = godine;
pas->godineVlasnika = godineVlasnika;
pas->vlasnik = imeVlasnika;
pas->vTable = &pasVTableGlobal;
}
Из Википедии: В языках программирования и теории типов полиморфизм (от греческого πολύς, polys, "многие, много" и μορφή, morphē, "форма, форма") представляет собой единый интерфейс для сущностей разных типов.
Поэтому я бы сказал, что единственный способ реализовать это в C - это использовать переменные аргументы вместе с некоторым (полу) автоматическим управлением информацией о типах. Например, в C++ вы можете написать (извините за банальность):
void add( int& result, int a1, int a2 );
void add( float& result, float a1, float a2 );
void add( double& result, double a1, double a2 );
В C, среди других решений, лучшее, что вы можете сделать, это что-то вроде этого:
int int_add( int a1, int a2 );
float float_add( float a1, fload a2 );
double double_add( double a1, double a2 );
void add( int typeinfo, void* result, ... );
Тогда вам нужно:
- реализовать "typeinfo" с перечислениями / макросами
- реализовать последнюю функцию с помощью stdarg.h
- попрощаться с C статической проверкой типов
Я почти уверен, что любая другая реализация полиморфизма должна выглядеть так же, как эта. Приведенные выше ответы, кажется, пытаются адресовать наследование больше, чем полиморфизм!
Очень грубый пример простой перегрузки функций, многого можно достичь с помощью макросов с переменным числом аргументов.
#include <stdio.h>
#include <stdlib.h>
#define SCOPE_EXIT(X) __attribute__((cleanup (X)))
struct A
{
int a;
};
struct B
{
int a, b;
};
typedef struct A * A_id;
typedef struct B * B_id;
A_id make_A()
{
return (A_id)malloc(sizeof(struct A));
}
void destroy_A(A_id * ptr)
{
free(*ptr);
*ptr = 0;
}
B_id make_B()
{
return (B_id)malloc(sizeof(struct B));
}
void destroy_B(B_id * ptr)
{
free(*ptr);
*ptr = 0;
}
void print_a(A_id ptr)
{
printf("print_a\n");
}
void print_b(B_id ptr)
{
printf("print_b\n");
}
#define print(X) _Generic((X),\
A_id : print_a, \
B_id : print_b\
)(X)
int main()
{
A_id aa SCOPE_EXIT(destroy_A) = make_A();
print(aa);
B_id bb SCOPE_EXIT(destroy_B) = make_B();
print(bb);
return 0;
}
Для того чтобы построить функциональность OO в C, вы можете посмотреть на предыдущие ответы.
Но (как было задано в других вопросах, перенаправленных на этот вопрос), если вы хотите понять, что такое полиморфизм, на примерах на языке Си. Может быть, я ошибаюсь, но я не могу придумать ничего более легкого для понимания, чем арифметика указателей Си. По моему мнению, арифметика указателей по своей природе полиморфна в Си. В следующем примере та же функция (метод в ОО), а именно сложение (+), будет вызывать другое поведение в зависимости от свойств входных структур.
Пример:
double a*;
char str*;
a=(double*)malloc(2*sizeof(double));
str=(char*)malloc(2*sizeof(char));
a=a+2; // make the pointer a, point 2*8 bytes ahead.
str=str+2; // make the pointer str, point 2*1 bytes ahead.
Отказ от ответственности: я очень новичок в C и очень жду, когда меня исправят, и я буду учиться на комментариях других пользователей или даже полностью стереть этот ответ, если он будет неправильным. Большое спасибо,
Различные реализации функций — одна из ключевых особенностей полиморфизма, поэтому необходимо использовать указатели на функции.
animal.h
typedef struct Animal {
const void (*jump)(struct Animal *self);
} Animal;
pig.h
#include "animal.h"
typedef struct {
Animal animal_interface;
char *name;
} Pig;
Pig *NewPig(char *name);
pig.c
#include <stdio.h>
#include <stdlib.h>
#include "pig.h"
static void PigJump(Animal *_self) {
Pig *self = (Pig *)_self;
printf("%s Pig jump.\n", self->name);
}
Pig *NewPig(char *name) {
Pig *self = (Pig *)malloc(sizeof(Pig));
self->animal_interface.jump = PigJump;
self->name = name;
return self;
}
main.c
#include "pig.h"
int main() {
Animal *a = &(NewPig("Peppa")->animal_interface);
Animal *b = &(NewPig("Daddy")->animal_interface);
a->jump(a);
b->jump(b);
return 0;
}
Выход:
Peppa Pig jump.
Daddy Pig jump.