Programmazione modulare in C.
Ok, il C non è un linguaggio OOP, e' tuttavia possibile (e auspicabile), ottenere una certo grado di modularita' nel codice C.
Se i moduli sono sufficientemente generici e ben strutturati, il codice sarà riusabile, mantenibile e leggibile, trasformando il vostro programma C da un caotico ammasso di funzioni scorrelate ad un programma quasi OOP.
Innanzitutto pensiamo ad oggetti in fase di progettazione, progettare ad oggetti consente di pensare in modo strutturato al nostro programma, raggruppando insiemi di funzionalità e dati correlati in classi.
Un progetto OOP può benissimo, e con successo, essere codificato in C, non è obbligatorio usare linguaggi OOP, e ci possono essere migliaia di buoni motivi per scegliere il C invece del C++ (anche se in futuro saranno sempre di meno).
Entriamo nel vivo, supponiamo di avere un progetto e di avere un idea dei moduli che ci occorreranno.
Nei linguaggi OOP esiste l'ereditarietà, il polimorfismo, l'incapsulamento dei dati, la tipazione forte, la delegazione e le interfacce.
In C dobbiamo arrangiarci con quello che abbiamo, se per eredita' e polimorfismo c'è troppo lavoro da fare (lavoro che il C++ fa automaticamente) possiamo simulare invece abbastanza bene l'incapsulamento dei dati e, un po' meno bene, le interfacce.
I ferri del mestiere saranno le parole chiave extern e static che ci servono per nascondere o rendere pubbliche variabili e funzioni contenute nei moduli, in modo da simulare le variabili pubbliche e private dei linguaggi OOP.
Le struct ci permettono di definire la struttura dei dati da incapsulare, le union ci permettono di definire tipi generici per potere simulare in parte le interfacce.
Il typedef infine ci consente di definire le nostre strutture come tipo di dato (quasi una class).
Per ogni modulo dobbiamo definire un file .h e un file .c
Il file .h inizierà sempre con una cosa del tipo
codice:
#ifndef _MIOFILE_H
#define _MIOFILE_H
.....
#endif
per evitare che venga incluso più di una volta durante la fase di compilazione.
nel file .h va inserita la definizione della struttura dati e il prototipo di tutte le funzioni pubbliche collegate alla struttura dati.
per esempio un modulo che si occupa di comunicazioni bufferizzate su tcp/ip potrebbe iniziare così
codice:
#ifndef _TCPSREAM_H
#define TCP_STREAM_H
#include <string.h>
/* definiamo la classe implementata nel modulo */
typedef struct _TCPSTREAM{
char mybuffer[TCPSTREAM_BUFSIZE];
char unbuffer[TCPSTREAM_UNGETC_BUFSIZE];
char * bp;
char * sp;
int unpos;
int uncount;
int sock;
}*TCPSTREAM ;
/* al posto del costruttore C++ */
TCPSTREAM tcp_new(int sock);
/* al posto del distruttore C++ */
void tcp_delete(TCPSTREAM this);
/* metodi */
int tcp_writebinary(TCPSTREAM this, void* data, size_t length, unsigned int timeout);
int tcp_writestring(TCPSTREAM this,char* textstring, unsigned int timeout);
int tcp_getc(TCPSTREAM this,unsigned int timeout);
int tcp_ungetc(TCPSTREAM this,int character);
int tcp_readbinary(TCPSTREAM this,void* vbuffer, size_t bytestoread,
size_t * readed, unsigned int timeout);
void* tcp_readbinary_all(TCPSTREAM this, size_t * readed, unsigned
int timeout) ;
int tcp_readline(TCPSTREAM this,char* buffer, size_t buffersize, unsigned int timeout);
#endif
Notare che i metodi prendono tutti come primo parametro il tipo definito come TCPSTREAM, cosa che i linguaggi orientati agli oggetti fanno in modo implicito.
Con l'esempio qua sopra l'utilizzatore del modulo non sa che TCPSTREAM è un puntatore, per lui è semplicemente un tipo che rappresenta un flusso di dati su canale tcp/ip, classico esempio di incapsulamento dei dati. L'utilizzatore del modulo sa che i metodi forniti sono tali da consentirgli ottenere cio' che vuole senza necessita' di sapere cosa significhi il campo bp. In realtà il C non lo nasconde e in teoria egli, contro ogni buonsenso, potrebbe decidere di metterci le mani e cambiarne il valore causando quasi certamente un danno al suo proprio software (cavoli suoi).
Se per qualsiasi motivo il modulo dovesse rendere pubbliche alcune variabili globali (es: nel caso che due o + moduli facciano parte di un supermodulo che li usa e li incapsula tutti e che questi moduli debbano condividere dei dati globali) vanno dichiarate extern nel .h e ridichiarate normalmente nel .c
Complichiamo un po' le cose: talvolta ci si rende conto che sarebbe opportuno usare un'unica interfaccia di programmazione per potere usare oggetti con funzioni simili ma che devono essere implementati in modo diverso, traduco con un esempio: per rappresentare un flusso di dati, vorrei usare un oggetto di tipo flusso nei miei algoritmi (es: per implementare un interprete di espressioni algebriche), senza dovermi occupare del fatto che si tratti di un flusso di dati prelevati da file, letti dalla memoria, o scaricati da una connessione tcp/ip.
In questi casi nei linguaggi OOP si definisce una classe base astratta che poi viene implementata da diverse classi eredi, oppure si definisce un'interfaccia e si creano diverse classi che la implementano.
(Un po' di nomenclatura: due classi implementano la stessa interfaccia quando implementano gli stessi identici metodi, con lo stesso nome e la stessa lista di argomenti e tipo di ritorno, presumibilmente con la stessa semantica, anche se con codice diverso, e operando su dati diversi).
In C come implementiamo questa bella proprietà dei linguaggi OOP?
Il risultato che vogliamo ottenere è una cosa del tipo
codice:
typedef struct _STREAM
{
.....
} * STREAM;
STREAM new_file_stream(FILE*file);
STREAM new_socket_stream(int socket);
STREAM new_string_stream(char * s);
delete_stream(STREAM this);
int stream_get(STREAM this);
int stream_put(STREAM this);
int stream_unget(STREAM this, int character);
Dove come stream vogliamo potere usare sia una socket, che un file, che un'area di memoria (stringa), ovvero STREAM deve potere contenere in certi casi i dati necessari a manipolare un file, in altri casi i dati necessari a manipolare una stringa, ma vogliamo usare in tutti i tre i casi le stesse funzioni stream_get, stream_put e stream_unget
Usando una union è possibile definire un tipo di dato che ne può contienere diversi, ma uno solo per volta, cioè una union può contenere un intero o un double ma non entrambi contemporaneamente.
codice:
union variant
{
int i_value;
double d_value;
}value;
il compilatore crea la union riservando uno spazio tale da poter contenere almeno il tipo che richiede maggiore spazio.
la union può a sua volta essere ridefinita come tipo usando il typedef, inoltre può contenere intere struct, così come una struct puo contenere una union.
Tornando all'esempio definiamo la nostra struttura in modo che possa contenere alternativamente uno dei tre tipi di sorgente stream
codice:
typedef struct _STREAM
{
union
{
/* dato usato dal codice che gestisce i file*/
FILE* file;
/* dato usato dal codice che gestisce le socket*/
int socket;
/*dato usato dal codice che gestisce lo stream di memoria */
char * buffer;
}values;
enum {ST_STREAM_FILE, ST_STREAM_SOCKET, ST_STREAM_STRING} type;
} * STREAM;
ecco come potrebbe essere il codice nel file .c che implementa l'interfaccia di cui piu' in alto
codice:
STREAM new_file_stream(FILE*file)
{
STREAM this;
if (!file)
return NULL;
this = new_stream();
if(!this)return NULL;
this->values.file = file;
this->type = ST_STREAM_FILE;
return s;
}
STREAM new_socket_stream(int socket)
{
STREAM this;
if(this==-1)
return NULL;
this= new_stream();
if(!this)return NULL;
this->values.socket = socket;
this->type = ST_STREAM_SOCKET;
return this;
}
STREAM new_string_stream(char * buffer)
{
STREAM this = new_stream();
if(!this)return NULL;
this->values.buffer = buffer;
// ...
// eccetera
// ...
return this;
}
static string_get(STREAM this)
{
/* preleva il byte successivo dalla stringa */
};
/*
altro codice
*/
int stream_get(STREAM this)
{
if (this==NULL) return -2;
switch(this->type)
{
case ST_STRING_STREAM:
return string_get(this);
case ST_FILE_STREAM:
return getc(this->values.file);
case ST_SOCKET_STREAM:
return socket_get(this->values.socket);
}
}
/* eccetera */
Notare che esistono alcune funzioni che non fanno parte dell'interfaccia, ma che sono necessarie invece all'implementazione dell'interfaccia, tali funzioni sono definite static il che significa che non saranno visibili all'esterno del file .c corrente.
Alla fine dei giochi ci ritroviamo con diversi moduli .c e .h, che dovremo compilare uno per uno per produrre il nostro programma, ogni modulo che intende usare i servizi offerti da un altro modulo deve includere il file .h corrispondente, alla fine bisogna linkare insieme tutti i vari file oggetto creati dal compilatore, la fatica è maggiore ma è l'unico modo di sopravvivere se si creano programmi un po' piu' complessi di dammi due numeri, ecco questa è la somma.
Piu che una pillola è stata una supposta ;)