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

157

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

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


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

Энтони Куоццо
источник
1
Могу ли я ответить, чтобы выучить D и использовать c-совместимый abi там, где вам действительно нужно C. digitalmars.com/d
Тим Мэтьюз,
2
@Dinah: Спасибо за "Смотрите также". Этот пост был интересным.
1
Интересный вопрос, кажется, почему вы хотите взломать ООП перед процессором на C.
Calyth
3
@Calyth: я считаю, что ООП полезен и «я работаю с некоторыми встроенными системами, в которых действительно доступен только компилятор C» (сверху). Кроме того, разве вы не находите интересные хаки препроцессора интересными для просмотра?
2
Возможные дубликаты Можете ли вы написать объектно-ориентированный код на C?
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Ответы:

31

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

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

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

philant
источник
@vonbrand COS перешел на github, где последний коммит был прошлым летом. Зрелость может объяснить отсутствие коммита.
Филант
185

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

struct monkey
{
    float age;
    bool is_male;
    int happiness;
};

void monkey_dance(struct monkey *monkey)
{
    /* do a little dance */
}

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

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

Чтобы получить полиморфизм (то есть виртуальные функции), вы используете указатели на функции и, необязательно, таблицы указателей на функции, также известные как виртуальные таблицы или 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;
}

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

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

Адам Розенфилд
источник
6
Адам, удовольствие от изменения глобального vtable типа состоит в том, чтобы имитировать типизацию утки в C. :)
jmucchiello
Теперь мне жаль C ++ ... Ну, конечно, синтаксис C ++ более ясен, но, поскольку это не тривиальный синтаксис, я смягчен. Интересно, можно ли достичь чего-то гибридного между C ++ и C, поэтому void * все равно будет допустимым типом castable. Часть с struct derived {struct base super;};, очевидно, угадывает, как это работает, так как по порядку байтов это правильно.
Jokoon
2
+1 за элегантный код, хорошо написано. Это именно то, что я искал!
Гомункул Ретулли
3
Отлично сработано. Это именно то, как я это делал, и это тоже правильный путь. Вместо того, чтобы требовать указатель на структуру / объект, вы должны просто передать указатель на целое число (адрес). Это позволит вам передавать любой тип объекта для неограниченного количества полиморфных вызовов методов. Кроме того, единственное, чего не хватает - это функции для инициализации ваших структур (объектов / классов). Это будет включать функцию malloc и возвращать указатель. Может быть, я добавлю кусок, как сделать передачу сообщений (
1
Это соломинка сломала мне C ++, и использовать C больше (раньше я использовал только C ++ для наследования) Спасибо
Anne Quinn
31

Однажды я работал с библиотекой 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 точно определяли, что вы должны делать с каждым объектом.

Kieveli
источник
Мне действительно нравится это решение, за исключением того, что все внутренние объекты объекта являются публичными.
Лоуренс Дол
6
@Software Monkey: C не имеет контроля доступа. Единственный способ скрыть детали реализации - взаимодействовать через непрозрачные указатели, что может быть довольно болезненным, так как все поля должны быть доступны через методы доступа, которые, вероятно, не могут быть встроены.
Адам Розенфилд
1
@Adam: компиляторы, поддерживающие оптимизацию во время компоновки, прекрасно их встроят ...
Кристоф
9
Если вы сделаете это, вы также должны убедиться, что все функции в файле .c, которые не определены как public, определены как статические, чтобы они не заканчивались как именованные функции в ваших объектных файлах. Это гарантирует, что никто не может найти свои имена в фазе ссылки.
jmucchiello
2
@Marcel: C использовался, потому что код был развернут на низкоуровневых платах с различными процессорами для автономных систем. Все они поддерживали компиляцию из C в соответствующие им двоичные файлы. Подход облегчил чтение кода, когда вы поняли, что они пытаются сделать.
Kieveli
18

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

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

Класс Источник

Заголовок класса

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

Джеймс Кейп
источник
к сожалению, файл read me / tutorial (ссылка на вики) не работает, и для этого есть только справочное руководство (я говорю о GObject, а не GTK). пожалуйста, предоставьте некоторые учебные файлы для того же ...
FL4SOF
Ссылки были исправлены.
Джеймс Кейп
4
Ссылки снова сломаны.
SeanRamey
6

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

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

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

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

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

dtb.c:

#include <limits.h>
#include <string.h>
#include <stdlib.h>

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 || maxsiz<minsiz) { maxsiz=minsiz;        }
    if(minsiz+incsiz>maxsiz)     { 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 - я использовал его, чтобы напомнить мне, что его длина варьируется от платформы к платформе (для портирования).

Лоуренс Дол
источник
7
святой моли, это может выиграть запутанный конкурс C! мне это нравится! :)
скакун
@horseyguy Нет, не мог. Это было опубликовано. Также они считают, что включение заголовочных файлов является неправильным в отношении инструмента iocccsize. Это также не полная программа. В 2009 году не было конкурсов, поэтому я не могу сравнить iocccsize. CPP также подвергался насилию много раз, поэтому он довольно старый. И т.д. Извините. Я не пытаюсь быть отрицательным, но я реалист. Я вроде понимаю ваше значение, и это хорошее чтение, и я проголосовал за него. (И да, я участвую в этом, и да, я тоже выигрываю.)
Pryftan
6

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

