Best practice OO per i programmi C [chiuso]


19

"Se vuoi davvero 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, in secondo luogo, il C ++ NON ha assorbito C.

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

Modellare un grande sistema in termini di oggetti e interazioni tra oggetti lo rende più gestibile, gestibile ed estensibile. Ma quando provi a tradurre questo modello in C che non porta oggetti (e tutto il resto), sei sfidato con alcune decisioni importanti.

Crei una libreria personalizzata per fornire le astrazioni OO necessarie al tuo sistema? Cose come oggetti, incapsulamento, eredità, polimorfismo, eccezioni, pub / sub (eventi / segnali), spazi dei nomi, introspezione, ecc. (Ad esempio GObject o COS ).

Oppure, basta usare i costrutti C di base ( structe le funzioni) per approssimare tutte le classi di oggetti (e altre astrazioni) in modi ad hoc. (ad esempio, alcune delle risposte a questa domanda su SO )

Il primo approccio offre un modo strutturato per implementare l'intero modello in C. Ma aggiunge anche un livello di complessità che è necessario mantenere. (Ricorda, la complessità era ciò che volevamo ridurre usando 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. Questa e questa domanda ne parlano, e c'è anche un libro su questo. Quello che mi interessa di più sono alcuni consigli / esempi realistici che affrontano i problemi reali che si presentano quando si dong.

Nota: per favore non consigliare perché C non dovrebbe essere usato a favore di C ++. Siamo passati ben oltre quel palcoscenico.


3
Puoi scrivere un server C ++ in modo che la sua interfaccia esterna sia extern "C"e possa essere usata da Python. Puoi farlo manualmente o puoi farti aiutare da SWIG . Quindi il desiderio di frontend Python non è un motivo per non usare C ++. Ciò non significa che non ci siano validi motivi per voler stare con C.
Jan Hudec,

1
Questa domanda necessita di chiarimenti. Attualmente, i paragrafi 4 e 5 chiedono sostanzialmente quale approccio si dovrebbe adottare, ma poi si dice che "non si chiede come farlo" e invece si desidera (un elenco?) Delle migliori pratiche. Se non stai cercando COME farlo in C, stai chiedendo un elenco di "migliori pratiche" relative all'OOP in generale? In tal caso, dillo, ma tieni presente che la domanda sarà probabilmente chiusa per essere soggettiva .
Caleb,

:) Sto chiedendo esempi concreti (codice o altro) in cui è stato fatto - e i problemi che hanno riscontrato durante lo svolgimento.
treecoder

4
Le tue esigenze sembrano confuse. Insisti sull'utilizzo dell'orientamento agli oggetti per nessuna ragione che riesco a vedere (in alcune lingue, è un modo per rendere i programmi più gestibili, ma non in C), e insisti nell'utilizzare C. L'orientamento agli oggetti è un mezzo, non un fine o una panacea . Inoltre, è molto avvantaggiato dal supporto linguistico. Se davvero volevi OO, dovresti averlo considerato durante la selezione della lingua. Una domanda su come realizzare un grande sistema software con C avrebbe molto più senso.
David Thornley,

Potresti dare un'occhiata a "Modellazione e progettazione orientate agli oggetti". (Rumbaugh et al.): C'è una sezione sulla mappatura dei progetti OO su linguaggi come C.
Giorgio,

Risposte:


16

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

La chiave è la modularità. È più facile progettare, implementare, compilare e gestire.

  • Identifica i moduli nella tua app, come le classi in un'app OO.
  • Interfaccia e implementazione separate per ciascun modulo, interfaccia solo ciò che è necessario per altri moduli. Ricorda che non esiste uno spazio dei nomi in C, quindi devi rendere unico tutto nelle tue interfacce (ad esempio, con un prefisso).
  • Nasconde le variabili globali nell'implementazione e usa le funzioni accessor per la lettura / scrittura.
  • Non pensare in termini di eredità, ma in termini di composizione. Come regola generale, non cercare di imitare C ++ in C, questo sarebbe molto difficile da leggere e mantenere.

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

La convenzione che utilizzo è:

  • Funzione pubblica (nel file di intestazione):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Funzione privata (statica nel file di implementazione)

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

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



1
Bella risposta. Punta alla modularità. OO dovrebbe fornirlo, ma 1) in pratica è fin troppo comune per finire con OO spaghetti e 2) non è l'unico modo. Per alcuni esempi nella vita reale, guarda il kernel di Linux (modularità in stile C) e i progetti che usano glib (OO in stile C). Ho avuto l'occasione di lavorare con entrambi gli stili e vince la modularità in stile C IMO.
Joh,

E perché esattamente la composizione è un approccio migliore dell'eredità? Razionale e riferimenti di supporto sono ben accetti. O ti riferivi solo ai programmi C?
Aleksandr Blekh,

1
@AleksandrBlekh - Sì, mi riferisco solo a C.
mouviciel,

16

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

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

Il polimorfismo diventa più complicato perché non c'è ereditarietà integrata e supporto per l'astrazione, quindi dovrai includere la struttura genitore nella tua classe figlio, o fare molte paste di copia, una di queste opzioni è francamente orribile, anche se tecnicamente semplice. Enorme quantità di bug in attesa di accadere.

Le funzioni virtuali possono essere facilmente ottenute attraverso i puntatori di funzione che puntano a funzioni diverse, come richiesto, ancora una volta - molto incline ai bug se fatto manualmente, molto lavoro noioso sull'inizializzazione corretta di tali puntatori.

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

Inutile dire che se puoi usare C ++ usa C ++. Non c'è una vera ragione per non farlo.


