Come si implementa una classe in C? [chiuso]


139

Supponendo che devo usare C (no C ++ o compilatori orientati agli oggetti) e non ho allocazione dinamica della memoria, quali sono alcune tecniche che posso usare per implementare una classe o una buona approssimazione di una classe? È sempre una buona idea isolare la "classe" in un file separato? Supponiamo che possiamo preallocare la memoria assumendo un numero fisso di istanze o persino definendo il riferimento a ciascun oggetto come costante prima del tempo di compilazione. Sentiti libero di fare ipotesi su quale concetto di OOP dovrò implementare (varierà) e suggerisci il metodo migliore per ciascuno.

restrizioni:

  • Devo usare C e non un OOP perché sto scrivendo il codice per un sistema incorporato e il compilatore e la base di codici preesistenti sono in C.
  • Non esiste allocazione dinamica della memoria perché non abbiamo memoria sufficiente per presumere ragionevolmente che non finiremo se iniziamo ad allocarla dinamicamente.
  • I compilatori con cui lavoriamo non hanno problemi con i puntatori a funzione

26
Domanda obbligatoria: devi scrivere un codice orientato agli oggetti? Se lo fai per qualsiasi motivo, va bene, ma dovrai combattere una battaglia piuttosto in salita. Probabilmente è il massimo se eviti di provare a scrivere codice orientato agli oggetti in C. È certamente possibile - vedi l'eccellente risposta di unfind - ma non è esattamente "facile", e se stai lavorando su un sistema incorporato con memoria limitata, potrebbe non essere fattibile. Potrei sbagliarmi, però: non sto cercando di discuterne, ma solo presentare alcuni contrappunti che potrebbero non essere stati presentati.
Chris Lutz,

1
A rigor di termini, non dobbiamo. Tuttavia, la complessità del sistema ha reso il codice non mantenibile. Ritengo che il modo migliore per ridurre la complessità sia implementare alcuni concetti OOP. Grazie per tutti coloro che hanno risposto entro 3 minuti. Siete pazzi e veloci!
Ben Gartner,

8
Questa è solo la mia modesta opinione, ma OOP non rende il codice immediatamente gestibile. Potrebbe semplificare la gestione, ma non necessariamente più gestibile. Puoi avere "spazi dei nomi" in C (Apache Portable Runtime antepone tutti i simboli dei globi con apr_e GLib li prefigura g_per creare uno spazio dei nomi) e altri fattori organizzativi senza OOP. Se intendi comunque ristrutturare l'app, prenderei in considerazione la possibilità di dedicare del tempo a trovare una struttura procedurale più gestibile.
Chris Lutz,

questo è stato discusso all'infinito prima - hai guardato una delle risposte precedenti?
Larry Watanabe,

Questa fonte, che era in una mia risposta cancellata, potrebbe anche essere di aiuto: planetpdf.com/codecuts/pdfs/ooc.pdf Descrive un approccio completo per fare OO in C.
Ruben Steins

Risposte:


86

Dipende dall'esatto set di funzionalità "orientato agli oggetti" che si desidera avere. Se hai bisogno di cose come il sovraccarico e / o metodi virtuali, probabilmente devi includere i puntatori di funzione nelle strutture:

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

Ciò ti consentirebbe di implementare una classe "ereditando" la classe base e implementando una funzione adatta:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

Questo ovviamente richiede anche l'implementazione di un costruttore, che assicuri che il puntatore a funzione sia impostato correttamente. Normalmente alloceresti dinamicamente la memoria per l'istanza, ma puoi anche lasciare che sia il chiamante a farlo:

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

Se vuoi diversi costruttori diversi, dovrai "decorare" i nomi delle funzioni, non puoi avere più di una rectangle_new()funzione:

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

Ecco un esempio di base che mostra l'utilizzo:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

Spero che questo ti dia qualche idea, almeno. Per un framework orientato agli oggetti ricco e di successo in C, guarda nella libreria GObject di glib .

Si noti inoltre che non esiste una "classe" esplicita modellata sopra, ogni oggetto ha i propri puntatori di metodo che sono un po 'più flessibili di quanto si possa trovare in C ++. Inoltre, costa memoria. Puoi evitarlo inserendo i puntatori del metodo in una classstruttura e inventando un modo per ogni istanza di oggetto di fare riferimento a una classe.


Non avendo dovuto provare a scrivere C orientato agli oggetti, di solito è meglio creare funzioni che prendano const ShapeClass *o const void *come argomenti? Sembrerebbe che quest'ultimo potrebbe essere un po 'più gentile in eredità, ma posso vedere gli argomenti in entrambi i modi ...
Chris Lutz,