Здесь сохранились .

zebrabox
источник
Я на самом деле видел это раньше. Я считаю, что это была хорошая работа.
@Anthony Cuozzo: Стэн Липпман написал отличную книгу под названием «C ++ - Внутри объектной модели», в которой он рассказал о своем опыте и дизайнерских решениях, написав и поддерживая c-front. Это все еще хорошее чтение, и оно мне очень помогло при переходе с C на C ++ много лет назад
zebrabox
5

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

Например:

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

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

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

Или что-то вроде того.

jjnguy
источник
6
@ Артелиус: Конечно, но иногда очевидного нет, пока не заявлено. +1 за это.
Лоуренс Дол
1
еще лучше будетstring->length(s);
OozeMeister
4

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

Мистер Фуз
источник
я не вижу в нем никаких фабричных функций (ffmpeg), скорее он не использует полиморфизм / наследование (тривиальный способ, предложенный выше).
FL4SOF
avcodec_open - это одна заводская функция. Он вставляет указатели функций в структуру AVCodecContext (например, draw_horiz_band). Если вы посмотрите на использование макроса FF_COMMON_FRAME в avcodec.h, вы увидите нечто похожее на наследование элементов данных. ИМХО, ffmpeg доказывает мне, что ООП лучше всего делать на C ++, а не на C.
Мистер Фуз
3

Если вы действительно думаете с любопытством, даже стандартная библиотека C использует ООП - подумайте 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-C-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];
}
Макстон Чан
источник
Что делает __attribute__((constructor))в void __meta_Foo_init(void) __attribute__((constructor))?
А.Е. Дрю
1
Это расширение GCC, которое гарантирует, что отмеченная функция будет вызываться при загрузке двоичного файла в память. @AEDrew
Maxthon Chan
popen(3)также возвращает a FILE *для другого примера.
Прифтан
3

Я думаю, что то, что написал Адам Розенфилд, является правильным способом выполнения ООП в 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 ++, такими как перегрузка или шаблоны. Да, это очень хорошие полезные функции, но они не представляют, что такое объектно-ориентированное программирование.

оборота user2074102
источник
Вы можете объявить структуру с помощью typedef struct Monkey {} Monkey; Что это за определение типа после того, как оно было создано?
MarcusJ
1
@MarcusJ Это struct _monkeyпросто прототип. Фактическое определение типа определено в файле реализации (файл .c). Это создает эффект инкапсуляции и позволяет разработчику API переопределять структуру обезьян в будущем без изменения API. Пользователи API должны интересоваться только фактическими методами. Разработчик API заботится о реализации, в том числе о том, как устроен объект / структура. Таким образом, детали объекта / структуры скрыты от пользователя (непрозрачный тип).
Я определяю свои структуры в заголовках, это не стандарт? Ну, я делаю это так, потому что мне иногда нужно получить доступ к членам структуры за пределами этой библиотеки.
MarcusJ
1
@MarcusJ Вы можете определить свои структуры в заголовках, если вы хотите (нет никакого стандарта). Но если вы хотите изменить его внутреннюю структуру в будущем, вы можете нарушить свой код. Инкапсуляция - это просто стиль кодирования, который облегчает изменение реализации, не нарушая ваш код. Вы всегда можете получить доступ к своим членам через методы доступа, такие как int getCount(ObjectType obj)etc, если вы решите определить структуру в файле реализации.
2

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

Я предпочитаю простые объекты, которые содержат некоторые четко определенные функциональные возможности. Отличным примером этого является 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. Легко реализовать, читать, понимать и поддерживать
Wadester
источник
1

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

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

jmucchiello
источник
1
При программировании на C я имею дело с областью видимости, используя ifоператоры и выпуская их в конце. Напримерif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }
1

Другой способ программирования в объектно-ориентированном стиле на C - это использовать генератор кода, который преобразует язык, специфичный для предметной области, в C. Как это делается с помощью TypeScript и JavaScript, чтобы перенести ООП в js.

