Orientamento agli oggetti in C


157

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 alla tua :-)!


Vedi anche Riesci a scrivere codice orientato agli oggetti in C?


1
Posso rispondere per imparare D e usare l'abi c compatibile per dove hai davvero bisogno di C. digitalmars.com/d
Tim Matthews,

2
@Dinah: Grazie per il "Vedi anche". Quel post è stato interessante.

1
La domanda interessante sembra essere il motivo per cui vorresti un hack pre-processore di OOP su C.
Calyth

3
@Calyth: Trovo che OOP sia utile e "Lavoro con alcuni sistemi embedded che hanno solo un compilatore C disponibile" (dall'alto). Inoltre, non trovi interessanti gli eleganti hack del preprocessore?

Risposte:


31

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 documento di bozza (PDF) che lo descrive.

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

Sia da Laurent Deniau, che sta lavorando molto sulla programmazione orientata agli oggetti in C .


@vonbrand COS è migrato su github dove è stato eseguito l'ultimo commit l'estate scorsa. La maturità può spiegare la mancanza di impegno.
filante,

185

Vorrei sconsigliare l'uso del preprocessore (ab) per cercare di rendere la sintassi C più simile a quella di un altro linguaggio più orientato agli oggetti. Al livello più elementare, basta usare le strutture semplici come oggetti e passarle in giro con 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 eredità 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), utilizzare 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.


6
Adam, il divertimento di cambiare la vtable globale di un tipo è simulare la tipizzazione delle anatre in C. :)
jmucchiello,

Ora provo compassione per C ++ ... Beh, certo che la sintassi C ++ è più chiara, ma dal momento che non è una sintassi banale, sono mitigata. Mi chiedo se si possa ottenere qualcosa di ibrido tra C ++ e C, quindi void * sarebbe comunque un tipo valida per il cast. La parte con struct derived {struct base super;};è ovvia a indovinare come funziona, dal momento che l'ordine dei byte è corretto.
jokoon

2
+1 per un codice elegante, ben scritto. Questo e 'esattamente quello che stavo cercando!
Homunculus Reticulli,

3
Molto bene. Questo è esattamente come l'ho fatto ed è anche il modo corretto. Invece di richiedere un puntatore alla struttura / oggetto in mente, dovresti semplicemente passare un puntatore a un intero (indirizzo). Ciò consentirebbe di passare qualsiasi tipo di oggetto per chiamate illimitate al metodo polimorfico. Inoltre, l'unica cosa che manca è una funzione per inizializzare le tue strutture (oggetti / classi). Ciò include una funzione malloc e restituisce un puntatore. Forse aggiungerò un pezzo di come fare il passaggio del messaggio (obiettivo-c) in C.

1
Questa è la goccia che mi ha spezzato di C ++ e di usare C di più (prima che io usassi solo C ++ per ereditarietà) Grazie
Anne Quinn,

31

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.


Mi piace molto questa soluzione, tranne per il fatto che tutti gli interni dell '"oggetto" sono pubblici.
Lawrence Dol,

6
@Software Monkey: C non ha controllo di accesso. L'unico modo per nascondere i dettagli dell'implementazione è interagire attraverso puntatori opachi, che possono diventare piuttosto dolorosi, dal momento che tutti i campi dovrebbero essere accessibili tramite metodi di accesso che probabilmente non possono essere incorporati.
Adam Rosenfield,

1
@Adam: i compilatori che supportano le ottimizzazioni del tempo di collegamento li inseriranno perfettamente ...
Christoph,

9
Se lo fai, dovresti anche assicurarti che tutte le funzioni nel file .c che non sono definite come pubbliche siano definite come statiche in modo da non finire come funzioni nominate nei tuoi file oggetto. Ciò garantisce che nessuno possa trovare i propri nomi nella fase di collegamento.
jmucchiello,

2
@Marcel: C è stato utilizzato perché il codice è stato distribuito su schede di basso livello che eseguono una varietà di processori per sistemi autonomi. Tutti supportavano la compilazione da C ai rispettivi binari nativi. L'approccio ha reso il codice molto facile da leggere una volta che hai capito cosa stavano cercando di fare.
Kieveli,

18

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"), runtime dattilografia, 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 typedef):

Fonte della classe

Intestazione di classe

All'interno della struttura GObject c'è 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).


sfortunatamente, il file Leggimi / Tutorial (link wiki) non funziona e esiste solo un manuale di riferimento (sto parlando di GObject e non di GTK). si prega di fornire alcuni file tutorial per lo stesso ...
FL4SOF

I collegamenti sono stati corretti.
James Cape,