1
@ Chris: Sì, questa è una domanda difficile. : | GTK + (che utilizza GObject) utilizza la classe corretta, ovvero RectangleClass *. Ciò significa che spesso devi eseguire i cast, ma forniscono macro utili per aiutarti, quindi puoi sempre lanciare BASECLASS * p su SUBCLASS * usando solo SUBCLASS (p).
Rilassati il

1
Il mio compilatore non riesce sulla seconda riga di codice: float (*computeArea)(const ShapeClass *shape);dicendo che ShapeClassè un tipo sconosciuto.
Daniel,

@DanielSank che è dovuto alla mancanza di dichiarazioni forward richieste da 'typedef struct` (non mostrato nell'esempio fornito). Poiché i structriferimenti stessi, richiede una dichiarazione prima di essere definita. Questo è spiegato con un esempio qui nella risposta di Lundin . La modifica dell'esempio per includere la dichiarazione in avanti dovrebbe risolvere il problema; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker,

Cosa succede quando Rectangle ha una funzione che non tutte le forme hanno. Ad esempio, get_corners (). Un cerchio non implementerebbe questo, ma un rettangolo potrebbe. Come si accede a una funzione che non fa parte della classe genitore da cui è stata ereditata?
Otus,

24

Ho dovuto farlo anche una volta per i compiti. Ho seguito questo approccio:

  1. Definire i membri dei dati in una struttura.
  2. Definisci i membri della tua funzione che prendono un puntatore alla tua struttura come primo argomento.
  3. Fai questi in un colpo di testa e uno c. Intestazione per la definizione della struttura e le dichiarazioni di funzione, c per le implementazioni.

Un semplice esempio sarebbe questo:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 

Questo è ciò che ho fatto in passato, ma con l'aggiunta di un finto ambito inserendo i prototipi di funzione nel file .c o .h secondo necessità (come ho già detto nella mia risposta).
Taylor Leese,

Mi piace questo, la dichiarazione struct alloca tutta la memoria. Per qualche motivo ho dimenticato che avrebbe funzionato bene.
Ben Gartner,

Penso che tu abbia bisogno di un typedef struct Queue Queue;dentro.
Craig McQueen,

3
O semplicemente digitare code {/ * members * /} Coda;
Brooks Moses,

#Craig: grazie per il promemoria, aggiornato.
erelender,

12

Se si desidera solo una classe, utilizzare una matrice di structs come dati "oggetti" e passare loro i puntatori alle funzioni "membro". È possibile utilizzare typedef struct _whatever Whateverprima di dichiarare struct _whateverdi nascondere l'implementazione dal codice client. Non c'è differenza tra un tale "oggetto" e l' FILEoggetto libreria standard C.

Se si desidera più di una classe con ereditarietà e funzioni virtuali, è comune disporre di puntatori alle funzioni come membri della struttura o di un puntatore condiviso a una tabella di funzioni virtuali. La libreria GObject utilizza sia questo che il trucco typedef ed è ampiamente utilizzata.

C'è anche un libro sulle tecniche per questo disponibili on-line - Object Oriented Programming con ANSI C .


1
Freddo! Altre raccomandazioni per libri su OOP in C? O qualsiasi altra tecnica di progettazione moderna in C? (o sistemi embedded?)
Ben Gartner,

7

puoi dare un'occhiata a GOBject. è una libreria del sistema operativo che ti dà un modo dettagliato per fare un oggetto.

http://library.gnome.org/devel/gobject/stable/


1
Molto interessato. Qualcuno sa sulla licenza? Per i miei scopi sul lavoro, trascinare una libreria open source in un progetto probabilmente non funzionerà dal punto di vista legale.
Ben Gartner,

GTK +, e tutte le librerie che fanno parte di quel progetto (incluso GObject), sono concesse in licenza sotto GNU LGPL, il che significa che è possibile collegarsi ad esse da software proprietario. Tuttavia, non so se sarà fattibile per il lavoro integrato.
Chris Lutz,

7

Interfacce e implementazioni C: tecniche per la creazione di software riutilizzabile , David R. Hanson

http://www.informit.com/store/product.aspx?isbn=0201498413

Questo libro fa un ottimo lavoro nel rispondere alla tua domanda. È nella serie di calcolo professionale Addison Wesley.

Il paradigma di base è qualcosa del genere:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);

5

Darò un semplice esempio di come OOP dovrebbe essere fatto in C. Mi rendo conto che questa testata è del 2009, ma vorrei aggiungerla comunque.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

