Об'єктно-орієнтована в С


157

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

Я знайомий з декількома різними об'єктно-орієнтованими мовами, тому, будь ласка, не відповідайте на відповіді на кшталт "Дізнайтесь C ++!" Я прочитав " Об'єктно-орієнтоване програмування за допомогою ANSI C " (будьте обережні: формат PDF ) та кілька інших цікавих рішень, але мене найбільше цікавлять ваші :-)!


Дивіться також Чи можете ви написати об'єктно-орієнтований код на C?


1
Чи можу я відповісти, щоб дізнатися D та використовувати c-сумісний abi там, де вам справді потрібна C. digitalmars.com/d
Тім Меттьюз

2
@Dinah: Дякую за "Дивіться також". Цей пост був цікавим.

1
Цікавим питанням здається, чому ви хочете, щоб передпроцесорний злом OOP на C.
Calyth

3
@Calyth: Я вважаю, що OOP є корисним, і "я працюю з деякими вбудованими системами, у яких лише дійсно є компілятор C" (зверху). Більше того, чи не знаєте ви прекрасних хакерських препроцесорів цікавих для погляду?

Відповіді:


31

C Object System (COS) звучить багатообіцяюче (воно все ще знаходиться в альфа-версії). Він намагається мінімізувати доступні концепції заради простоти та гнучкості: рівномірне об'єктно-орієнтоване програмування, включаючи відкриті класи, метакласи, метакласи властивостей, генеріки, багатометоди, делегування, право власності, винятки, контракти та закриття. Є проект документа (PDF), який описує його.

Виняток у C - це реалізація C89 TRY-CATCH-FINALLY, знайдена в інших мовах ОО. Він постачається з тестовим набором та деякими прикладами.

І Лоран Deniau, який працює багато на об'єктно - орієнтованому програмуванні в C .


@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 ++ ... Ну, звичайно, синтаксис C ++ зрозуміліший, але оскільки це не тривіальний синтаксис, я пом'якшується. Цікаво, чи вдасться досягти чогось гібридного між C ++ та C, тому пустота * все-таки буде дійсним типовим типом. Частина з struct derived {struct base super;};, очевидно, здогадується, як вона працює, оскільки по порядку байтів це правильно.
jokoon

2
+1 для елегантного коду, добре написаний. Це саме те, що я шукав!
Homunculus Reticulli

