OO best practice per i programmi C [chiuso]

19

"Se vuoi davvero lo zucchero OO - vai a usare C ++" - è stata la risposta immediata che ho ricevuto da uno dei miei amici quando ho chiesto questo. So che due cose sono completamente sbagliate qui. Il primo OO NON è 'zucchero', e il secondo, il C ++ NON ha assorbito C.

Abbiamo bisogno di scrivere un server in C (il front-end al quale sarà in Python), quindi sto esplorando modi migliori per gestire programmi C di grandi dimensioni.

La modellazione di un sistema di grandi dimensioni in termini di oggetti e le interazioni degli oggetti lo rendono più gestibile, gestibile ed estensibile. Ma quando provi a tradurre questo modello in C che non sopporta oggetti (e tutto il resto), sei sfidato con alcune decisioni importanti.

Crei una libreria personalizzata per fornire le astrazioni OO di cui il tuo sistema ha bisogno? Cose come oggetti, incapsulamento, ereditarietà, polimorfismo, eccezioni, pub / sub (eventi / segnali), namespace, introspezione, ecc. (Ad esempio GObject o COS ).

Oppure, usi solo i costrutti di base C ( struct e funzioni) per approssimare tutte le classi oggetto (e altre astrazioni) in modi ad-hoc. (ad esempio, alcune delle risposte a questa domanda su SO )

Il primo approccio ti offre un modo strutturato per implementare l'intero modello in C. Ma aggiunge anche un livello di complessità che devi mantenere. (Ricorda, la complessità era ciò che volevamo ridurre utilizzando gli oggetti in primo luogo).

Non conosco il secondo approccio e quanto sia efficace approssimare tutte le astrazioni che potresti richiedere.

Quindi, le mie semplici domande sono: Quali sono le migliori pratiche per realizzare un design orientato agli oggetti in C. Ricorda che non sto chiedendo COME farlo. Questo e questa domande ne parlano, e c'è anche un prenota su questo. Quello che mi interessa di più sono alcuni consigli / esempi realistici che affrontano i problemi reali che si presentano quando questo dong.

Nota: per favore non consigliare perché C non dovrebbe essere usato a favore di C ++. Siamo andati oltre quella fase.

    
posta treecoder 07.11.2011 - 09:05
fonte

7 risposte

16

Dalla mia risposta a Come dovrei strutturare progetti complessi in C (non OO ma sulla gestione della complessità in C):

The key is modularity. This is easier to design, implement, compile and maintain.

  • Identify modules in your app, like classes in an OO app.
  • Separate interface and implementation for each module, put in interface only what is needed by other modules. Remember that there is no namespace in C, so you have to make everything in your interfaces unique (e.g., with a prefix).
  • Hide global variables in implementation and use accessor functions for read/write.
  • Don't think in terms of inheritance, but in terms of composition. As a general rule, don't try to mimic C++ in C, this would be very difficult to read and maintain.

Dalla mia risposta a Quali sono le tipiche convenzioni di denominazione per le funzioni pubbliche e private di OO C (penso che questo è una buona pratica):

The convention I use is:

  • Public function (in header file):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
    
  • Private function (static in implementation file)

    static functionname(struct Classname * me, other args...)
    

Inoltre, molti strumenti UML sono in grado di generare codice C da diagrammi UML. Uno open source è Topcased .

    
risposta data 07.11.2011 - 10:13
fonte
16

Penso che sia necessario differenziare OO e C ++ in questa discussione.

L'implementazione di oggetti è possibile in C, ed è piuttosto semplice: basta creare strutture con puntatori di funzione. Questo è il tuo "secondo approccio", e ci andrei. Un'altra opzione sarebbe quella di non utilizzare i puntatori di funzione nella struttura, ma piuttosto passare la struttura dei dati alle funzioni nelle chiamate dirette, come un puntatore "contesto". È meglio, IMHO, in quanto è più leggibile, più facilmente tracciabile e consente di aggiungere funzioni senza modificare la struttura (facilità di ereditarietà se non vengono aggiunti dati). Questo è in effetti il modo in cui C ++ implementa il puntatore this .