Il concetto di base prevede che la "classe ereditaria" sia posta in cima alla struttura. In questo modo, l'accesso ai primi 4 byte nella struttura accede anche ai primi 4 byte nella "classe ereditata" (Assumendo ottimizzazioni non pazze). Ora, quando il puntatore della struttura viene lanciato sulla "classe ereditata", la "classe ereditata" può accedere ai "valori ereditati" nello stesso modo in cui accederà ai suoi membri normalmente.

Questa e alcune convenzioni di denominazione per i costruttori, i distruttori, le funzioni di allocazione e deallocarion (raccomando init, clean, new, free) ti aiuteranno molto.

Per quanto riguarda le funzioni virtuali, utilizzare i puntatori a funzione nella struttura, possibilmente con Class_func (...); anche involucro. Per quanto riguarda i modelli (semplici), aggiungi un parametro size_t per determinare la dimensione, richiedere un puntatore void * o richiedere un tipo 'class' con solo la funzionalità che ti interessa. (es. int GetUUID (Object * self); GetUUID (& p);)


Disclaimer: tutto il codice scritto sullo smartphone. Aggiungi i controlli di errore dove necessario. Verifica la presenza di bug.
yyny,

4

Utilizzare a structper simulare i membri di dati di una classe. In termini di ambito del metodo è possibile simulare metodi privati ​​inserendo i prototipi di funzioni private nel file .c e le funzioni pubbliche nel file .h.


4
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};

3

Nel tuo caso la buona approssimazione della classe potrebbe essere un ADT . Ma non sarà più lo stesso.


1
Qualcuno può fornire una breve differenza tra un tipo di dati astratto e una classe? Ho sempre considerato i due concetti strettamente collegati.
Ben Gartner,

Sono infatti strettamente correlati. Una classe può essere vista come un'implementazione di un ADT, poiché (presumibilmente) potrebbe essere sostituita da un'altra implementazione che soddisfi la stessa interfaccia. Penso che sia difficile dare una differenza esatta, poiché i concetti non sono chiaramente definiti.
Jørgen Fogh,

3

La mia strategia è:

  • Definire tutto il codice per la classe in un file separato
  • Definire tutte le interfacce per la classe in un file di intestazione separato
  • Tutte le funzioni membro prendono un "ClassHandle" che sta per il nome dell'istanza (invece di o.foo (), call foo (oHandle)
  • Il costruttore viene sostituito con una funzione void ClassInit (ClassHandle h, int x, int y, ...) OR ClassHandle ClassInit (int x, int y, ...) a seconda della strategia di allocazione della memoria
  • Tutte le variabili membro vengono archiviate come membro di una struttura statica nel file di classe, incapsulandolo nel file, impedendo ai file esterni di accedervi
  • Gli oggetti sono memorizzati in una matrice della struttura statica sopra, con handle predefiniti (visibili nell'interfaccia) o un limite fisso di oggetti che possono essere istanziati
  • Se utile, la classe può contenere funzioni pubbliche che attraverseranno l'array e chiameranno le funzioni di tutti gli oggetti istanziati (RunAll () chiama ogni Run (oHandle)
  • Una funzione Deinit (ClassHandle h) libera la memoria allocata (indice di array) nella strategia di allocazione dinamica

Qualcuno vede problemi, buchi, potenziali insidie ​​o vantaggi / svantaggi nascosti a entrambe le varianti di questo approccio? Se sto reinventando un metodo di progettazione (e presumo che debba essere), puoi indicarmi il nome?


Per motivi di stile, se si dispone di informazioni da aggiungere alla domanda, è necessario modificare la domanda per includere tali informazioni.
Chris Lutz,

Sembra che tu abbia spostato da malloc l'allocazione dinamica da un grande heap a ClassInit () selezionando in modo dinamico da un pool di dimensioni fisse, piuttosto che fare effettivamente qualsiasi cosa accadrà quando chiedi un altro oggetto e non hai le risorse per fornirne uno .
Pete Kirkham,

Sì, il carico di gestione della memoria viene spostato sul codice che chiama ClassInit () per verificare che l'handle restituito sia valido. Essenzialmente abbiamo creato il nostro heap dedicato per la classe. Non sono sicuro di vedere un modo per evitarlo se vogliamo fare qualsiasi allocazione dinamica, a meno che non abbiamo implementato un heap di uso generale. Preferirei isolare il rischio ereditario nell'heap in una classe.
Ben Gartner,

3

Vedi anche questa risposta e questa

È possibile. Sembra sempre una buona idea al momento, ma in seguito diventa un incubo per la manutenzione. Il tuo codice si riempie di pezzi di codice che legano tutto insieme. Un nuovo programmatore avrà molti problemi a leggere e comprendere il codice se si utilizzano i puntatori a funzione poiché non sarà ovvio quali funzioni vengano chiamate.

Nascondere i dati con le funzioni get / set è facile da implementare in C ma fermarsi qui. Ho visto più tentativi in ​​questo ambito nell'ambiente incorporato e alla fine è sempre un problema di manutenzione.

Dal momento che tutti voi avete problemi di manutenzione, vorrei evitare.


2

Il mio approccio sarebbe quello di spostare le structe tutte le funzioni associate principalmente in un file di origine separato in modo che possa essere usato "portabilmente".

A seconda del tuo compilatore, potresti essere in grado di includere funzioni in struct, ma questa è un'estensione molto specifica per il compilatore e non ha nulla a che fare con l'ultima versione dello standard che ho usato abitualmente :)


