Объектная ориентация в C

Каким будет набор отличных препроцессорных хаков (совместимый с ANSI C89 / ISO C90), которые позволяют использовать какую-то уродливую (но полезную) объектно-ориентированную ориентацию в C?

Я знаком с несколькими объектно-ориентированными языками, поэтому, пожалуйста, не отвечайте на такие ответы, как «Learn C ++!». Я прочитал « Объектно-ориентированное программирование с ANSI C » (остерегайтесь: формат PDF ) и несколько других интересных решений, но меня больше всего интересует ваше :-)!


См. Также Можете ли вы написать объектно-ориентированный код в C?

C Object System (COS) звучит многообещающе (он все еще находится в альфа-версии). Он пытается сохранить минимальные доступные концепции для простоты и гибкости: единообразное объектно-ориентированное программирование, включая открытые classы, метаclassы, метаclassы свойств, дженерики, мультиметоды, делегирование, право собственности, исключения, контракты и закрытия. Существует проект документа (PDF), который описывает его.

Исключение в C – реализация C89 TRY-CATCH-FINALLY, найденная на других языках OO. Он поставляется с testuite и некоторыми примерами.

И Лоран Денияу, который много работает над ООП в C.

Я бы посоветовал использовать препроцессор (ab), чтобы попытаться сделать синтаксис Си более похожим на еще один объектно-ориентированный язык. На самом базовом уровне вы просто используете простые структуры как объекты и передаете их указателями:

struct monkey { float age; bool is_male; int happiness; }; void monkey_dance(struct monkey *monkey) { /* do a little dance */ } 

Чтобы получить такие вещи, как наследование и polymorphism, вам нужно работать немного сложнее. Вы можете выполнять ручное наследование, если первый член структуры является экземпляром суперclassа, а затем вы можете свободно перемещать указатели на базовые и производные classы:

 struct base { /* base class members */ }; struct derived { struct base super; /* derived class members */ }; struct derived d; struct base *base_ptr = (struct base *)&d; // upcast struct derived *derived_ptr = (struct derived *)base_ptr; // downcast 

Чтобы получить polymorphism (т.е. виртуальные функции), вы используете указатели на функции и необязательно функциональные указательные таблицы, также известные как виртуальные таблицы или vtables:

 struct base; struct base_vtable { void (*dance)(struct base *); void (*jump)(struct base *, int how_high); }; struct base { struct base_vtable *vtable; /* base members */ }; void base_dance(struct base *b) { b->vtable->dance(b); } void base_jump(struct base *b, int how_high) { b->vtable->jump(b, how_high); } struct derived1 { struct base super; /* derived1 members */ }; void derived1_dance(struct derived1 *d) { /* implementation of derived1's dance function */ } void derived1_jump(struct derived1 *d, int how_high) { /* implementation of derived 1's jump function */ } /* global vtable for derived1 */ struct base_vtable derived1_vtable = { &derived1_dance, /* you might get a warning here about incompatible pointer types */ &derived1_jump /* you can ignore it, or perform a cast to get rid of it */ }; void derived1_init(struct derived1 *d) { d->super.vtable = &derived1_vtable; /* init base members d->super.foo */ /* init derived1 members d->foo */ } struct derived2 { struct base super; /* derived2 members */ }; void derived2_dance(struct derived2 *d) { /* implementation of derived2's dance function */ } void derived2_jump(struct derived2 *d, int how_high) { /* implementation of derived2's jump function */ } struct base_vtable derived2_vtable = { &derived2_dance, &derived2_jump }; void derived2_init(struct derived2 *d) { d->super.vtable = &derived2_vtable; /* init base members d->super.foo */ /* init derived1 members d->foo */ } int main(void) { /* OK! We're done with our declarations, now we can finally do some polymorphism in C */ struct derived1 d1; derived1_init(&d1); struct derived2 d2; derived2_init(&d2); struct base *b1_ptr = (struct base *)&d1; struct base *b2_ptr = (struct base *)&d2; base_dance(b1_ptr); /* calls derived1_dance */ base_dance(b2_ptr); /* calls derived2_dance */ base_jump(b1_ptr, 42); /* calls derived1_jump */ base_jump(b2_ptr, 42); /* calls derived2_jump */ return 0; } 

