Orientacja obiektowa w C


157

Jaki byłby zestaw sprytnych hacków preprocesora (kompatybilnych z ANSI C89 / ISO C90), które umożliwiają jakąś brzydką (ale użyteczną) orientację obiektową w C?

Znam kilka różnych języków zorientowanych obiektowo, więc nie odpowiadaj na pytania typu „Naucz się C ++!”. Przeczytałem " Programowanie obiektowe z ANSI C " (uwaga: format PDF ) i kilka innych ciekawych rozwiązań, ale najbardziej interesuje mnie twoje :-)!


Zobacz także Czy potrafisz pisać kod zorientowany obiektowo w C?


1
Czy mogę odpowiedzieć, aby nauczyć się D i używać abi kompatybilnego z c tam, gdzie naprawdę potrzebujesz C. digitalmars.com/d
Tim Matthews

2
@Dinah: Dziękuję za "Zobacz także". Ten post był interesujący.

1
Wydaje się, że interesującym pytaniem jest, dlaczego chcesz
zhakować

3
@Calyth: Uważam, że OOP jest przydatne i "Pracuję z niektórymi systemami wbudowanymi, które tak naprawdę mają dostępny tylko kompilator C" (z góry). Co więcej, czy nie uważasz, że sprytne hacki preprocesorów są interesujące do obejrzenia?

Odpowiedzi:


31

C Object System (COS) brzmi obiecująco (nadal jest w wersji alfa). Stara się ograniczyć do minimum dostępne koncepcje ze względu na prostotę i elastyczność: jednolite programowanie obiektowe, w tym klasy otwarte, metaklasy, metaklasy właściwości, rodzaje generyczne, multimetody, delegowanie, własność, wyjątki, kontrakty i zamknięcia. Istnieje szkic dokumentu (PDF), który to opisuje.

Wyjątkiem w C jest implementacja C89 TRY-CATCH-FINALLY znaleziona w innych językach OO. Zawiera zestaw testowy i kilka przykładów.

Zarówno Laurent Deniau, który pracuje dużo na OOP w C .


@vonbrand COS migrował do github, gdzie ostatnie zatwierdzenie miało miejsce zeszłego lata. Dojrzałość może wyjaśniać brak zaangażowania.
filant

185

Odradzałbym używanie preprocesora (ab) do prób nadawania składni C bardziej podobnej do innego, bardziej obiektowego języka. Na najbardziej podstawowym poziomie po prostu używasz zwykłych struktur jako obiektów i przekazujesz je za pomocą wskaźników:

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

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

Aby uzyskać takie rzeczy, jak dziedziczenie i polimorfizm, musisz trochę bardziej popracować. Możesz zrobić ręczne dziedziczenie, mając pierwszy element struktury jako instancję nadklasy, a następnie możesz swobodnie rzutować wskaźniki na klasy bazowe i pochodne:

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

Aby uzyskać polimorfizm (tj. Funkcje wirtualne), użyj wskaźników funkcji i opcjonalnie tabel wskaźników funkcji, znanych również jako tabele wirtualne lub 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;
}

I tak robi się polimorfizm w C. Nie jest ładny, ale spełnia swoje zadanie. Występują pewne przyklejone problemy dotyczące rzutowania wskaźnika między klasami podstawowymi i pochodnymi, które są bezpieczne, o ile klasa bazowa jest pierwszym składnikiem klasy pochodnej. Dziedziczenie wielokrotne jest znacznie trudniejsze - w takim przypadku, aby dokonać wielkości liter między klasami bazowymi innymi niż pierwsza, musisz ręcznie dostosować wskaźniki w oparciu o odpowiednie przesunięcia, co jest naprawdę trudne i podatne na błędy.

Inną (trudną) rzeczą, którą możesz zrobić, jest zmiana typu dynamicznego obiektu w czasie wykonywania! Po prostu przypisujesz mu nowy wskaźnik vtable. Możesz nawet selektywnie zmieniać niektóre funkcje wirtualne, zachowując inne, tworząc nowe typy hybrydowe. Po prostu uważaj, aby utworzyć nową tabelę vtable zamiast modyfikować globalną tabelę vtable, w przeciwnym razie przypadkowo wpłyniesz na wszystkie obiekty danego typu.


6
Adam, zabawą ze zmiany globalnej tabeli typu vtable jest symulacja pisania kaczego w C. :)
jmucchiello