2
I puntatori a funzione sono tutti buoni. Tendiamo a usarli per sostituire le istruzioni switch di grandi dimensioni con una tabella di ricerca.
Ben Gartner,

2

Il primo compilatore c ++ era in realtà un preprocessore che traduceva il codice C ++ in C.

Quindi è molto possibile avere classi in C. Potresti provare a scavare un vecchio preprocessore C ++ e vedere che tipo di soluzioni crea.


Quello sarebbe cfront; si sono verificati problemi quando sono state aggiunte eccezioni al C ++ - la gestione delle eccezioni non è banale.
Jonathan Leffler,

2

GTK è interamente costruito su C e utilizza molti concetti OOP. Ho letto il codice sorgente di GTK ed è piuttosto impressionante e sicuramente più facile da leggere. Il concetto di base è che ogni "classe" è semplicemente una struttura e funzioni statiche associate. Tutte le funzioni statiche accettano la struttura "istanza" come parametro, fanno tutto ciò che serve e restituiscono i risultati se necessario. Ad esempio, potresti avere una funzione "GetPosition (CircleStruct obj)". La funzione scava semplicemente attraverso la struttura, estrae i numeri di posizione, probabilmente costruisce un nuovo oggetto PositionStruct, inserisce la xey nel nuovo PositionStruct e lo restituisce. GTK implementa anche l'ereditarietà in questo modo incorporando le strutture all'interno delle strutture. abbastanza intelligente.


1

Vuoi metodi virtuali?

In caso contrario, è sufficiente definire un set di puntatori a funzione nella struttura stessa. Se assegni tutti i puntatori a funzioni C standard, sarai in grado di chiamare funzioni da C in sintassi molto simili a come faresti in C ++.

Se vuoi avere metodi virtuali, diventa più complicato. Fondamentalmente sarà necessario implementare il proprio VTable per ogni struttura e assegnare i puntatori di funzione al VTable a seconda della funzione chiamata. Avresti quindi bisogno di un set di puntatori a funzione nella struttura stessa che a sua volta chiamano il puntatore a funzione nel VTable. Questo è essenzialmente ciò che fa C ++.

TBH però ... se vuoi quest'ultimo, probabilmente stai meglio trovando un compilatore C ++ che puoi usare e ricompilare il progetto. Non ho mai capito l'ossessione per il C ++ di non essere utilizzabile in embedded. L'ho usato molte volte e funziona velocemente e non ha problemi di memoria. Sicuramente devi stare un po 'più attento a quello che fai ma non è poi così complicato.


L'ho già detto e lo dirò di nuovo, ma lo dirò di nuovo: non hai bisogno di puntatori di funzione o la possibilità di chiamare funzioni da strutture in stile C ++ per creare OOP in C, OOP riguarda principalmente l'ereditarietà di funzionalità e variabili (contenuto) entrambi i quali possono essere raggiunti in C senza puntatori a funzioni o codice duplicato.
yyny

0

C non è un linguaggio OOP, come giustamente fai notare, quindi non esiste un modo integrato per scrivere una vera classe. La cosa migliore da fare è guardare le strutture e i puntatori di funzioni , che ti permetteranno di costruire un'approssimazione di una classe. Tuttavia, poiché C è procedurale, potresti prendere in considerazione la possibilità di scrivere più codice simile a C (ovvero senza provare a utilizzare le classi).

Inoltre, se puoi usare C, puoi probabilmente usare C ++ e ottenere classi.


4
Non voglio sottovalutare, ma FYI, puntatori di funzione o la capacità di chiamare funzioni da strutture (che suppongo sia la tua intenzione) non ha nulla a che fare con OOP. OOP riguarda principalmente l'ereditarietà di funzionalità e variabili, entrambe ottenibili in C senza puntatori o duplicazioni di funzioni.
yyny
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.