4
I collegamenti vengono nuovamente interrotti.
SeanRamey,

6

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, dati una dimensione minima, un incremento e una dimensione massima. Questa particolare implementazione era basata su "elementi", vale a dire che era progettata 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 metodi "membro" richiede un puntatore tipicamente digitato su cui operare.

Ho implementato un elenco collegato, 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 una buona strada.

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


7
santo molino, questo potrebbe vincere un contest offuscato C! mi piace! :)
Horseyguy,

@horseyguy No, non è stato possibile. È stato pubblicato Inoltre considerano l'inclusione degli abusi dei file di intestazione contro lo strumento iocccsize. Inoltre, non è un programma completo. Il 2009 non ha avuto contest, quindi non posso confrontare lo iocccsize. Anche il CPP è stato abusato molte volte, quindi è piuttosto vecchio. Ecc. Scusa. Non sto cercando di essere negativo, sono comunque realistico. In qualche modo ho capito il tuo significato ed è una buona lettura e l'ho votato. (E sì, vi partecipo e sì, anch'io vinco.)
Pryftan,

6

Leggermente fuori tema, ma il compilatore C ++ originale, Cfront , ha compilato C ++ in C e quindi in assembler.

Conservato qui .


L'ho già visto prima. Credo che sia stato un bel lavoro.

@Anthony Cuozzo: Stan Lippman ha scritto un grande libro intitolato "C ++ - Inside the object model" in cui ha raccontato molte delle sue esperienze e decisioni progettuali nello scrivere e nel mantenere c-front. È ancora una buona lettura e mi ha aiutato immensamente quando sono passato da C a C ++ molti anni fa
zebrabox

5

Se si considerano i metodi chiamati su oggetti come metodi statici che trasmettono un implicito ' this' nella funzione, può rendere più semplice pensare a OO in C.

Per 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 di simile.


6
@Artelius: Certo, ma a volte l'ovvio non lo è, fino a quando non viene dichiarato. +1 per questo.
Lawrence Dol,

1
meglio ancora sarebbestring->length(s);
OozeMeister l'

4

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 i puntatori "metodo" appropriati.


non vedo alcuna funzione di fabbrica in esso (ffmpeg), piuttosto non sembra usare il polimorfismo / ereditarietà (modo banale suggerito sopra).
FL4SOF,

avcodec_open è una funzione di fabbrica. Riempie i puntatori a funzione in una struttura AVCodecContext (come draw_horiz_band). Se osservi l'utilizzo della macro FF_COMMON_FRAME in avcodec.h, vedrai qualcosa di simile all'eredità dei membri dei dati. IMHO, ffmpeg mi dimostra che OOP è meglio farlo in C ++, non in C.
Mr Fooz,

3

Se si pensa davvero catefully, anche di serie l'uso della libreria C OOP - considerare FILE *come un esempio: fopen()inizializza un FILE *oggetto, e lo si utilizza utilizzare metodi membri fscanf(), fprintf(), fread(), fwrite()e altri, e alla fine finalizzare con fclose().

Puoi anche andare con il modo 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;
}
// ...

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

Cosa fa __attribute__((constructor))dentro void __meta_Foo_init(void) __attribute__((constructor))?
AE Drew

1
Questa è un'estensione GCC che farà in modo che la funzione contrassegnata venga chiamata quando il binario viene caricato in memoria. @AEDrew
Maxthon Chan,

popen(3)restituisce anche a FILE *per un altro esempio.
Pryftan,

3

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 .cfile, mentre l'interfaccia verrebbe inserita nel .hfile di intestazione . 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 nel .hfile di interfaccia che stai solo definendo i prototipi. È quindi possibile compilare il " .cfile" della parte di implementazione in una libreria statica o dinamica. Questo crea incapsulamento e puoi anche cambiare 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.


Puoi dichiarare una struttura con typedef struct Monkey {} Monkey; Qual è il punto di scriverla dopo che è stata creata?
MarcusJ,

1
@MarcusJ The struct _monkeyè semplicemente un prototipo. La definizione del tipo effettivo è definita nel file di implementazione (il file .c). Ciò crea l'effetto di incapsulamento e consente allo sviluppatore dell'API di ridefinire la struttura della scimmia in futuro senza modificare l'API. Gli utenti dell'API devono solo preoccuparsi dei metodi effettivi. Il progettista dell'API si occupa dell'implementazione incluso il modo in cui l'oggetto / struttura è disposta. Quindi i dettagli dell'oggetto / struttura sono nascosti all'utente (un tipo opaco).

