Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/wikka.php on line 315
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/libs/Wakka.class.php on line 176
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/libs/Wakka.class.php on line 463
Deprecated: Function set_magic_quotes_runtime() is deprecated in /home/demetres/public_html/didattica/ae/wikka.php on line 120
Tipi di dato astratti in C
In questa dispensa mostriamo mediante un semplice esempio come realizzare tipi di dato astratti in linguaggio C. Partiamo da una realizzazione in Java e mostriamo una corrispondente realizzazione in linguaggio C utilizzando il costrutto
struct del C al posto del costrutto
class di Java.
Tipo di dato astratto Persona
Consideriamo il problema di rappresentare il tipo di dato
Persona con:
- Attributi:
- nome di tipo stringa
- eta di tipo intero
- Operazioni:
- costruttore a due argomenti che inizializza una istanza del tipo di dato
- operazioni set/get per i due attributi.
Realizzazione in Java
Una semplice realizzazione in Java potrebbe essere la seguente:
class Persona
{
private String nome;
private int eta;
public Persona
(String n,
int e
){
this.
nome = n;
this.
eta = e;
}
public String getNome
(){
return this.
nome;
}
public int getEta
(){
return this.
eta;
}
public void setNome
(String n
){
this.
nome = n;
}
public void setEta
(int e
){
this.
eta = e;
}
}
Un possibile programma di prova potrebbe essere:
class ProvaPersona
{
public static void main
(String[] args
){
Persona p =
new Persona
("Mario Rossi",
25);
System.
out.
println("Persona: nome=" + p.
getNome() +
", eta=" + p.
getEta());
}
}
Realizzazione in C
Una possibile rappresentazione in C richiede di definire una struct che descrive gli attributi:
struct Persona {
char* nome;
int eta;
};
di cui è utile definire un alias di tipo:
typedef struct Persona Persona;
Osserviamo che la nozione di classe Java può essere realizzata mediante una struct in C. Diversamente dalle classi Java, in C non è possibile inserire operazioni all'interno di una struttura. Le operazioni devono essere pertanto realizzate come funzioni C che operano su oggetti della struct. Ad esempio, il costruttore può essere realizzato come segue:
Persona* nuovaPersona(char* nome, int eta){
Persona* oggetto = (Persona*)malloc(sizeof(Persona));
oggetto->nome = nome;
oggetto->eta = eta;
return oggetto;
}
Si noti che viene dapprima allocato dinamicamente un oggetto struct di tipo Persona, poi ne vengono inizializzati gli attributi e infine ne viene restituito l'indirizzo. Diversamente da Java, che fornisce deallocazione automatica (garbage collection) degli oggetti non più in uso, in C è necessario prevedere un
distruttore per il tipo di dato, che si occuperà di rilasciare la memoria allocata dal costruttore e più in generale di ogni blocco allocato dinamicamente la cui responsabilità è dell'oggetto stesso (es. attributi che puntano ad altri oggetti).
Il seguente esempio mostra un possibile distruttore per il tipo di dato Persona:
void cancellaPersona(Persona* oggetto) {
free(oggetto);
}
Si noti che viene deallocato l'oggetto, ma non la memoria puntata dall'attributo nome, la cui responsabilità rimane del cliente del tipo di dato Persona:
questa è una scelta di progetto del programmatore che realizza il tipo di dato e deve essere specificata nella documentazione a corredo della realizzazione stessa in modo che sia chiaro ai clienti che usano il tipo di dato.
Le altre operazioni possono essere realizzate come segue, assumendo di passare primo argomento un puntatore all'oggetto su cui si intende operare:
char* getNome(Persona* oggetto){
return oggetto->nome;
}
int getEta(Persona* oggetto){
return oggetto->eta;
}
void setNome(Persona* oggetto, char* n){
oggetto->nome = n;
}
void setEta(Persona* oggetto, int e){
oggetto->eta = e;
}
Una realizzazione completa dovrebbe essere composta da due file:
- Persona.h: header che dichiara il tipo Persona e le funzioni ad esso associate
- Persona.c: modulo che definisce le funzioni fornendone il codice C
come illustrato di seguito.
typedef struct Persona {
char* nome;
int eta;
} Persona;
Persona* nuovaPersona (char* nome, int eta);
void cancellaPersona (Persona* oggetto);
char* getNome (Persona* oggetto);
int getEta (Persona* oggetto);
void setNome (Persona* oggetto, char* nome);
void setEta (Persona* oggetto, int eta);
#include <stdlib.h>
#include "Persona.h"
/* costruttore */
Persona* nuovaPersona(char* nome, int eta){
Persona* oggetto = (Persona*)malloc(sizeof(Persona));
oggetto->nome = nome;
oggetto->eta = eta;
return oggetto;
}
/* distruttore */
void cancellaPersona(Persona* oggetto) {
free(oggetto);
}
/* lettura nome oggetto */
char* getNome(Persona* oggetto){
return oggetto->nome;
}
/* lettura eta oggetto */
int getEta(Persona* oggetto){
return oggetto->eta;
}
/* modifica nome oggetto */
void setNome(Persona* oggetto, char* n){
oggetto->nome = n;
}
/* modifica eta oggetto */
void setEta(Persona* oggetto, int e){
oggetto->eta = e;
}
Il programma di prova Java visto sopra può essere tradotto in C come segue:
#include <stdio.h>
#include "Persona.h"
int main
(){
Persona* p = nuovaPersona
("Mario Rossi",
25);
printf("Persona: nome=%s, eta=%d\n", getNome
(p
), getEta
(p
));
cancellaPersona
(p
);
}
In un sistema UNIX/Linux il programma può essere compilato ed eseguito come segue:
> gcc Persona.c main.c -o mioProgramma
> ./mioProgramma
Persona: nome=Mario Rossi, eta=25
Alternativamente, è possibile compilare separatamente i due moduli:
> gcc Persona.c -c
> gcc main.c -c
L'operazione produce i due file oggetto Persona.o e main.o, che possono essere collegati dal linker per produrre il file eseguibile mioProgramma come segue:
> gcc Persona.o main.o -o mioProgramma
Raffinamento: soluzione problema inclusioni multiple
Per evitare il problema delle possibili inclusioni multiple, è buona pratica modificare la header Persona.h come segue:
#ifndef __Persona__
#define __Persona__
typedef struct Persona {
char* nome;
int eta;
} Persona;
Persona* nuovaPersona (char* nome, int eta);
void cancellaPersona (Persona* oggetto);
char* getNome (Persona* oggetto);
int getEta (Persona* oggetto);
void setNome (Persona* oggetto, char* nome);
void setEta (Persona* oggetto, int eta);
#endif
racchiudendo il corpo della header stessa fra direttive #ifndef ... #endif.
Raffinamento: rendere privati gli attributi dell'oggetto
Si noti che, dichiarando la struct Persona nella header Persona.h, di fatto si rende possibile l'accesso diretto agli attributi degli oggetti Persona, come nel seguente esempio:
#include <stdio.h>
#include "Persona.h"
int main
(){
Persona* p = nuovaPersona
("Mario Rossi",
25);
p->eta =
30;
/* <-- accesso diretto all'attributo, non desiderabile */
printf("Persona: nome=%s, eta=%d\n", getNome
(p
), getEta
(p
));
cancellaPersona
(p
);
}
che stamperebbe:
Persona: nome=Mario Rossi, eta=30
Per ovviare il problema si può dichiarare nel file Persona.h una struct
incompleta come segue:
#ifndef __Persona__
#define __Persona__
typedef struct Persona Persona;
Persona* nuovaPersona (char* nome, int eta);
void cancellaPersona (Persona* oggetto);
char* getNome (Persona* oggetto);
int getEta (Persona* oggetto);
void setNome (Persona* oggetto, char* nome);
void setEta (Persona* oggetto, int eta);
#endif
Di fatto, la dichiarazione:
typedef struct Persona Persona;
introduce lo struct tag Persona e ne crea contestualmente un alias Persona, ma non specifica gli attributi della struttura. In C può usare un puntatore a una struct incompleta, ma non si può accedere ai suoi membri poiché non sono noti.
Così facendo, si otterrebbe il seguente errore di compilazione nel main definito sopra:
> gcc Persona.c main.c -o mioProgramma
main.c: In function ‘main’:
main.c:6: error: dereferencing pointer to incomplete type
dove la riga 6 è proprio l'istruzione
p->eta = 30;. In questo modo gli attributi dell'oggetto Persona diventano privati ai clienti, come il modulo main.c, che includono Persona.h.
La definizione (o completamento) della struct Persona si può a questo punto spostare all'interno del file Persona.c:
#include <stdlib.h>
#include "Persona.h"
/* completamento della struct Persona precedentemente introdotta come tipo incompleto in Persona.h */
struct Persona {
char* nome;
int eta;
};
/* costruttore */
Persona* nuovaPersona(char* nome, int eta){
Persona* oggetto = (Persona*)malloc(sizeof(Persona));
oggetto->nome = nome;
oggetto->eta = eta;
return oggetto;
}
...
Raffinamento: creare uno spazio di nomi per il tipo di dato
In Java una classe fornisce uno spazio di nomi isolato dal resto del programma, per cui è possibile avere due metodi con lo stesso nome in classi diverse. Questo non avviene in C, spostando sul programmatore l'onere di fornire nomi opportuni agli identificatori in modo da evitare di avere più funzioni con lo stesso nome ma pensate per operare su tipi di dato diversi. Ad esempio, definendo la funzione
getNome per operare su oggetti
Persona, non è possibile ridefinirlo per operare su altri tipi di dato, per cui il nome è "bruciato" non essendo possibile fare overloading in C. Una possibile strategia molto usata è quella di aggiungere un prefisso ai nomi degli identificatori associati a un tipo di dato in modo da specializzarli per quel tipo.
Nel caso del tipo
Persona, possiamo effettuare ad esempio i seguenti cambi di nome:
- nuovaPersona -> Persona_New
- cancellaPersona -> Persona_Delete
- getNome -> Persona_GetNome
- setNome -> Persona_SetNome
- getEta -> Persona_GetEta
- setEta -> Persona_SetEta
Questo rende possibile definire operazioni concettualmente simili su tipi di dato diversi senza avere conflitti sui nomi delle funzioni. Ad esempio, possiamo usare
Persona_SetNome per il tipo
Persona e
Studente_SetNome per un eventuale tipo
Studente.
Programma completo
Riassumiamo di seguito la realizzazione finale completa del tipo di dato Persona e del relativo programma di prova:
#ifndef __Persona__
#define __Persona__
typedef struct Persona Persona;
Persona* Persona_New (char* nome, int eta);
void Persona_Delete (Persona* oggetto);
char* Persona_GetNome (Persona* oggetto);
int Persona_GetEta (Persona* oggetto);
void Persona_SetNome (Persona* oggetto, char* nome);
void Persona_SetEta (Persona* oggetto, int eta);
#endif
#include <stdlib.h>
#include "Persona.h"
struct Persona {
char* nome;
int eta;
};
Persona* Persona_New(char* nome, int eta){
Persona* oggetto = (Persona*)malloc(sizeof(Persona));
oggetto->nome = nome;
oggetto->eta = eta;
return oggetto;
}
void Persona_Delete(Persona* oggetto) {
free(oggetto);
}
char* Persona_GetNome(Persona* oggetto){
return oggetto->nome;
}
int Persona_GetEta(Persona* oggetto){
return oggetto->eta;
}
void Persona_SetNome(Persona* oggetto, char* n){
oggetto->nome = n;
}
void Persona_SetEta(Persona* oggetto, int e){
oggetto->eta = e;
}
#include <stdio.h>
#include "Persona.h"
int main
(){
Persona* p = Persona_New
("Mario Rossi",
25);
printf("Persona: nome=%s, eta=%d\n", Persona_GetNome
(p
), Persona_GetEta
(p
));
Persona_Delete
(p
);
}