Szkoda C ++ ... Cóż, oczywiście składnia C ++ jest jaśniejsza, ale ponieważ nie jest to trywialna składnia, jestem złagodzony. Zastanawiam się, czy można by osiągnąć coś hybrydowego między C ++ i C, więc void * nadal byłby prawidłowym typem rzutowalnym. Część z struct derived {struct base super;};jest oczywistym do odgadnięcia, jak to działa, ponieważ według kolejności bajtów jest poprawne.
jokoon

2
+1 za elegancki kod, dobrze napisany. To jest dokładnie to, czego szukałem!
Homunculus Reticulli

3
Dobra robota. Dokładnie tak to robiłem i to też jest właściwy sposób. Zamiast wymagać wskaźnika do struktury / obiektu, powinieneś po prostu przekazać wskaźnik do liczby całkowitej (adresu). Umożliwiłoby to przekazanie dowolnego rodzaju obiektu dla nieograniczonej liczby wywołań metod polimorficznych. Brakuje tylko funkcji do inicjalizacji twoich struktur (obiektów / klas). Obejmuje to funkcję malloc i zwraca wskaźnik. Może dodam fragment, jak zrobić przekazywanie wiadomości (cel-c) w C.

1
To jest słomka, która złamała mi C ++ i używanie C więcej (zanim użyłem C ++ tylko do dziedziczenia) Dziękuję
Anne Quinn

31

Kiedyś pracowałem z biblioteką C, która została zaimplementowana w sposób, który wydał mi się dość elegancki. Napisali w C sposób definiowania obiektów, a następnie dziedziczenia po nich, dzięki czemu były tak samo rozszerzalne jak obiekt w C ++. Podstawowy pomysł był taki:

  • Każdy obiekt miał swój własny plik
  • Funkcje i zmienne publiczne są zdefiniowane w pliku .h obiektu
  • Prywatne zmienne i funkcje znajdowały się tylko w pliku .c
  • Aby „dziedziczyć”, tworzona jest nowa struktura, z pierwszym składnikiem struktury będącym obiektem dziedziczenia

Dziedziczenie jest trudne do opisania, ale zasadniczo wyglądało to tak:

struct vehicle {
   int power;
   int weight;
}

Następnie w innym pliku:

struct van {
   struct vehicle base;
   int cubic_size;
}

Wtedy mógłbyś mieć furgonetkę utworzoną w pamięci i używaną przez kod, który wiedział tylko o pojazdach:

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

Działało pięknie, a pliki .h dokładnie definiowały, co powinieneś być w stanie zrobić z każdym obiektem.


Bardzo podoba mi się to rozwiązanie, poza tym, że wszystkie elementy wewnętrzne „obiektu” są publiczne.
Lawrence Dol,

6
@Software Monkey: C nie ma kontroli dostępu. Jedynym sposobem na ukrycie szczegółów implementacji jest interakcja za pomocą nieprzezroczystych wskaźników, co może być dość bolesne, ponieważ dostęp do wszystkich pól musiałby być możliwy za pomocą metod dostępu, których prawdopodobnie nie można wstawić.
Adam Rosenfield

1
@Adam: Kompilatory obsługujące optymalizacje czasu łącza wbudują je dobrze ...
Christoph,

9
Jeśli to zrobisz, powinieneś również upewnić się, że wszystkie funkcje w pliku .c, które nie są zdefiniowane jako publiczne, są zdefiniowane jako statyczne, aby nie były nazwane funkcjami w plikach obiektowych. Gwarantuje to, że nikt nie może znaleźć swoich nazwisk w fazie łącza.
jmucchiello

2
@Marcel: C został użyty, ponieważ kod został wdrożony na płytach niskopoziomowych z różnymi procesorami dla systemów autonomicznych. Wszystkie one wspierały kompilację z C do swoich natywnych plików binarnych. Takie podejście sprawiło, że kod był bardzo łatwy do odczytania, gdy zdałeś sobie sprawę, co próbują zrobić.
Kieveli

18

Pulpit GNOME dla Linuksa jest napisany w zorientowanym obiektowo C i ma model obiektowy o nazwie „ GObject ”, który obsługuje właściwości, dziedziczenie, polimorfizm, a także inne dodatki, takie jak referencje, obsługa zdarzeń (zwanych „sygnałami”), środowisko wykonawcze pisanie, prywatne dane itp.