И вот как вы делаете polymorphism в C. Это некрасиво, но он выполняет эту работу. Существуют некоторые липкие проблемы, связанные с приведением указателей между базовыми и производными classами, которые являются безопасными до тех пор, пока базовый class является первым членом производного classа. Множественное наследование намного сложнее – в этом случае для случая между базовыми classами, отличными от первого, вам необходимо вручную настроить указатели на основе правильных смещений, что очень сложно и подвержено ошибкам.

Другая (сложная) вещь, которую вы можете сделать, это изменить динамический тип объекта во время выполнения! Вы просто переназначаете его новым указателем vtable. Вы можете даже выборочно изменять некоторые из виртуальных функций, сохраняя других, создавая новые гибридные типы. Просто будьте осторожны, чтобы создать новую vtable вместо изменения глобальной таблицы vtable, иначе вы случайно окажетесь на всех объектах данного типа.

Я когда-то работал с библиотекой C, которая была реализована таким образом, что это показалось мне довольно элегантным. Они написали на языке C способ определения объектов, а затем наследуют их, чтобы они были такими же расширяемыми, как объект C ++. Основная идея заключалась в следующем:

  • У каждого объекта был свой файл
  • Публичные функции и переменные определены в файле .h для объекта
  • Частные переменные и функции были только в файле .c
  • Для «наследования» создается новая структура с первым членом структуры, являющимся объектом, наследующим от

Наследование сложно описать, но в основном это было так:

 struct vehicle { int power; int weight; } 

Затем в другом файле:

 struct van { struct vehicle base; int cubic_size; } 

Тогда у вас может быть фургон, созданный в памяти, и используемый кодом, который знал только о транспортных средствах:

 struct van my_van; struct vehicle *something = &my_van; vehicle_function( something ); 

Он работал красиво, и файлы .h точно определяли, что вы должны делать с каждым объектом.

Рабочий стол GNOME для Linux написан в объектно-ориентированном C и имеет объектную модель под названием « GObject », которая поддерживает свойства, наследование, polymorphism, а также некоторые другие полезные свойства, такие как ссылки, обработка событий (называемые «сигналы»), время выполнения ввод, личные данные и т. д.

Он включает в себя препроцессорные хаки, чтобы делать такие вещи, как typecasting в иерархии classов и т. Д. Вот пример classа, который я написал для GNOME (такие вещи, как gchar typedefs):

Источник

Заголовок заголовка

Внутри структуры GObject есть целое число GType, которое используется как магическое число для динамической системы ввода GLib (вы можете отбросить всю структуру до «GType», чтобы найти ее тип).

Если вы думаете о методах, называемых объектами как статические методы, которые передают неявное « this » в функцию, это может облегчить мышление OO в C.

Например:

 String s = "hi"; System.out.println(s.length()); 

будет выглядеть так:

 string s = "hi"; printf(length(s)); // pass in s, as an implicit this 

Или что-то типа того.

Раньше я делал подобные вещи на C, прежде чем знал, что такое ООП.

Ниже приведен пример, который реализует буфер данных, который растет по требованию, учитывая минимальный размер, прирост и максимальный размер. Эта конкретная реализация была основана на «элементах», то есть она была предназначена для того, чтобы позволить список-подобный набор любого типа C, а не только байтовый буфер переменной длины.

Идея заключается в том, что объект создается с помощью xxx_crt () и удаляется с помощью xxx_dlt (). Каждый из методов «member» использует специально введенный указатель для работы.

Таким образом я реализовал связанный список, циклический буфер и ряд других вещей.

