Domanda

Quale sarebbe un insieme di abili hack del preprocessore (compatibile ANSI C89 / ISO C90) che consentono un brutto (ma utilizzabile) orientamento agli oggetti in C?

Conosco alcuni diversi linguaggi orientati agli oggetti, quindi per favore non rispondere con risposte come " Impara C ++! " ;. Ho letto " Programmazione orientata agli oggetti con ANSI C " (attenzione: formato PDF ) e molte altre soluzioni interessanti, ma sono per lo più interessato alle tue :-)!


Vedi anche Puoi scrivere codice orientato agli oggetti in C?

È stato utile?

Soluzione

C Object System (COS) sembra promettente (è ancora in versione alfa). Cerca di mantenere al minimo i concetti disponibili per motivi di semplicità e flessibilità: programmazione uniforme orientata agli oggetti tra cui classi aperte, metaclassi, metaclassi di proprietà, generici, multimetodi, delega, proprietà, eccezioni, contratti e chiusure. C'è un bozza di carta (PDF) che lo descrive.

L'eccezione in C è un'implementazione C89 di TRY-CATCH-FINALLY trovato in altre lingue OO. Viene fornito con una suite di test e alcuni esempi.

Entrambi di Laurent Deniau, che sta lavorando molto su OOP in C .

Altri suggerimenti

Vorrei sconsigliare l'uso del preprocessore (ab) per provare a rendere la sintassi C più simile a quella di un altro linguaggio più orientato agli oggetti. Al livello più elementare, usi semplicemente le strutture semplici come oggetti e le passi attraverso i puntatori:

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

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

Per ottenere cose come ereditarietà e polimorfismo, devi lavorare un po 'di più. È possibile eseguire l'ereditarietà manuale facendo in modo che il primo membro di una struttura sia un'istanza della superclasse, quindi è possibile eseguire il cast dei puntatori alle classi di base e derivate liberamente:

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

Per ottenere il polimorfismo (cioè le funzioni virtuali), si usano i puntatori a funzione e, facoltativamente, le tabelle dei puntatori a funzione, note anche come tabelle virtuali o 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;
}

Ed è così che fai il polimorfismo in C. Non è carino, ma fa il lavoro. Esistono alcuni problemi permanenti che coinvolgono cast di puntatori tra le classi base e derivate, che sono sicuri purché la classe base sia il primo membro della classe derivata. L'ereditarietà multipla è molto più difficile: in tal caso, per eseguire il caso tra classi base diverse dalla prima, è necessario regolare manualmente i puntatori in base agli offset corretti, il che è davvero complicato e soggetto a errori.

Un'altra cosa (complicata) che puoi fare è cambiare il tipo dinamico di un oggetto in fase di esecuzione! È sufficiente riassegnarlo a un nuovo puntatore vtable. Puoi persino modificare selettivamente alcune delle funzioni virtuali mantenendo altre, creando nuovi tipi ibridi. Fai solo attenzione a creare una nuova vtable invece di modificare la vtable globale, altrimenti influenzerai accidentalmente tutti gli oggetti di un determinato tipo.

Una volta ho lavorato con una libreria C che è stata implementata in un modo che mi è sembrato abbastanza elegante. Avevano scritto, in C, un modo per definire oggetti, quindi ereditandoli in modo da renderli estensibili come un oggetto C ++. L'idea di base era questa:

  • Ogni oggetto aveva il suo file
  • Le funzioni e le variabili pubbliche sono definite nel file .h per un oggetto
  • Le variabili e le funzioni private si trovavano solo nel file .c
  • Per " ereditare " viene creata una nuova struttura con il primo membro della struttura come oggetto da cui ereditare

Ereditare è difficile da descrivere, ma fondamentalmente era questo:

struct vehicle {
   int power;
   int weight;
}

Quindi in un altro file:

struct van {
   struct vehicle base;
   int cubic_size;
}

Quindi potresti avere un furgone creato in memoria e utilizzato da un codice che conosceva solo i veicoli:

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

Ha funzionato magnificamente e i file .h hanno definito esattamente cosa dovresti essere in grado di fare con ogni oggetto.

Il desktop GNOME per Linux è scritto in C orientato agli oggetti e ha un modello a oggetti chiamato " GObject " che supporta proprietà, ereditarietà, polimorfismo, nonché alcune altre chicche come riferimenti, gestione degli eventi (chiamati "segnali", digitazione runtime, dati privati, ecc.

Include hack di preprocessore per fare cose come il typecasting nella gerarchia di classi, ecc. Ecco un esempio di classe che ho scritto per GNOME (cose come gchar sono typedefs):

Fonte classe

Header di classe

All'interno della struttura GObject è presente un numero intero GType che viene utilizzato come numero magico per il sistema di tipizzazione dinamica di GLib (è possibile eseguire il cast dell'intera struttura in un "GType" per trovare il tipo).