Il polimorfismo diventa più complicato perché non esiste un'eredità e un supporto di astrazione incorporati, quindi dovrai includere la struttura genitore nella classe figlia, o fare un sacco di incolla, entrambe le opzioni sono francamente orribili, anche se tecnicamente semplice. Enormi quantità di bug che attendono di succedere.

Le funzioni virtuali possono essere facilmente ottenute attraverso puntatori di funzioni che puntano a funzioni diverse come richiesto, ancora una volta - molto incline al bug quando fatto manualmente, un sacco di lavoro tedioso per inizializzare correttamente quei puntatori.

Per quanto riguarda gli spazi dei nomi, le eccezioni, i modelli, ecc. - Penso che se sei limitato a C - dovresti semplicemente rinunciare a questi. Ho lavorato a scrivere OO in C, non lo farei se avessi una scelta (in quel luogo di lavoro mi sono letteralmente battuto per introdurre il C ++ e i gestori sono stati "sorpresi" da quanto fosse facile integrarlo con il resto dei moduli C alla fine.).

Va da sé che se puoi usare C ++ - usa C ++. Non c'è una vera ragione per non farlo.

    
risposta data 07.11.2011 - 09:33
fonte
8

Ecco le basi su come creare l'orientamento degli oggetti in C

1. Creazione di oggetti ed incapsulamento

Di solito - si crea un oggetto come

object_instance = create_object_typex(parameter);

I metodi possono essere definiti in uno dei due modi qui.

object_type_method_function(object_instance,parameter1)
OR
object_instance->method_function(object_instance_private_data,parameter1)

Si noti che nella maggior parte dei casi, viene restituito object_instance (or object_instance_private_data) di tipo void *. L'applicazione non può fare riferimento a singoli membri o funzioni di questo.

Inoltre, ogni metodo usa queste istanze_oggetto per il metodo successivo.

2. Polimorfismo

Possiamo utilizzare molte funzioni e puntatori di funzioni per sovrascrivere determinate funzionalità in fase di esecuzione.

per esempio - tutti gli object_methods sono definiti come puntatori a funzioni che possono essere estesi a metodi pubblici e privati.

Possiamo anche applicare l'overloading di funzioni in un senso limitato, usando var_args che è molto simile al numero variabile di argomenti definiti in printf. sì, questo non è abbastanza flessibile in C ++ - ma questo è il modo più vicino.

3. Definire l'ereditarietà

Definire l'ereditarietà è un po 'complicato ma si può fare quanto segue con le strutture.

typedef struct { 
     int age,
     int sex,
} person; 

typedef struct { 
     person p,
     enum specialty s;
} doctor;

typedef struct { 
     person p,
     enum subject s;
} engineer;

// use it like
engineer e1 = create_engineer(); 
get_person_age( (person *)e1); 

qui doctor e engineer sono derivati da persona ed è possibile convertirli a livello superiore, ad esempio person .

Il migliore esempio di questo è usato in GObject e oggetti derivati da esso.

4. Creazione di classi virtuali Sto citando un esempio di vita reale da una libreria chiamata libjpeg usata da tutti i browser per la decodifica jpeg. Crea una classe virtuale chiamata error_manager che l'applicazione può creare istanza concreta e restituire -

struct djpeg_dest_struct {
  /* start_output is called after jpeg_start_decompress finishes.
   * The color map will be ready at this time, if one is needed.
   */
  JMETHOD(void, start_output, (j_decompress_ptr cinfo,
                               djpeg_dest_ptr dinfo));
  /* Emit the specified number of pixel rows from the buffer. */
  JMETHOD(void, put_pixel_rows, (j_decompress_ptr cinfo,
                                 djpeg_dest_ptr dinfo,
                                 JDIMENSION rows_supplied));
  /* Finish up at the end of the image. */
  JMETHOD(void, finish_output, (j_decompress_ptr cinfo,
                                djpeg_dest_ptr dinfo));

  /* Target file spec; filled in by djpeg.c after object is created. */
  FILE * output_file;