Должен признаться, я никогда не задумывался о том, как реализовать наследование с помощью этого подхода. Я полагаю, что некоторая смесь того, что предлагает Кивели, может быть хорошим путем.

dtb.c:

 #include  #include  #include  static void dtb_xlt(void *dst, const void *src, vint len, const byte *tbl); DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz) { DTABUF *dbp; if(!minsiz) { return NULL; } if(!incsiz) { incsiz=minsiz; } if(!maxsiz || maxsizmaxsiz) { incsiz=maxsiz-minsiz; } if((dbp=(DTABUF*)malloc(sizeof(*dbp))) == NULL) { return NULL; } memset(dbp,0,sizeof(*dbp)); dbp->min=minsiz; dbp->inc=incsiz; dbp->max=maxsiz; dbp->siz=minsiz; dbp->cur=0; if((dbp->dta=(byte*)malloc((vuns)minsiz)) == NULL) { free(dbp); return NULL; } return dbp; } DTABUF *dtb_dlt(DTABUF *dbp) { if(dbp) { free(dbp->dta); free(dbp); } return NULL; } vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen) { if(!dbp) { errno=EINVAL; return -1; } if(dtalen==-1) { dtalen=(vint)strlen((byte*)dtaptr); } if((dbp->cur + dtalen) > dbp->siz) { void *newdta; vint newsiz; if((dbp->siz+dbp->inc)>=(dbp->cur+dtalen)) { newsiz=dbp->siz+dbp->inc; } else { newsiz=dbp->cur+dtalen; } if(newsiz>dbp->max) { errno=ETRUNC; return -1; } if((newdta=realloc(dbp->dta,(vuns)newsiz))==NULL) { return -1; } dbp->dta=newdta; dbp->siz=newsiz; } if(dtalen) { if(xlt256) { dtb_xlt(((byte*)dbp->dta+dbp->cur),dtaptr,dtalen,xlt256); } else { memcpy(((byte*)dbp->dta+dbp->cur),dtaptr,(vuns)dtalen); } dbp->cur+=dtalen; } return 0; } static void dtb_xlt(void *dst,const void *src,vint len,const byte *tbl) { byte *sp,*dp; for(sp=(byte*)src,dp=(byte*)dst; len; len--,sp++,dp++) { *dp=tbl[*sp]; } } vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...) { byte textÝ501¨; va_list ap; vint len; va_start(ap,format); len=sprintf_len(format,ap)-1; va_end(ap); if(len<0 || len>=sizeof(text)) { sprintf_safe(text,sizeof(text),"STRTOOLNG: %s",format); len=(int)strlen(text); } else { va_start(ap,format); vsprintf(text,format,ap); va_end(ap); } return dtb_adddta(dbp,xlt256,text,len); } vint dtb_rmvdta(DTABUF *dbp,vint len) { if(!dbp) { errno=EINVAL; return -1; } if(len > dbp->cur) { len=dbp->cur; } dbp->cur-=len; return 0; } vint dtb_reset(DTABUF *dbp) { if(!dbp) { errno=EINVAL; return -1; } dbp->cur=0; if(dbp->siz > dbp->min) { byte *newdta; if((newdta=(byte*)realloc(dbp->dta,(vuns)dbp->min))==NULL) { free(dbp->dta); dbp->dta=null; dbp->siz=0; return -1; } dbp->dta=newdta; dbp->siz=dbp->min; } return 0; } void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen) { if(!elmlen || (elmidx*elmlen)>=dbp->cur) { return NULL; } return ((byte*)dbp->dta+(elmidx*elmlen)); } 

dtb.h

 typedef _Packed struct { vint min; /* initial size */ vint inc; /* increment size */ vint max; /* maximum size */ vint siz; /* current size */ vint cur; /* current data length */ void *dta; /* data pointer */ } DTABUF; #define dtb_dtaptr(mDBP) (mDBP->dta) #define dtb_dtalen(mDBP) (mDBP->cur) DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz); DTABUF *dtb_dlt(DTABUF *dbp); vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen); vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...); vint dtb_rmvdta(DTABUF *dbp,vint len); vint dtb_reset(DTABUF *dbp); void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen); 