Se pensi ai metodi chiamati sugli oggetti come metodi statici che passano un implicito " this " nella funzione, può rendere più semplice pensare a OO in C.

Ad esempio:

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

diventa:

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

O qualcosa del genere.

Leggermente fuori tema, ma il compilatore C ++ originale, Cfront , ha compilato da C ++ a C e poi all'assemblatore.

Conservato qui .

Facevo questo genere di cose in C, prima di sapere cosa fosse OOP.

Di seguito è riportato un esempio, che implementa un buffer di dati che cresce su richiesta, data la dimensione minima, l'incremento e la dimensione massima. Questa particolare implementazione era "elemento" basato, vale a dire che è stato progettato per consentire una raccolta simile a un elenco di qualsiasi tipo C, non solo un buffer di byte di lunghezza variabile.

L'idea è che l'oggetto viene istanziato usando xxx_crt () ed eliminato usando xxx_dlt (). Ciascuno dei "membri" metodi utilizza un puntatore specificamente digitato su cui operare.

Ho implementato un elenco collegato, un buffer ciclico e una serie di altre cose in questo modo.

Devo confessare che non ho mai pensato a come attuare l'eredità con questo approccio. Immagino che una miscela di quella offerta da Kieveli potrebbe essere un buon percorso.

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 era semplicemente un typedef di int - l'ho usato per ricordarmi che la sua lunghezza era variabile da piattaforma a piattaforma (per il porting).

ffmpeg (un toolkit per l'elaborazione video) è scritto in C (e linguaggio assembly), ma usando uno stile orientato agli oggetti. È pieno di strutture con puntatori a funzione. Esistono un insieme di funzioni di fabbrica che inizializzano le strutture con l'appropriato "metodo" puntatori.

Se pensi davvero con gratitudine, anche la libreria C standard usa OOP - considera FILE * come esempio: fopen () inizializza un FILE * oggetto e lo usi con i metodi membro fscanf () , fprintf () , fread () , fwrite () e altri, e infine finalizzalo con fclose () .

Puoi anche seguire la via pseudo-Obiettivo-C, che non è altrettanto difficile:

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

Per usare:

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

Questo è ciò che può derivare da un codice Objective-C come questo, se viene usato un traduttore abbastanza vecchio da Objective-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];
}

Penso che ciò che Adam Rosenfield abbia pubblicato sia il modo corretto di fare OOP in C. Vorrei aggiungere che ciò che mostra è l'implementazione dell'oggetto. In altre parole, l'implementazione effettiva verrebbe inserita nel file .c , mentre l'interfaccia verrebbe inserita nell'intestazione .h . Ad esempio, usando l'esempio della scimmia sopra:

L'interfaccia sarebbe simile a:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

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

Puoi vedere nell'interfaccia .h che stai solo definendo prototipi. Puoi quindi compilare la parte di implementazione " File .c " in una libreria statica o dinamica. Questo crea incapsulamento e inoltre è possibile modificare l'implementazione a piacimento. L'utente del tuo oggetto non deve sapere quasi nulla sull'implementazione di esso. Ciò pone anche l'accento sul design complessivo dell'oggetto.

È mia convinzione personale che oop sia un modo di concettualizzare la struttura del codice e la riusabilità e non ha davvero nulla a che fare con quelle altre cose che vengono aggiunte al c ++ come sovraccarico o template. Sì, quelle sono funzioni utili molto carine ma non sono rappresentative di ciò che è veramente la programmazione orientata agli oggetti.

La mia raccomandazione: mantienila semplice. Uno dei maggiori problemi che ho è il mantenimento di software più vecchi (a volte più di 10 anni). Se il codice non è semplice, può essere difficile. Sì, si può scrivere OOP molto utile con polimorfismo in C, ma può essere difficile da leggere.

Preferisco oggetti semplici che racchiudono alcune funzionalità ben definite. Un ottimo esempio di ciò è GLIB2 , ad esempio una tabella hash:

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

Le chiavi sono:

  1. Modello semplice di architettura e design
  2. Ottiene l'incapsulamento OOP di base.
  3. Facile da implementare, leggere, comprendere e mantenere

Se avessi scritto OOP in CI probabilmente avrei scelto uno pseudo- Pimpl design. Invece di passare i puntatori alle strutture, si finisce per passare i puntatori ai puntatori alle strutture. Ciò rende il contenuto opaco e facilita il polimorfismo e l'ereditarietà.

Il vero problema con OOP in C è cosa succede quando le variabili escono dall'ambito. Non esistono distruttori generati dal compilatore e ciò può causare problemi. Macro può eventualmente aiutare, ma sarà sempre brutto da guardare.

Un altro modo di programmare in uno stile orientato agli oggetti con C è usare un generatore di codice che trasforma un linguaggio specifico del dominio in C. Come è fatto con TypeScript e JavaScript per portare OOP su js.

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

Output:

6.56
13.12

Ecco uno spettacolo di ciò che è la programmazione OO con C.