Zawiera hacki preprocesora do wykonywania rzeczy, takich jak rzutowanie typów w hierarchii klas, itp. Oto przykładowa klasa, którą napisałem dla GNOME (rzeczy takie jak gchar są typedefami):

Źródło klasy

Nagłówek klasy

Wewnątrz struktury GObject znajduje się liczba całkowita GType, która jest używana jako magiczna liczba dla systemu dynamicznego pisania GLib (możesz rzutować całą strukturę na „GType”, aby znaleźć jej typ).


niestety plik read me / tutorial (link do wiki) nie działa i jest do tego tylko podręcznik referencyjny (mówię o GObject, a nie o GTK). proszę dostarczyć pliki z samouczkami dla tego samego ...
FL4SOF

Linki zostały naprawione.
James Cape

4
Linki są znowu zerwane.
SeanRamey

6

Robiłem tego typu rzeczy w C, zanim wiedziałem, co to jest OOP.

Poniżej znajduje się przykład, który implementuje bufor danych, który rośnie na żądanie, biorąc pod uwagę minimalny rozmiar, przyrost i maksymalny rozmiar. Ta konkretna implementacja była oparta na "elemencie", co oznacza, że ​​została zaprojektowana tak, aby umożliwić zbiór podobny do listy dowolnego typu C, a nie tylko bufor bajtowy o zmiennej długości.

Chodzi o to, że obiekt jest tworzony za pomocą xxx_crt () i usuwany za pomocą xxx_dlt (). Każda z metod „składowych” wymaga do działania określonego typu wskaźnika.

Zaimplementowałem w ten sposób listę połączoną, bufor cykliczny i wiele innych rzeczy.

Muszę przyznać, że nigdy nie zastanawiałem się, jak wdrożyć dziedziczenie przy takim podejściu. Wyobrażam sobie, że połączenie tego, co oferuje Kieveli, może być dobrą drogą.

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 był po prostu typem int - użyłem go, aby przypomnieć mi, że jego długość była zmienna w zależności od platformy (w celu przeniesienia).


7
święty moly, to może wygrać zawody w zaciemnionym konkursie! lubię to! :)
horseyguy

@horseyguy Nie, nie udało się. Został opublikowany. Rozważają również włączenie nadużyć plików nagłówkowych względem narzędzia iocccsize. To także nie jest kompletny program. Rok 2009 nie miał konkurencji, więc nie mogę porównać iocccsize. CPP był wielokrotnie nadużywany, więc jest dość stary. Itd. Przepraszam. Nie próbuję być negatywny, jestem jednak realistą. W pewnym sensie rozumiem twoje znaczenie i jest to dobra lektura i zagłosowałem za nią. (I tak, biorę w nim udział i tak, też wygrywam.)
Pryftan

6

Nieco nie na temat, ale oryginalny kompilator C ++, Cfront , skompilował C ++ do C, a następnie do assemblera.

Tutaj zachowane .


Właściwie już to widziałem. Uważam, że to była niezła robota.

@Anthony Cuozzo: Stan Lippman napisał świetną książkę zatytułowaną „C ++ - Inside the object model”, w której opisał wiele swoich doświadczeń i decyzji projektowych, pisząc i utrzymując c-front. To wciąż dobra lektura i bardzo mi pomogła podczas przechodzenia z C na C ++ wiele lat temu
zebrabox

5

Jeśli myślisz o metodach wywoływanych na obiektach jako o metodach statycznych, które przekazują niejawny 'this ' do funkcji, może to ułatwić myślenie OO w C.

Na przykład:

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

staje się:

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

Czy jakoś tak.


6
@Artelius: Jasne, ale czasami oczywiste nie jest, dopóki nie zostanie powiedziane. +1 za to.
Lawrence Dol,

1
jeszcze lepiej byłobystring->length(s);
OozeMeister

4

ffmpeg (zestaw narzędzi do przetwarzania wideo) jest napisany w prostym C (i języku asemblera), ale przy użyciu stylu obiektowego. Jest pełen struktur ze wskaźnikami funkcji. Istnieje zestaw funkcji fabrycznych, które inicjalizują struktury za pomocą odpowiednich wskaźników „metod”.