PS: vint был просто typedef int – я использовал его, чтобы напомнить мне, что его длина варьируется от платформы к платформе (для портирования).

Немного не по теме, но исходный компилятор C ++, Cfront , скомпилировал C ++ в C, а затем в ассемблер.

Сохраняется здесь .

ffmpeg (набор инструментов для обработки видео) написан на прямом языке C (и ассемблере), но с использованием объектно-ориентированного стиля. Он полон структур с указателями функций. Существует набор заводских функций, которые инициализируют структуры с помощью соответствующих указателей «метод».

Если вы действительно думаете с умом, даже стандартная библиотека C использует OOP – рассмотрите FILE * в качестве примера: fopen() инициализирует объект FILE * , и вы используете его, используя методы-члены fscanf() , fprintf() , fread() , fwrite() и другие, и в конечном итоге завершить его с помощью fclose() .

Вы также можете пойти с псевдо-Objective-C способом, который тоже не сложно:

 typedef void *Class; typedef struct __class_Foo { Class isa; int ivar; } Foo; typedef struct __meta_Foo { Foo *(*alloc)(void); Foo *(*init)(Foo *self); int (*ivar)(Foo *self); void (*setIvar)(Foo *self); } meta_Foo; meta_Foo *class_Foo; void __meta_Foo_init(void) __attribute__((constructor)); void __meta_Foo_init(void) { class_Foo = malloc(sizeof(meta_Foo)); if (class_Foo) { class_Foo = {__imp_Foo_alloc, __imp_Foo_init, __imp_Foo_ivar, __imp_Foo_setIvar}; } } Foo *__imp_Foo_alloc(void) { Foo *foo = malloc(sizeof(Foo)); if (foo) { memset(foo, 0, sizeof(Foo)); foo->isa = class_Foo; } return foo; } Foo *__imp_Foo_init(Foo *self) { if (self) { self->ivar = 42; } return self; } // ... 