In realtà, è possibile ereditare da una struttura e aggiungere dati: è sufficiente dichiarare il primo elemento della struttura figlio come una variabile il cui tipo è la struttura padre. Quindi cast di cui hai bisogno.
mouviciel,

1
@mouviciel - si. L'ho detto. " ... quindi dovrai includere la struttura genitore nella tua classe figlio, o ... "
littleadv

5
Nessun motivo per provare a implementare l'ereditarietà. Come mezzo per ottenere il riutilizzo del codice, è un'idea sbagliata per cominciare. La composizione dell'oggetto è più facile e migliore.
KaptajnKold,

@KaptajnKold - d'accordo.
Littleadv,

8

Ecco le basi di come creare l'orientamento agli oggetti in C

1. Creazione di oggetti e incapsulamento

Di solito - uno crea un oggetto simile

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, object_instance (or object_instance_private_data)viene restituito è di tipo void *.L'applicazione non può fare riferimento a singoli membri o funzioni di questo.

Inoltre, ogni metodo utilizza queste object_instance per il metodo successivo.

2. Polimorfismo

Possiamo usare molte funzioni e puntatori a funzione per sovrascrivere determinate funzionalità in fase di esecuzione.

per esempio - tutti i metodi_oggetto sono definiti come puntatori a funzioni che possono essere estesi a metodi sia pubblici che privati.

Possiamo anche applicare il sovraccarico delle funzioni in un senso limitato, usando ciò 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. Definizione dell'eredità

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

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 il doctore engineerè derivato dalla persona ed è possibile tipografarlo a un livello superiore, diciamo person.

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

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

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

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


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

Inoltre, ci saranno molti argomenti secondo cui questa non sarà esattamente la vera proprietà dell'equivalente C ++. So che OO in C non sarà così rigoroso per la sua definizione. Ma lavorando in questo modo si comprenderebbero alcuni dei principi fondamentali.

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

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

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


3

L'orientamento agli oggetti 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 sintassi utili come costruttori / distruttori, modelli ecc.

1 è di gran lunga la più importante, ed è anche completamente indipendente dalla lingua, è tutto sulla progettazione del programma. In C lo fai creando "moduli di codice" autonomi costituiti da un file .h e un file .c. Considera questo come l'equivalente di una classe OO. Puoi decidere cosa dovrebbe essere inserito all'interno di questo modulo attraverso il buon senso, UML o qualunque metodo di progettazione OO che stai usando per i programmi C ++.

2 è anche abbastanza importante, non solo per proteggere un accesso intenzionale ai dati privati, ma anche per proteggere da un accesso involontario, ad esempio "disordine dello spazio dei nomi". C ++ lo fa in modi più eleganti di C, ma può ancora essere ottenuto in C usando la parola chiave statica. Tutte le variabili che avresti dichiarato private in una classe C ++, dovrebbero essere dichiarate come statiche in C e posizionate 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. È possibile scrivere programmi OO senza ereditarietà o senza costruttori / distruttori. Queste cose sono belle da avere, possono sicuramente rendere i programmi più eleganti e forse anche più sicuri (o il contrario se usati con noncuranza). Ma non sono necessari. Poiché C non supporta nessuna di queste utili funzioni, dovrai semplicemente farne a meno. I costruttori possono essere sostituiti con funzioni init / destruct.

L'ereditarietà può essere fatta attraverso vari trucchi strutturali, ma io sconsiglierei, poiché probabilmente renderà il tuo programma più complesso senza alcun guadagno (l'ereditarietà in generale dovrebbe essere attentamente applicata, 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 sgradevole, strana complessità ai tuoi programmi C che non vale la pena. (Il libro è disponibile gratuitamente qui per coloro che sono ancora interessati nonostante il mio avvertimento.)

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


2

Prendendo in prestito un po 'di esperienza dai vari runtime di Objective-C, scrivere una capacità di OO dinamica e polimorfica in C non è troppo difficile (rendendola veloce e facile da usare, d'altra parte, sembra essere ancora in corso dopo 25 anni). Tuttavia, se si implementa una funzionalità di oggetto in stile Objective-C senza estendere la sintassi del linguaggio, il codice che si ottiene è piuttosto disordinato:

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

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

La posizione in cui trovare le menzogne ​​dipende dal tuo progetto: sembra però che altri programmatori non utilizzino le tue interfacce C, quindi la scelta dipende dalle preferenze interne. Naturalmente, se decidi di volere qualcosa come la libreria di runtime objc, allora ci saranno librerie di runtime objc multipiattaforma che funzioneranno.


1

GObject non nasconde realmente 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 i segnali o il macchinario dell'interfaccia.

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

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


1

Prima di tutto, a meno che non si tratti di compiti a casa 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 da C ++ abbastanza facilmente.

In secondo luogo, esaminerei quanto farai affidamento sul polimorfismo e le eccezioni e le altre caratteristiche che i framework possono fornire, se non sono molto semplici strutture con funzioni correlate saranno molto più facili di un framework completo, se una parte significativa del tuo il design ne ha bisogno, quindi morde il proiettile e usa un framework in modo da non dover implementare le funzionalità da soli.

Se non hai ancora un progetto per prendere la decisione, fai un picco e vedi cosa ti dice il codice.

infine non deve essere una o l'altra scelta (anche se se si va dal framework fin dall'inizio si potrebbe anche attenersi ad esso), dovrebbe essere possibile iniziare con semplici strutture per le parti semplici e aggiungere solo nelle librerie come necessario.

EDIT: Quindi è una decisione della direzione destra, quindi ignorare il primo punto.

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.