nie widzę w nim żadnych funkcji fabrycznych (ffmpeg), raczej nie wydaje się, aby używał polimorfizmu / dziedziczenia (trywialny sposób sugerowany powyżej).
FL4SOF

avcodec_open to jedna funkcja fabryczna. Umieszcza wskaźniki funkcji w strukturze AVCodecContext (np. Draw_horiz_band). Jeśli spojrzysz na użycie makra FF_COMMON_FRAME w avcodec.h, zobaczysz coś podobnego do dziedziczenia członków danych. IMHO, ffmpeg udowadnia mi, że OOP najlepiej wykonywać w C ++, a nie w C.
Mr Fooz

3

Jeśli naprawdę myśli catefully, nawet standardowe użycie biblioteki C OOP - rozważyć FILE *jako przykład: fopen()inicjuje FILE *obiekt i użyć go użyć metod członków fscanf(), fprintf(), fread(), fwrite()i innych, a ostatecznie sfinalizować ją fclose().

Możesz także pójść drogą pseudo-celu-C, która również nie jest trudna:

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;
}
// ...

Używać:

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

Oto, co może wynikać z takiego kodu Objective-C, jak ten, jeśli używany jest dość stary translator 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];
}

Co robi __attribute__((constructor))w void __meta_Foo_init(void) __attribute__((constructor))?
AE Drew

1
To jest rozszerzenie GCC, które zapewni, że zaznaczona funkcja zostanie wywołana, gdy plik binarny zostanie załadowany do pamięci. @AEDrew
Maxthon Chan,

popen(3)zwraca również a FILE *dla innego przykładu.
Pryftan

3

Myślę, że to, co opublikował Adam Rosenfield, to właściwy sposób robienia OOP w C. Chciałbym dodać, że to, co pokazuje, to implementacja obiektu. Innymi słowy, rzeczywista implementacja zostanie umieszczona w pliku.c pliku, a interfejs w nagłówku.h pliku . Na przykład, używając powyższego przykładu małpy:

Interfejs wyglądałby następująco:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

    //memory management
    monkey * monkey_new();
    int monkey_delete(monkey *thisobj);
    //methods
    void monkey_dance(monkey *thisobj);

W .hpliku interfejsu widać, że definiujesz tylko prototypy. Następnie możesz skompilować część dotyczącą implementacji ".c plik” do biblioteki statycznej lub dynamicznej. Tworzy to hermetyzację, a także możesz dowolnie zmieniać implementację. Użytkownik twojego obiektu nie musi prawie nic wiedzieć o jego implementacji. To również kładzie nacisk na ogólny projekt obiektu.

Osobiście uważam, że oop jest sposobem na konceptualizację struktury kodu i możliwości ponownego wykorzystania i nie ma tak naprawdę nic wspólnego z innymi elementami dodanymi do c ++, takimi jak przeciążanie lub szablony. Tak, są to bardzo przydatne, przydatne funkcje, ale nie są reprezentatywne dla tego, czym naprawdę jest programowanie obiektowe.


Możesz zadeklarować strukturę za pomocą typedef struct Monkey {} Monkey; Jaki jest cel wpisywania jej po utworzeniu?
MarcusJ

1
@MarcusJ The struct _monkeyto po prostu prototyp. Rzeczywista definicja typu jest zdefiniowana w pliku implementacji (plik .c). Tworzy to efekt hermetyzacji i umożliwia programiście interfejsu API ponowne zdefiniowanie struktury małpy w przyszłości bez modyfikowania interfejsu API. Użytkownicy interfejsu API muszą zajmować się tylko rzeczywistymi metodami. Projektant API dba o implementację, w tym sposób ułożenia obiektu / struktury. Zatem szczegóły obiektu / struktury są ukryte przed użytkownikiem (typ nieprzezroczysty).

Definiuję moje struktury w nagłówkach, czy to nie jest standard? Cóż, robię to w ten sposób, ponieważ czasami potrzebuję uzyskać dostęp do elementów struktury spoza tej biblioteki.
Marcus J

1
@MarcusJ Jeśli chcesz, możesz zdefiniować swoje struktury w nagłówkach (nie ma standardu). Ale jeśli chcesz zmienić jego wewnętrzną strukturę w przyszłości, możesz złamać swój kod. Hermetyzacja to po prostu styl kodowania, który ułatwia zmianę implementacji bez uszkadzania kodu. Zawsze możesz uzyskać dostęp do swoich członków za pomocą metod dostępu, takich jak int getCount(ObjectType obj)itp., Jeśli zdecydujesz się zdefiniować strukturę w pliku implementacji.