Использовать:

 int main(void) { Foo *foo = (class_Foo->init)((class_Foo->alloc)()); printf("%d\n", (foo->isa->ivar)(foo)); // 42 foo->isa->setIvar(foo, 60); printf("%d\n", (foo->isa->ivar)(foo)); // 60 free(foo); } 

Это то, что может быть получено из некоторого кода Objective-C, подобного этому, если используется довольно старый переводчик Objective-C-to-C:

 @interface Foo : NSObject { int ivar; } - (int)ivar; - (void)setIvar:(int)ivar; @end @implementation Foo - (id)init { if (self = [super init]) { ivar = 42; } return self; } @end int main(void) { Foo *foo = [[Foo alloc] init]; printf("%d\n", [foo ivar]); [foo setIvar:60]; printf("%d\n", [foo ivar]); [foo release]; } 

Я думаю, что то, что написал Адам Розенфилд, является правильным способом делать ООП в C. Я бы хотел добавить, что то, что он показывает, – это реализация объекта. Другими словами, фактическая реализация будет помещена в .c файл, тогда как интерфейс будет помещен в заголовок .h файла. Например, используя пример обезьяны:

Интерфейс будет выглядеть так:

 //monkey.h struct _monkey; typedef struct _monkey monkey; //memory management monkey * monkey_new(); int monkey_delete(monkey *thisobj); //methods void monkey_dance(monkey *thisobj); 

Вы можете видеть в интерфейсе .h файл, который вы определяете только прототипы. Затем вы можете скомпилировать часть реализации « .c file» в статическую или динамическую библиотеку. Это создает инкапсуляцию, а также вы можете изменить реализацию по своему усмотрению. Пользователь вашего объекта не должен знать почти ничего о его реализации. Это также фокусируется на общем дизайне объекта.

Это мое личное убеждение, что oop – это способ концептуализации вашей структуры кода и повторного использования и не имеет ничего общего с теми вещами, которые добавляются в c ++, например, с перегрузкой или шаблонами. Да, это очень приятные полезные функции, но они не являются репрезентативными для объектно-ориентированного программирования.

Моя рекомендация: держите это просто. Одна из самых больших проблем, с которыми я столкнулась, заключается в том, чтобы поддерживать более старое программное обеспечение (иногда более 10 лет). Если код не прост, это может быть сложно. Да, можно написать очень полезный ООП с polymorphismом в С, но его может быть трудно читать.

Я предпочитаю простые объекты, которые инкапсулируют некоторые четко определенные функции. Отличным примером этого является GLIB2 , например хеш-таблица:

 GHastTable* my_hash = g_hash_table_new(g_str_hash, g_str_equal); int size = g_hash_table_size(my_hash); ... g_hash_table_remove(my_hash, some_key); 

Ключи:

  1. Простая архитектура и дизайн
  2. Достигает базовой инкапсуляции ООП.
  3. Легко внедрять, читать, понимать и поддерживать

Если бы я собирался писать ООП в CI, вероятно, пошел бы с псевдо- Pimpl- дизайном. Вместо того, чтобы передавать указатели на структуры, вы в конечном итоге передаете указатели на указатели на структуры. Это делает контент непрозрачным и облегчает polymorphism и наследование.

Реальная проблема с ООП в С – это то, что происходит, когда переменная выходит из области видимости. Нет деструкторов, генерируемых компилятором, и это может вызвать проблемы. Макросы могут помочь, но это всегда будет уродливо смотреть.

 #include "triangle.h" #include "rectangle.h" #include "polygon.h" #include  int main() { Triangle tr1= CTriangle->new(); Rectangle rc1= CRectangle->new(); tr1->width= rc1->width= 3.2; tr1->height= rc1->height= 4.1; CPolygon->printArea((Polygon)tr1); printf("\n"); CPolygon->printArea((Polygon)rc1); } 

Вывод:

 6.56 13.12 

Здесь показано, что такое программирование OO с C.

Это реальный, чистый C, не препроцессор макросов. У нас есть наследование, polymorphism и инкапсуляция данных (включая данные, частные для classов или объектов). Нет никакого шанса для защищенного эквивалента квалификатора, то есть частные данные также закрыты в цепочке преследований. Но это не неудобство, потому что я не думаю, что это необходимо.

CPolygon не CPolygon , потому что мы используем его только для манипулирования объектами сети преследования, которые имеют общие аспекты, но различную реализацию их (polymorphism).

@Adam Rosenfield имеет очень хорошее объяснение того, как достичь ООП с помощью C

Кроме того, я бы рекомендовал вам прочитать

1) pjsip

Очень хорошая библиотека C для VoIP. Вы можете узнать, как он достигает ООП, хотя структуры и таблицы указателей функций

2) Время выполнения iOS

Узнайте, как работает среда выполнения iOS Objective C. Достигает OOP через указатель isa, meta class

Для меня объектная ориентация в C должна иметь следующие особенности:

  1. Инкапсуляция и скрытие данных (может быть достигнуто с помощью структур / непрозрачных указателей)

  2. Наследование и поддержка polymorphismа (одно наследование может быть достигнуто с использованием структур – убедитесь, что абстрактная база не является реальной)

  3. Конструктор и деструктор (нелегко достичь)

  4. Проверка типов (по крайней мере, для пользовательских типов, поскольку C не применяет никаких ограничений)

  5. Подсчет ссылок (или что-то для реализации RAII )

  6. Ограниченная поддержка обработки исключений (setjmp и longjmp)

В дополнение к вышеизложенному, он должен полагаться на спецификации ANSI / ISO и не должен полагаться на специфические для компилятора функции.

