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 Deprecated: Function ereg() is deprecated in /home/demetres/public_html/didattica/ae/libs/Wakka.class.php on line 648 Ingegneria degli Algoritmi: Tipi di dato astratti in C

Ingegneria degli Algoritmi

Corso di Laurea in Ingegneria Informatica e Automatica - A.A. 2014-2015

HomePage | Avvisi | Diario lezioni | Programma | Materiale didattico | Esami | Forum | Login

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:


Realizzazione in Java

Una semplice realizzazione in Java potrebbe essere la seguente:

Persona.java
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:

ProvaPersona.java
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:


come illustrato di seguito.

Persona.h
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);


Persona.c
#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:

main.c
#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:

Persona.h
#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:

main.c
#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:

Persona.h
#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:

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:


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:

Persona.h
#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


Persona.c
#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;
}


main.c
#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);
}



Valid XHTML 1.0 Transitional :: Valid CSS :: Powered by Wikka Wakka Wiki 1.1.6.3
Page was generated in 0.1011 seconds