2

Moja rada: nie komplikuj. Jednym z największych problemów, jakie mam, jest konserwacja starszego oprogramowania (czasami starszego niż 10 lat). Jeśli kod nie jest prosty, może być trudny. Tak, można napisać bardzo przydatne OOP z polimorfizmem w C, ale może być trudne do odczytania.

Wolę proste obiekty, które zawierają pewne dobrze zdefiniowane funkcje. Świetnym tego przykładem jest GLIB2 , na przykład tablica mieszająca:

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);

Klucze to:

  1. Prosta architektura i wzorzec projektowy
  2. Osiąga podstawową enkapsulację OOP.
  3. Łatwy do wdrożenia, odczytania, zrozumienia i utrzymania

1

Gdybym miał pisać OOP w CI, prawdopodobnie poszedłbym z pseudo-Pimplem projektem . Zamiast przekazywać wskaźniki do struktur, w końcu przekazujesz wskaźniki do wskaźników do struktur. To sprawia, że ​​treść jest nieprzejrzysta i ułatwia polimorfizm i dziedziczenie.

Prawdziwy problem z OOP w C polega na tym, co się dzieje, gdy zmienne wychodzą z zakresu. Nie ma destruktorów generowanych przez kompilator, co może powodować problemy. Makra mogą pomóc, ale zawsze będzie brzydko na nie patrzeć.


1
Podczas programowania w C radzę sobie z zakresem używając ifinstrukcji i zwalniając je na końcu. Na przykładif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

Innym sposobem programowania w stylu obiektowym w C jest użycie generatora kodu, który przekształca język specyficzny dla domeny na C. Tak jak w przypadku TypeScript i JavaScript w celu przeniesienia OOP do 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);
}

Wynik:

6.56
13.12

Oto pokaz tego, czym jest programowanie obiektowe w C.

To jest prawdziwe, czyste C, bez makr preprocesora. Mamy dziedziczenie, polimorfizm i enkapsulację danych (w tym dane prywatne dla klas lub obiektów). Nie ma szans na chroniony odpowiednik kwalifikatora, co oznacza, że ​​prywatne dane są również prywatne w łańcuchu dziedziczenia. Ale to nie jest niedogodność, ponieważ uważam, że nie jest to konieczne.

CPolygon nie jest tworzony, ponieważ używamy go tylko do manipulowania obiektami w dół łańcucha dziedziczenia, które mają wspólne aspekty, ale różne ich implementacje (polimorfizm).


0

@Adam Rosenfield ma bardzo dobre wyjaśnienie, jak osiągnąć OOP w C

Poza tym polecam lekturę

1) pjsip

Bardzo dobra biblioteka C dla VoIP. Możesz dowiedzieć się, jak osiąga OOP, korzystając ze struktur i tabel wskaźników funkcji

2) Środowisko wykonawcze iOS

Dowiedz się, w jaki sposób środowisko wykonawcze iOS napędza Cel C. Osiąga OOP za pomocą wskaźnika isa, meta klasy


0

Dla mnie orientacja obiektowa w C powinna mieć następujące cechy:

  1. Hermetyzacja i ukrywanie danych (można osiągnąć za pomocą struktur / nieprzezroczystych wskaźników)

  2. Dziedziczenie i obsługa polimorfizmu (pojedyncze dziedziczenie można osiągnąć za pomocą struktur - upewnij się, że abstrakcyjna podstawa nie jest instancyjna)

  3. Funkcje konstruktora i destruktora (niełatwe do osiągnięcia)

  4. Sprawdzanie typów (przynajmniej dla typów zdefiniowanych przez użytkownika, ponieważ C nie wymusza żadnego)

  5. Liczenie referencji (lub coś do zaimplementowania RAII )

  6. Ograniczona obsługa wyjątków (setjmp i longjmp)

Ponadto powinien opierać się na specyfikacjach ANSI / ISO i nie powinien polegać na funkcjonalności specyficznej dla kompilatora.