3
Молодці. Саме так я це робив, і це правильний шлях. Замість того, щоб вимагати вказівника на struct / object на увазі, вам слід просто передати вказівник на ціле число (адресу). Це дозволить вам передавати будь-який об’єкт для необмежених викликів поліморфних методів. Крім того, єдине, чого не вистачає, - це функція ініціалізації ваших структур (об'єктів / класів). Це включало б функцію malloc та повертало вказівник. Можливо, я додам

1
Це солома зламала мене на C ++, і використовувати C більше (до того, як я використовував лише С ++ для спадщини) Дякую
Енн Квін

31

Я колись працював з бібліотекою С, яка була реалізована таким чином, що вразила мене досить елегантним. Вони написали в 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 файли визначали саме те, що ви повинні мати можливість робити з кожним об’єктом.


Мені дуже подобається це рішення, за винятком того, що всі внутрішні об'єкти є загальнодоступними.
Лоуренс Дол

6
@Software Monkey: C не має контролю доступу. Єдиний спосіб приховати деталі реалізації - це взаємодія через непрозорі вказівники, які можуть отримати досить болісний характер, оскільки до всіх полів потрібно отримати доступ через методи аксесуарів, які, ймовірно, не можуть бути вписані.
Адам Розенфілд

1
@Adam: Компілятори, що підтримують оптимізацію в режимі зв’язку, допоможуть їм просто чудово ...
Крістоф

9
Якщо ви це зробите, ви також повинні переконатися, що всі функції у .c-файлі, які не визначені як загальнодоступні, визначені як статичні, щоб вони не закінчувалися як іменовані функції у ваших об'єктних файлах. Це гарантує, що ніхто не може знайти їхні імена у фазі посилання.
jmucchiello

2
@Marcel: C використовувався тому, що код розгортався на дошках низького рівня, де працюють різні процесори для автономних систем. Усі вони підтримували компіляцію з C у відповідні назви. Підхід зробив код дуже легким для читання, як тільки ви зрозуміли, що вони намагаються зробити.
Kieveli

18

Робочий стіл GNOME для Linux написаний на об’єктно-орієнтованому C, і в ньому є об'єктна модель під назвою " GObject ", яка підтримує властивості, успадкування, поліморфізм, а також деякі інші товари, такі як посилання, обробка подіями (звані "сигнали"), час виконання введення тексту, приватні дані тощо.

Він включає препроцесорні хаки для таких речей, як набір тексту в ієрархії класів тощо. Ось приклад класу, який я написав для GNOME (такі речі, як gchar - це typedefs):

Джерело класу

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

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


на жаль, файл читання мене / підручника (посилання на wiki) не працює, і для цього є лише посібник (я говорю про GObject, а не про GTK). будь ласка, надайте декілька файлів підручника для того ж ...
FL4SOF

Посилання було виправлено.
Джеймс Кейп

4
Посилання знову розриваються.
SeanRamey

6

Раніше я робив подібні речі в С, перш ніж я знав, що таке ООП.

Далі наводиться приклад, який реалізує буфер даних, який зростає на вимогу, з урахуванням мінімального розміру, приросту та максимального розміру. Ця конкретна реалізація була заснована на "елементі", тобто, вона розроблена таким чином, щоб дозволити колекцію подібного до списку будь-якого типу C, а не просто байт-буфер змінної довжини.

Ідея полягає в тому, що об'єкт інстанціюється за допомогою xxx_crt () і видаляється за допомогою xxx_dlt (). Кожен з методів "члена" приймає спеціально набраний покажчик для роботи.

Я реалізував пов'язаний список, циклічний буфер та ряд інших речей таким чином.

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

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

@horseyguy Ні, це не могло. Це було опубліковано. Також вони розглядають можливість включення файлів заголовків до зловживання інструментом iocccsize. Це також не повна програма. 2009 рік не мав жодних змагань, тому не можна порівняти iocccsize. CPP зазнавали зловживань багато разів, тому він досить старий. І т.д. Вибачте. Я не намагаюся бути негативним, наскільки я реалістичний. Я начебто розумію ваше значення, і це добре прочитане, і я його проголосував. (І так, я беру участь у цьому, і так, я теж перемагаю.)
Прифтан

6

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

Збережено тут .


Я насправді це бачив і раніше. Я вважаю, це була приємна робота.

@Anthony Cuozzo: Стен Ліппман написав чудову книгу під назвою "C ++ - Всередині об'єктної моделі", де він написав багато свого досвіду та дизайнерських рішень у написанні та підтримці c-front. Це все ще добре читати і допомогло мені надзвичайно при переході від C до C ++ багато років тому
zebrabox

5

Якщо ви думаєте про методи, що викликаються об'єктами, як статичні методи, які передають неявну ' this' функцію, це може полегшити мислення OO в C.

Наприклад:

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

стає:

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

Або щось подібне.


6
@Artelius: Безумовно, але іноді очевидного немає, поки не буде заявлено. +1 для цього.
Лоуренс Дол

1
ще краще було бstring->length(s);
OozeMeister

4

ffmpeg (інструментарій для обробки відео) пишеться прямою мовою C (і мовою складання), але використовуючи об'єктно-орієнтований стиль. Він сповнений структур з покажчиками функцій. Існує набір заводських функцій, які ініціалізують структури за допомогою відповідних покажчиків «методу».


я не бачу в ньому жодних заводських функцій (ffmpeg), скоріше, схоже, він не використовує поліморфізм / успадкування (тривіальний спосіб, запропонований вище).
FL4SOF

avcodec_open - одна фабрична функція. Він містить функції покажчиків у структуру AVCodecContext (наприклад, draw_horiz_band). Якщо ви подивитесь на використання макросів FF_COMMON_FRAME в avcodec.h, ви побачите щось, що схоже на спадкування членів даних. IMHO, ffmpeg доводить мені, що OOP найкраще робити на C ++, а не на C.
Пан Фооз

3

Якщо ви на справді думаєте catefully, навіть стандартне використання бібліотеки 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-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];
}

Що __attribute__((constructor))робити в void __meta_Foo_init(void) __attribute__((constructor))?
AE Drew

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файл" реалізації у статичну або динамічну бібліотеку. Це створює інкапсуляцію, а також ви можете змінити реалізацію за бажанням. Користувач вашого об’єкта не повинен майже нічого знати про його реалізацію. Це також зосереджує увагу на загальному дизайні об'єкта.

Це моє особисте переконання, що oop - це спосіб концептуалізації вашої структури коду та повторного використання і насправді не має нічого спільного з тими іншими речами, які додаються до c ++, як перевантаження або шаблони. Так, це дуже приємні корисні функції, але вони не є представником того, що насправді є об'єктно-орієнтоване програмування.


Ви можете оголосити структуру за typedef struct Monkey {} Monkey; допомогою пункту "Який сенс вводити її після її створення?"
MarcusJ

