PDA

Visualizza la versione completa : Pillola: [C] Programmazione modulare


Andrea Simonassi
18-01-2003, 02:08
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



#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ì



#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




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.



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



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




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

Luc@s
18-01-2003, 06:53
:eek:
Bello
:eek:

Johnny_Depp
18-01-2003, 09:40
Ottima idea ;)

Appena saibal ripristina i messaggi in "RILIEVO"
lo aggiungo alle pillole...
nel frattempo potresti inserire un link a questa discussione
nella tua firma.

r0x
18-01-2003, 12:42
Mooolto molto interessante! ;)

Aggiungerei un suggerimento. Si potrebbero utilizzare dei puntatori a funzione, che ancora di più faciliterebbero la leggibilità. Ad esempio il tutto potrebbe essere implementato così:



typedef struct _FILE_STRUCT
{
FILE* file;
int ( *get_byte )( FILE* ); /* punterà a getc() */
int ( *put_byte )( int, FILE* ); /* punterà a putc() */

} FILE_STRUCT;

...

typedef struct _STREAM
{
union
{
FILE_STRUCT file;
SOCKET_STRUCT socket;
STRING_STRUCT buffer;

} values;

enum { ST_STREAM_FILE, ST_STREAM_SOCKET, ST_STREAM_STRING } type;

} *STREAM;


E' solo un'idea, così come l'ho postato in realtà non aiuta molto. :)

GOOD PILLOLA! :gren:

saibal
20-01-2003, 01:24
ripristina pure:fagiano:

Luc@s
02-03-2003, 22:05
mi spieghi questo?


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 */

Andrea Simonassi
02-03-2003, 22:13
Significa che ho 3 diversi costruttori, uno per costruire uno stream bassato su file, uno per la memoria, uno per i socket, modificando opportunamente i campi della union interna, la funzione string_get è un esempio di una funzione nascosta all'interno di un modulo, che serve ad implementare l'estrazione di un byte da una stringa, la funzione stream_get è la funzione che dato un qualsiasi tipo di stream restituisce un byte....

se io scrivo un algoritmo che necessita uno stream, posso poi usarlo sia per lavorare con dati su un file, con dati che arrivano dalla rete o dati letti dalla memoria senza modificare l'algoritmo.

Lo svantaggio rispetto ai template C++ è che qui devo dinamicamente decidere il tipo di dato con cui sto lavorando (lo switch(this->type)), mentre un template C++ è risolto staticamente dal compilatore...., il vantaggio è che ho programmato quasi OOP con il C, ad esempio per una piattaforma senza compilatore C++.

TheGreatWorld
04-03-2003, 18:16
Visto che siamo in tema vorrei mostrare come in C sia possibile implementare un rudimentale sistema throw-catch tipico delle eccezioni del C++.

guardate questo code:




#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>

jmp_buf env;

int takecent(int i)
{

if ((i * i) != 100)
longjmp(env, 1); /* equivale al throw */
return i;
}

void control()
{
int var = 10;

if (!setjmp(env))
{
var = takecent(var);
printf("Il risultato e' corretto, ed e': %d\n", var);
}
else /* equivale al meccanismo try-catch */
{
fprintf(stderr, "Error n.1\t%s:%d\n", __FILE__, __LINE__);
exit(EXIT_FAILURE);
}

}


int main()
{
control();
return 0;
}



Solitamente il sistema setjmp/longjmp e' utilizzato per gestire delle situazioni intricate (in C). In pratica se ci troviamo in una funzione possiamo salvare lo stato dello stack e richiamarlo da un'altra funzione se necessario ripartendo dalla situazione salvata. L'importante e' che la funzione chiamante (quella con il setjmp) non ritorni prima del longjmp. Ora longjmp fa lo stesso lavoro di throw, ovvero srotola lo stack per trovare il punto in cui 'impiantarsi' facendo un raffronto con la var di tipo jmp_buf. Penso sia automatico pensare alla corrispondenza longjmp -> throw. Se qualcosa non e' chiaro fate sapere.

Bye

TheGreatWorld
04-03-2003, 18:27
Dimenticavo: questo programma come e' ora ovviamente non ritorna nessun errore, ma se provate a cambiare l'argomento passato a takecent...

bye

Andrea Simonassi
04-03-2003, 18:33
Interessantissimo... :)

Me lo studio un attimo.

Loading