Люка Винерт
источник
0
#include "triangle.h"
#include "rectangle.h"
#include "polygon.h"

#include <stdio.h>

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

Вот пример того, что такое ОО-программирование на C.

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

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

rogergc
источник
0

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

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

1) pjsip

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

2) iOS Runtime

Узнайте, как среда выполнения iOS поддерживает Objective C. Он достигает ООП с помощью указателя isa, мета-класса.

onmyway133
источник
0

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

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

  2. Наследование и поддержка полиморфизма (одиночное наследование может быть достигнуто с помощью структур - убедитесь, что абстрактная база не является экземпляром)

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

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

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

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

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

FL4SOF
источник
Для числа (5) - Вы не можете реализовать RAII на языке без деструкторов (что означает, что RAII не поддерживается техникой на C или Java).
Том
Конструкторы и деструкторы могут быть написаны для объекта на основе c - я думаю, GObject делает это. и, конечно же, RAAI (это не прямолинейно, может быть некрасиво и вовсе не должно быть прагматичным) - все, что я искал, - это определить семантику на основе Си для достижения вышеизложенного.
FL4SOF
C не поддерживает деструкторы. Вы должны напечатать что-то , чтобы заставить их работать. Это означает, что они не убирают себя. GObject не меняет язык.
Том
0

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

Тим
источник
3
Пожалуйста, предоставьте контекст для ссылки, которой вы делитесь. Хотя ссылка, которой вы поделились, действительно может быть очень полезной, желательно, чтобы в ней были отражены ключевые аспекты общей статьи, которые отвечают на вопрос. Таким образом, даже если ссылка будет удалена, ваш ответ останется актуальным и полезным.
ishmaelMakitla
0

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

/*
 * OOP in C
 *
 * gcc -o oop oop.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

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 по умолчанию) для некоторых из более вероятных ошибок, полезны вместо того, чтобы путать. Весь смысл в повышении производительности труда программиста нет?

дуанев
источник
0

Если вам нужно написать немного кода, попробуйте это: 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);
}
оборота cicciodarkast
источник
2
Пожалуйста, не размещайте какой-либо инструмент или библиотеку в качестве ответа. По крайней мере, продемонстрируйте, как это решает проблему в самом ответе.
Baum mit Augen
0

Я также работаю над этим на основе макро решения. Так что это только для самых смелых, я думаю ;-) Но это уже довольно приятно, и я уже работаю над несколькими проектами в дополнение к этому. Это работает так, что вы сначала определяете отдельный заголовочный файл для каждого класса. Как это:

#define CLASS Point
#define BUILD_JSON

#define Point__define                            \
    METHOD(Point,public,int,move_up,(int steps)) \
    METHOD(Point,public,void,draw)               \
                                                 \
    VAR(read,int,x,JSON(json_int))               \
    VAR(read,int,y,JSON(json_int))               \

Чтобы реализовать класс, вы создаете для него заголовочный файл и файл C, в котором вы реализуете методы:

METHOD(Point,public,void,draw)
{
    printf("point at %d,%d\n", self->x, self->y);
}

В заголовок, который вы создали для класса, вы включаете другие нужные вам заголовки и определяете типы и т. Д., Относящиеся к классу. И в заголовке класса, и в файле C вы включаете файл спецификации класса (см. Первый пример кода) и X-макрос. Эти X-макросы ( 1 , 2 , 3 и т. Д.) Расширяют код до фактических структур классов и других объявлений.

Унаследовать класс, #define SUPER supername и добавить supername__define \в качестве первой строки определение класса. Оба должны быть там. Также есть поддержка JSON, сигналы, абстрактные классы и т. Д.

Чтобы создать объект, просто используйте W_NEW(classname, .x=1, .y=2,...). Инициализация основана на инициализации структуры, представленной в C11. Это работает хорошо, и все, что не перечислено, установлено в ноль.

Чтобы вызвать метод, используйте W_CALL(o,method)(1,2,3) . Это похоже на вызов функции более высокого порядка, но это просто макрос. Это расширяется до того, ((o)->klass->method(o,1,2,3))что действительно хороший взломать.

Смотрите документацию и код .

Поскольку фреймворку нужен некоторый шаблонный код, я написал Perl-скрипт (wobject), который выполняет эту работу. Если вы используете это, вы можете просто написать

class Point
    public int move_up(int steps)
    public void draw()
    read int x
    read int y

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

J.P.
источник
0

Вы можете попробовать COOP , дружественную к программисту среду для ООП в C, которая включает классы, исключения, полиморфизм и управление памятью (важно для встроенного кода). Это относительно легкий синтаксис, см. Учебник в Wiki там.

Шмуэль Файн
источник