Посмотрите на http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Если ничто иное не прочитает документацию, это просветительский опыт.

Я немного опаздываю на вечеринку, но мне нравится избегать обоих крайних макросов – слишком много или слишком запутывает код, но пара очевидных макросов может сделать код OOP более легким для разработки и чтения:

 /* * OOP in C * * gcc -o oop oop.c */ #include  #include  #include  struct obj2d { float x; // object center x float y; // object center y float (* area)(void *); }; #define X(obj) (obj)->b1.x #define Y(obj) (obj)->b1.y #define AREA(obj) (obj)->b1.area(obj) void * _new_obj2d(int size, void * areafn) { struct obj2d * x = calloc(1, size); x->area = areafn; // obj2d constructor code ... return x; } // -------------------------------------------------------- struct rectangle { struct obj2d b1; // base class float width; float height; float rotation; }; #define WIDTH(obj) (obj)->width #define HEIGHT(obj) (obj)->height float rectangle_area(struct rectangle * self) { return self->width * self->height; } #define NEW_rectangle() _new_obj2d(sizeof(struct rectangle), rectangle_area) // -------------------------------------------------------- struct triangle { struct obj2d b1; // deliberately unfinished to test error messages }; #define NEW_triangle() _new_obj2d(sizeof(struct triangle), triangle_area) // -------------------------------------------------------- struct circle { struct obj2d b1; float radius; }; #define RADIUS(obj) (obj)->radius float circle_area(struct circle * self) { return M_PI * self->radius * self->radius; } #define NEW_circle() _new_obj2d(sizeof(struct circle), circle_area) // -------------------------------------------------------- #define NEW(objname) (struct objname *) NEW_##objname() int main(int ac, char * av[]) { struct rectangle * obj1 = NEW(rectangle); struct circle * obj2 = NEW(circle); X(obj1) = 1; Y(obj1) = 1; // your decision as to which of these is clearer, but note above that // macros also hide the fact that a member is in the base class WIDTH(obj1) = 2; obj1->height = 3; printf("obj1 position (%f,%f) area %f\n", X(obj1), Y(obj1), AREA(obj1)); X(obj2) = 10; Y(obj2) = 10; RADIUS(obj2) = 1.5; printf("obj2 position (%f,%f) area %f\n", X(obj2), Y(obj2), AREA(obj2)); // WIDTH(obj2) = 2; // error: struct circle has no member named width // struct triangle * obj3 = NEW(triangle); // error: triangle_area undefined } 

Я думаю, что у этого есть хороший баланс, и ошибки, которые он генерирует (по крайней мере, с вариантами gcc 6.3 по умолчанию), для некоторых более вероятных ошибок полезны, а не путают. Все дело в том, чтобы улучшить производительность программистов нет?

Если вам нужно написать небольшой код, попробуйте это: https://github.com/fulminati/class-framework

 #include "class-framework.h" CLASS (People) { int age; }; int main() { People *p = NEW (People); p->age = 10; printf("%d\n", p->age); } 
  • Скрытие переменных экземпляра classа
  • Когда использовать интерфейсы или абстрактные classы? Когда использовать оба?
  • Эквивалент C ++ экземпляра
  • Являются ли polymorphism, перегрузка и переопределение аналогичных понятий?
  • «Закрытие - это объекты бедного человека и наоборот». Что это значит?
  • Какое определение «интерфейс» в объектно-ориентированном программировании
  • Неужели плохая практика заставит сеттера вернуть «это»?
  • Заводской шаблон в C #: Как обеспечить, чтобы экземпляр объекта мог быть создан только фабричным classом?
  • Монада на простом английском языке? (Для программиста OOP без фона FP)
  • Список всех базовых classов в иерархии данного classа?
  • Что такое абстрактный тип данных в объектно-ориентированном программировании?
  • Давайте будем гением компьютера.