Questo è reale, C puro, senza macro del preprocessore. Abbiamo eredità,     polimorfismo e incapsulamento dei dati (compresi i dati privati ??di classi o oggetti).     Non esiste alcuna possibilità per un equivalente qualificato qualificato, ovvero     i dati privati ??sono anche privati ??lungo la catena dell'ereditarietà.     Ma questo non è un inconveniente perché non credo sia necessario.

CPolygon non è istanziato perché lo usiamo solo per manipolare oggetti     lungo la catena dell'ereditarietà che ha aspetti comuni ma diversi     loro attuazione (polimorfismo).

@Adam Rosenfield ha un'ottima spiegazione su come ottenere OOP con C

Inoltre, ti consiglio di leggere

1) pjsip

Un'ottima libreria C per VoIP. Puoi imparare come raggiunge OOP attraverso le strutture e le tabelle dei puntatori di funzioni

2) Runtime iOS

Scopri come iOS Runtime alimenta l'obiettivo C. Raggiunge OOP tramite puntatore isa, meta classe

Per me l'orientamento agli oggetti in C dovrebbe avere queste caratteristiche:

  1. Incapsulamento e nascondimento dei dati (può essere ottenuto usando strutture / puntatori opachi)

  2. Ereditarietà e supporto per il polimorfismo (l'ereditarietà singola può essere ottenuta usando le strutture - assicurarsi che la base astratta non sia istantanea)

  3. Funzionalità di costruttore e distruttore (non facile da raggiungere)

  4. Verifica del tipo (almeno per i tipi definiti dall'utente in quanto C non ne applica nessuno)

  5. Conteggio dei riferimenti (o qualcosa da implementare RAII )

  6. Supporto limitato per la gestione delle eccezioni (setjmp e longjmp)

Inoltre, dovrebbe fare affidamento sulle specifiche ANSI / ISO e non sulla funzionalità specifica del compilatore.

Guarda http://ldeniau.web.cern.ch/ ldeniau / html / oopc / oopc.html . Se non altro leggere la documentazione è un'esperienza illuminante.

Sono un po 'in ritardo alla festa qui, ma mi piace evitare entrambi macro estremi - troppi o troppi codici offuscati, ma un paio di macro ovvie possono facilitare lo sviluppo del codice OOP e leggi:

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

Penso che questo abbia un buon equilibrio, e gli errori che genera (almeno con le opzioni predefinite di gcc 6.3) per alcuni degli errori più probabili sono utili invece di confondere. Il punto è migliorare la produttività del programmatore no?

Ci sto lavorando anche su una soluzione macro. Quindi è solo per i più coraggiosi, immagino ;-) Ma è già abbastanza bello e sto già lavorando su alcuni progetti. Funziona in modo da definire prima un file di intestazione separato per ogni classe. In questo modo:

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

Per implementare la classe, si crea un file header per essa e un file C dove si implementano i metodi:

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

Nell'intestazione creata per la classe, includi altre intestazioni necessarie e definisci tipi ecc. relativi alla classe. Sia nell'intestazione della classe che nel file C includi il file di specifica della classe (vedi il primo esempio di codice) e una X-macro. Questi X-macro ( 1 , < a href = "https://github.com/plainC/wondermacros/blob/master/wondermacros/objects/x/object_instance.h" rel = "nofollow noreferrer"> 2 , 3 ecc.) espande il codice alle strutture di classe effettive e altro dichiarazioni.

Per ereditare una classe, #define SUPER supername e aggiungere supername__define \ come prima riga nella definizione della classe. Entrambi devono essere lì. C'è anche supporto JSON, segnali, classi astratte, ecc.

Per creare un oggetto, basta usare W_NEW (classname, .x = 1, .y = 2, ...) . L'inizializzazione si basa sull'inizializzazione della struttura introdotta in C11. Funziona bene e tutto ciò che non è elencato è impostato su zero.

Per chiamare un metodo, utilizzare W_CALL (o, method) (1,2,3) . Sembra una chiamata di funzione di ordine superiore ma è solo una macro. Si espande in ((o) - > klass- > method (o, 1,2,3)) che è davvero un bel trucco.

Vedi Documentazione e la codice stesso .

Dato che il framework ha bisogno di un po 'di codice boilerplate, ho scritto uno script Perl (wobject) che fa il lavoro. Se lo usi, puoi semplicemente scrivere

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

e creerà il file di specifica della classe, l'intestazione della classe e un file C, che include Point_impl.c dove implementate la classe. Risparmia parecchio lavoro, se hai molte lezioni semplici ma tutto è ancora in C. wobject è uno scanner basato su espressioni regolari molto semplice, facile da adattare a esigenze specifiche o riscritto da zero.

Il progetto open source Dynace fa esattamente questo. È all'indirizzo https://github.com/blakemcbride/Dynace

Se devi scrivere un piccolo codice prova questo: 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);
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top