  /* Output pixel-row buffer.  Created by module init or start_output.
   * Width is cinfo->output_width * cinfo->output_components;
   * height is buffer_height.
   */
  JSAMPARRAY buffer;
  JDIMENSION buffer_height;
};

Si noti qui che JMETHOD si espande in un puntatore a funzione attraverso una macro che deve essere caricata rispettivamente con i giusti metodi.

Ho provato a dire tante cose senza troppe spiegazioni individuali. Ma spero che le persone possano provare le proprie cose. Tuttavia, la mia intenzione è solo quella di mostrare come le cose si mappano.

Inoltre, ci saranno molti argomenti che questo non sarà esattamente vera proprietà dell'equivalente C ++. So che OO in C -non sarà così severo per la sua definizione. Ma lavorare in questo modo comprenderebbe alcuni dei principi fondamentali.

La cosa importante non è che OO sia rigoroso come in C ++ e JAVA. È che si può organizzare strutturalmente il codice pensando a OO e operandolo in questo modo.

Consiglio vivamente alle persone di vedere il vero design di libjpeg e le seguenti risorse

a. Programmazione orientata agli oggetti in C
b. questo è un buon posto dove persone si scambiano idee
c. ed ecco il libro completo

    
risposta data 07.11.2011 - 10:31
fonte
3

L'orientamento all'oggetto si riduce a tre cose:

1) Progettazione di programmi modulari con classi autonome.

2) Protezione dei dati con incapsulamento privato.

3) Ereditarietà / polimorfismo e varie altre utili sintassi come costruttori / distruttori, modelli ecc.

1 è di gran lunga il più importante, ed è anche completamente indipendente dalla lingua, è tutto basato sulla progettazione del programma. In C lo fai creando "moduli di codice" autonomi composti da un file .h e un file .c. Consideralo come l'equivalente di una classe OO. Puoi decidere cosa deve essere inserito in questo modulo attraverso il senso comune, UML o qualsiasi altro metodo di progettazione OO che stai utilizzando per i programmi C ++.

2 è anche molto importante, non solo per proteggere l'accesso intenzionale ai dati privati, ma anche per proteggere da accessi non intenzionali, ad esempio "clasp di spazio dei nomi". C ++ lo fa in modi più eleganti di C, ma può essere comunque ottenuto in C usando la parola chiave static. Tutte le variabili che avresti dichiarato private in una classe C ++, dovrebbero essere dichiarate come statiche in C e collocate nell'ambito del file. Sono accessibili solo dal proprio modulo di codice (classe). Puoi scrivere "setter / getter" proprio come faresti in C ++.

3 è utile ma non necessario. Puoi scrivere programmi OO senza ereditarietà o senza costruttori / distruttori. Queste cose sono belle da avere, possono certamente rendere i programmi più eleganti e forse anche più sicuri (o il contrario se usati con noncuranza). Ma non sono necessari. Dal momento che C non supporta nessuna di queste funzioni utili, dovrai semplicemente farne a meno. I costruttori possono essere sostituiti con le funzioni init / destruct.

L'ereditarietà può essere fatta attraverso vari trucchi di struttura, ma vorrei sconsigliarlo, poiché probabilmente renderà il tuo programma più complesso senza alcun guadagno (l'ereditarietà in generale dovrebbe essere applicata con attenzione, non solo in C ma in qualsiasi lingua) .

Infine, ogni trucco OO può essere fatto nel libro di C. Axel-Tobias Schreiner "Programmazione orientata agli oggetti con ANSI C" dei primi anni '90 lo dimostra. Tuttavia, non consiglierei questo libro a nessuno: aggiunge una complessità spiacevole e strana ai tuoi programmi C che non vale la pena. (Il libro è disponibile gratuitamente qui per chi è ancora interessato nonostante il mio avvertimento.)

Quindi il mio consiglio è di implementare 1) e 2) sopra e saltare il resto. È un modo di scrivere programmi C che ha avuto successo per oltre 20 anni.

    
risposta data 07.11.2011 - 17:09
fonte
2