Dla numeru (5) - Nie można zaimplementować RAII w języku bez destruktorów (co oznacza, że ​​RAII nie jest techniką obsługiwaną przez kompilator w C lub Javie).
Tom

konstruktory i destruktory można napisać dla obiektu opartego na c - myślę, że robi to GObject. i oczywiście RAAI (nie jest to proste, może być brzydkie i wcale nie musi być pragmatyczne) - wszystko, czego szukałem, to zidentyfikowanie semantyki opartej na C, aby osiągnąć powyższe.
FL4SOF

C nie obsługuje destruktorów. Musisz coś wpisać , żeby działały. Oznacza to, że się nie sprzątają. GObject nie zmienia języka.
Tom

0

Spójrz na http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Jeśli nic innego, przeczytanie dokumentacji jest pouczającym doświadczeniem.


3
Podaj kontekst udostępnianego łącza. Chociaż udostępniony przez Ciebie link może być bardzo pomocny, wskazane jest raczej uchwycenie kluczowych aspektów udostępnionego artykułu, które stanowią odpowiedź na pytanie. W ten sposób, nawet jeśli link zostanie usunięty, Twoja odpowiedź będzie nadal trafna i pomocna.
ishmaelMakitla

0

Trochę spóźniłem się na imprezę, ale lubię unikać obu makro ekstremów - zbyt wiele lub zbyt wiele zaciemnia kod, ale kilka oczywistych makr może ułatwić tworzenie i czytanie kodu 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
}

Myślę, że ma to dobrą równowagę, a błędy, które generuje (przynajmniej przy domyślnych opcjach gcc 6.3) dla niektórych bardziej prawdopodobnych błędów, są pomocne, a nie mylące. Chodzi o to, aby poprawić produktywność programistów, nie?



0

Pracuję również nad tym w oparciu o rozwiązanie makro. To chyba tylko dla odważnych ;-) Ale już jest całkiem fajnie, a na dodatek pracuję już nad kilkoma projektami. Działa tak, że najpierw definiujesz oddzielny plik nagłówkowy dla każdej klasy. Lubię to:

#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))               \

Aby zaimplementować klasę, tworzysz dla niej plik nagłówkowy i plik C, w którym implementujesz metody:

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

W nagłówku, który utworzyłeś dla klasy, dołączasz inne potrzebne nagłówki i definiujesz typy itp. Związane z klasą. Zarówno w nagłówku klasy, jak iw pliku C dołączasz plik specyfikacji klasy (zobacz pierwszy przykład kodu) i X-makro. Te X-makra ( 1 , 2 , 3 itd.) Rozszerzą kod do rzeczywistych struktur klas i innych deklaracji.

Aby odziedziczyć klasę #define SUPER supernamei dodaćsupername__define \ jako pierwszy wiersz w definicji klasy. Obie muszą tam być. Istnieje również obsługa JSON, sygnały, klasy abstrakcyjne itp.

Aby stworzyć obiekt, po prostu użyj W_NEW(classname, .x=1, .y=2,...). Inicjalizacja jest oparta na inicjalizacji struktury wprowadzonej w C11. Działa ładnie i wszystko, czego nie ma na liście, jest ustawione na zero.

Aby wywołać metodę, użyj W_CALL(o,method)(1,2,3). Wygląda jak wywołanie funkcji wyższego rzędu, ale jest to tylko makro. Rozszerza się, ((o)->klass->method(o,1,2,3))co jest naprawdę fajnym hackem.

Zobacz dokumentację i sam kod .

Ponieważ framework potrzebuje trochę standardowego kodu, napisałem skrypt Perla (wobject), który wykonuje to zadanie. Jeśli tego używasz, możesz po prostu pisać

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

i utworzy plik specyfikacji klasy, nagłówek klasy i plik C, który zawiera miejsce, w Point_impl.cktórym zaimplementujesz klasę. Oszczędza sporo pracy, jeśli masz wiele prostych klas, ale nadal wszystko jest w C. wobject jest bardzo prostym skanerem opartym na wyrażeniach regularnych, który można łatwo dostosować do określonych potrzeb lub przepisać od nowa.



0

Możesz wypróbować COOP , przyjazny dla programistów framework do OOP w C, zawiera klasy, wyjątki, polimorfizm i zarządzanie pamięcią (ważne dla kodu osadzonego). Jest to stosunkowo lekka składnia, zobacz samouczek na Wiki .

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.