Definisco le mie strutture nelle intestazioni, non è standard? Bene, lo faccio in questo modo perché ogni tanto ho bisogno di accedere ai membri della struttura al di fuori di quella libreria.
MarcusJ,

1
@MarcusJ Puoi definire le tue strutture nelle intestazioni se lo desideri (non esiste uno standard). Ma se vuoi cambiare la sua struttura interna lungo la strada potresti rompere il tuo codice. L'incapsulamento è semplicemente uno stile di codifica che semplifica la modifica di un'implementazione senza interrompere il codice. Puoi sempre accedere ai tuoi membri tramite metodi di accesso come int getCount(ObjectType obj)etc se scegli di definire la struttura nel file di implementazione.

2

Il mio consiglio: mantienilo 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 incapsulino 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. Raggiunge l'incapsulamento OOP di base.
  3. Facile da implementare, leggere, comprendere e mantenere

1

Se avessi scritto OOP in CI probabilmente avrei optato per un design pseudo- Pimpl . 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. Le macro possono eventualmente aiutare, ma sarà sempre brutto da guardare.


1
Quando programmo in C, mi occupo dell'ambito usando le ifdichiarazioni e rilasciandole alla fine. Ad esempioif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

Un altro modo per programmare in uno stile orientato agli oggetti con C è usare un generatore di codice che trasforma un linguaggio specifico di dominio in C. Come è fatto con TypeScript e JavaScript per portare OOP su 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);
}

Produzione:

6.56
13.12

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

Si tratta di C reale, pura, senza macro del preprocessore. Abbiamo ereditarietà, polimorfismo e incapsulamento dei dati (compresi i dati privati ​​di classi o oggetti). Non vi è alcuna possibilità per un equivalente qualificatore protetto, ovvero i dati privati ​​sono privati ​​anche lungo la catena dell'ereditarietà. Ma questo non è un inconveniente perché non penso sia necessario.

CPolygon non è istanziato perché lo usiamo solo per manipolare oggetti lungo la catena dell'ereditarietà che hanno aspetti comuni ma diversa implementazione di essi (polimorfismo).


0

@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) iOS Runtime

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


0

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

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

  2. Ereditarietà e supporto per il polimorfismo (l'ereditarietà singola può essere ottenuta utilizzando 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 applica alcuno)

  5. Conteggio dei riferimenti (o qualcosa per 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.


Per il numero (5) - Non è possibile implementare RAII in un linguaggio senza distruttori (il che significa che RAII non è una tecnica supportata dal compilatore in C o Java).
Tom,

costruttori e distruttori possono essere scritti per oggetti basati su c - immagino che GObject lo faccia. e naturalmente RAAI (non è semplice, può essere brutto e non è necessario che sia pragmatico) - tutto quello che stavo cercando è identificare la semantica basata su C per ottenere quanto sopra.
FL4SOF,

C non supporta i distruttori. Devi digitare qualcosa per farli funzionare. Ciò significa che non si puliscono da soli. GObject non cambia la lingua.
Tom,

0

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


3
Fornisci il contesto per il link che condividi. Sebbene il link che hai condiviso possa davvero essere molto utile, è consigliabile catturare piuttosto gli aspetti chiave dell'articolo condiviso che rispondono alla domanda. In questo modo, anche se il link viene rimosso, la tua risposta sarà comunque pertinente e utile.
Ismaele Makitla,

0

Sono un po 'in ritardo alla festa qui, ma mi piace evitare entrambi i macro estremi: troppi o troppi codici offuscati, ma un paio di macro ovvie possono semplificare lo sviluppo e la lettura del codice 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
}

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?



0

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. Come questo:

#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, è necessario creare un file di intestazione per esso e un file C in cui implementare 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 i 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. Queste macro X ( 1 , 2 , 3 ecc.) Espanderanno il codice nelle strutture di classe effettive e in altre dichiarazioni.

Per ereditare una classe #define SUPER supernamee 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 a ((o)->klass->method(o,1,2,3))cui è davvero un bel trucco.

Vedere la documentazione e il codice stesso .

Dato che il framework ha bisogno di un po 'di codice boilerplate, ho scritto uno script Perl (oggetto) 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.cdove implementare la classe. Risparmia parecchio lavoro, se hai molte lezioni semplici ma tutto è ancora in C. wobject è uno scanner basato su espressioni regolari molto semplice che è facile da adattare a esigenze specifiche o da riscrivere da zero.



0

Puoi provare COOP , un framework intuitivo per programmatori per OOP in C, con Classi, Eccezioni, Polimorfismo e Gestione della memoria (importante per il codice incorporato). È una sintassi relativamente leggera, vedi il tutorial nel Wiki lì.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.