Prendendo in prestito alcune esperienze dai vari runtime dell'Obiettivo-C, scrivere una capacità OO dinamica e polimorfa in C non è troppo difficile (rendendolo veloce e facile da usare, d'altra parte, sembra essere ancora in corso dopo 25 anni) . Tuttavia, se si implementa una capacità dell'oggetto stile Objective-C senza estendere la sintassi del linguaggio, il codice che si finisce è piuttosto disordinato:

  • ogni classe è definita da una struttura che dichiara la sua superclasse, le interfacce a cui è conforme, i messaggi che implementa (come una mappa di "selettore", il nome del messaggio, a "implementazione", la funzione che fornisce il comportamento) e il layout della variabile di istanza della classe.
  • ogni istanza è definita da una struttura che contiene un puntatore alla sua classe, quindi le sue variabili di istanza.
  • l'invio di messaggi è implementato (dà o prende alcuni casi speciali) usando una funzione che assomiglia a objc_msgSend(object, selector, …) . Sapendo quale classe l'oggetto è un'istanza di, può trovare l'implementazione corrispondente al selettore e quindi eseguire la funzione corretta.

Fa tutto parte di una libreria OO generica progettata per consentire a più sviluppatori di utilizzare ed estendere le rispettive classi, quindi potrebbe essere eccessivo per il tuo progetto. Ho spesso progettato progetti C come progetti "statici" orientati alla classe utilizzando strutture e funzioni:  - ogni classe è la definizione di una struttura C che specifica il layout di ivar  - ogni istanza è solo un'istanza della struttura corrispondente  - gli oggetti non possono essere "messaggeriati", ma sono definite funzioni simili a metodi che assomigliano a MyClass_doSomething(struct MyClass *object, …) . Questo rende le cose più chiare nel codice rispetto all'approccio ObjC, ma ha meno flessibilità.

Dove le bugie del trade-off dipendono dal tuo stesso progetto: suona come se altri programmatori non usassero le tue interfacce C così la scelta ricade sulle preferenze interne. Naturalmente, se si decide di volere qualcosa come la libreria di runtime objc, allora ci saranno librerie di runtime objc multipiattaforma che serviranno.

    
risposta data 07.11.2011 - 10:39
fonte
1

GObject non nasconde alcuna complessità e introduce una sua complessità. Direi che la cosa ad-hoc è più facile di GObject a meno che tu non abbia bisogno delle cose avanzate di GObject come segnali o il macchinario dell'interfaccia.

La cosa è leggermente diversa con COS poiché viene fornita con un preprocessore che estende la sintassi C con alcuni costrutti OO. Esiste un preprocessore simile per GObject, il G Object Builder .

Potresti anche provare il Vala linguaggio di programmazione, che è un linguaggio di alto livello completo che compila in C e in un modo che consente usando le librerie Vala dal codice C semplice. Può utilizzare GObject, il proprio framework di oggetti o il modo ad hoc (con funzionalità limitate).

    
risposta data 07.11.2011 - 09:29
fonte
1

Prima di tutto, a meno che questo non sia compito o non ci sia un compilatore C ++ per il dispositivo di destinazione non penso che tu debba usare C, puoi fornire un'interfaccia C con collegamento C in C ++ abbastanza facilmente.

In secondo luogo, guarderei a quanto ti appoggerai sul polimorfismo e sulle eccezioni e le altre caratteristiche che i framework possono fornire, se non saranno semplici le strutture con funzioni correlate saranno molto più semplici di un framework completo, se un significativo parte del tuo design ha bisogno di loro quindi morde il proiettile e usa un framework in modo da non dover implementare le funzionalità da solo.

Se non hai ancora un design da cui prendere la decisione, fai un picco e guarda cosa ti dice il codice.

infine non deve essere una o l'altra scelta (anche se se si va dal principio alla fine si potrebbe anche tenerlo d'occhio), dovrebbe essere possibile iniziare con semplici strutture per le parti semplici e solo aggiungi librerie in base alle esigenze.

EDIT: Quindi una decisione da parte del management lascia ignorare il primo punto allora.

    
risposta data 07.11.2011 - 09:17
fonte

Leggi altre domande sui tag