1
@MarcusJ Це struct _monkeyпросто прототип. Фактичне визначення типу визначено у файлі реалізації (файл .c). Це створює ефект інкапсуляції і дозволяє розробнику API перевизначати структуру мавпи в майбутньому без зміни API. Користувачам API потрібно потурбуватися лише про фактичні методи. Дизайнер API дбає про реалізацію, включаючи те, як розміщено об'єкт / структуру. Отже, дані об’єкта / структури приховані від користувача (непрозорий тип).

Я визначаю свої структури в заголовках, це не стандарт? Ну, я роблю це так, бо мені періодично потрібно звертатися до членів структури поза цією бібліотекою.
MarcusJ

1
@MarcusJ Ви можете визначити, що ви хочете в заголовках, якщо хочете (стандарт не існує). Але якщо ви хочете змінити внутрішню структуру в дорозі, ви можете порушити свій код. Інкапсуляція - це просто стиль кодування, який полегшує зміну реалізації, не порушуючи код. Ви завжди можете отримати доступ до своїх членів за допомогою методів accessor, таких як int getCount(ObjectType obj)etc., якщо ви вирішите визначити структуру у файлі реалізації.

2

Моя рекомендація: нехай це буде просто. Одне з найбільших проблем, яке я маю - це підтримка старшого програмного забезпечення (іноді старше 10 років). Якщо код не простий, це може бути важко. Так, можна написати дуже корисну ООП з поліморфізмом на С, але її важко читати.

Я віддаю перевагу простим об’єктам, які інкапсулюють певну чітко визначену функціональність. Чудовим прикладом цього є 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. Досягає базової інкапсуляції OOP.
  3. Легко реалізувати, читати, розуміти та підтримувати

1

Якби я збирався писати OOP в CI, певно, пішов би з псевдо- Pimpl дизайном. Замість того, щоб передавати покажчики на структури, ви закінчуєте передавати вказівники на вказівники на структури. Це робить зміст непрозорим і полегшує поліморфізм та успадкування.

Справжня проблема з OOP в C полягає в тому, що відбувається, коли змінні виходять із області застосування. Немає створених компілятором деструкторів, що може спричинити проблеми. Макроси, можливо, можуть допомогти, але на це завжди буде неприємно.


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

1

Ще один спосіб програмування в об'єктно-орієнтованому стилі на C - це використання генератора коду, який перетворює мову, доменну для домену, в C. Як це робиться з TypeScript і JavaScript для доведення OOP до 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

Ось показ того, що таке OO програмування на C.

Це справжній, чистий С, без макросів препроцесора. Ми маємо спадщину, поліморфізм та інкапсуляцію даних (включаючи дані, приватні для класів чи об’єктів). Немає шансів на захищений еквівалент класифікатора, тобто приватні дані також є приватними за ланцюжком спадщини. Але це не незручність, тому що я не вважаю це необхідним.

CPolygon не миттєво, тому що ми використовуємо його лише для маніпулювання об'єктами вниз по ланцюгу безнасінностей, які мають загальні аспекти, але різні їх реалізація (Поліморфізм).


0

@Adam Rosenfield має дуже гарне пояснення, як досягти OOP за допомогою C

Крім того, я б рекомендував вам прочитати

1) pjsip

Дуже хороша бібліотека С для VoIP. Ви можете дізнатися, як він досягає OOP, хоча структури та функції вказівних таблиць

2) Виконання iOS

Дізнайтеся, як повноваження під час роботи iOS. Завдання C. Він досягає OOP через isa pointer, meta class


0

Для мене орієнтація об'єкта в C повинна мати такі особливості:

  1. Інкапсуляція та приховування даних (можна досягти за допомогою структур / непрозорих покажчиків)

  2. Спадкування та підтримка поліморфізму (єдине успадкування можна досягти за допомогою структур - переконайтеся, що абстрактна база не є миттєвою)

  3. Функціонал конструктора та деструктора (досягти не просто)

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

  5. Підрахунок посилань (або щось для впровадження RAII )

  6. Обмежена підтримка поводження з винятками (setjmp та longjmp)

На додаток до вищезазначеного, він повинен покладатися на специфікації ANSI / ISO і не повинен покладатися на особливості компілятора.


Для числа (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:

/*
 * 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);
}

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))чого це справді приємний злом.

Див. Документацію та сам код .

Оскільки фреймворку потрібен якийсь код коробки, я написав сценарій (wobject) Perl, який виконує цю роботу. Якщо ви використовуєте це, ви можете просто написати

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

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



0

Ви можете спробувати COOP , зручну для програмістів рамку для OOP в C, особливості Класи, Винятки, Поліморфізм та Управління пам'яттю (важливо для Вбудованого коду). Це відносно легкий синтаксис, дивіться підручник у Вікі там.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.