LISTE LINKATE |
VERSO GLI OGGETTI |
CLASSI |
INCAPSULAZIONE |
HIDDEN |
METODI DI INPUT E OUTPUT |
Grazie alle strutture si possono raggruppare i dati in modo al tempo stesso ordinato e veloce. Definendo una struttura che al suo interno contiene un puntatore ad un elemento (oggetto) dello stesso tipo, si possono definire degli oggetti tutti collegati tra loro tramite puntatore.
struct list { | ||
int dato; | ||
list *next; | ||
}; | ||
... | ||
list *head,*p; | ||
head = new list; | ||
head->dato=7; | ||
head->next = new list; | ||
p = head->next; | ||
p->dato = 11; | ||
p->next = 0; | ||
... |
next è un puntatore che punta ad un elemento di tipo list. head è pure un puntatore che punta ad un elemento di tipo list, precisamente al primo elemento della lista. E' importante non cancellare mai il puntatore head per non perdere l'accesso alla lista: infatti per accedere ai dati della lista è necessario procedere in maniera sequenziale, seguendo i puntatori definiti dentro ogni elemento. L'istruzione new alloca memoria sufficiente a contenere un elemento di tipo list e ritorna un puntatore alla zona di memoria allocata. Ogni volta che si vuole aggiungere un elemento alla lista bisogna fare una new e inizializzare il puntatore next dell'elemento precedente al valore ritornato dalla new. Il puntatore next dell'ultimo elemento della lista viene inizializzato a zero (cioè non punta a nulla). Il puntatore p è utile per scandire la lista, inizializzandolo, di volta in volta, al valore del puntatore next dell'elemento corrente. Poichè gli elementi della lista sono memorizzati in zone di memoria non contigue, è possibile inserire un elemento nuovo in mezzo alla lista con facilità: basta giocare con i puntatori. Nella lista precedente è possibile inserire un terzo elemento tra gli altri due nel seguente modo:
list *ptemp; | ||
ptemp = new list; | ||
ptemp->dato = 9; | ||
ptemp->next = head->next; //oppure ptemp->next = p; | ||
head->next = ptemp; |
Ora la lista ha tre elementi ordinati che contengono i dati:7, 9, 11. E' anche facile cancellare un elemento in mezzo alla lista (sempre scambiando i puntatori, magari con l'aiuto di un puntatore temporaneo come ptemp). Inoltre è banale aggiungere elemento in testa o in coda alla lista. Se si aggiungono sempre elementi in fondo alla lista allora questa prende il nome di coda (LILO: last input last output). Se gli elementi invece vengono aggiunti sempre in testa alla lista allora si parla di pila o stack (LIFO: last input first output). Per una struttura come lo stack generalmente si definiscono delle funzioni (metodi) che permettono di accedere allo stack in modo rapido:
push: inserisce un elemento in testa allo stack.
pop: toglie l'ultimo elemento inserito (cioè quello in testa) e ne restituisce il valore.
peek: ritorna il valore dell'elemento in testa ma non lo elimina dallo stack.
cleanup: cancella l'intero stack e libera la memoria.
initialize: prepara lo stack per essere utilizzato e ne inizializza il primo elemento
Vedi esempio:
Header file | ||
Definizione dei metodi | ||
File sorgente | ||
Per le liste con le classi vedi lezione 7-1-2002 | ||
Come accennato nella lezione precedente, le strutture sono molto simili alle classi: hanno degli attributi (i campi della struttura che contengono i dati) e possono avere anche dei metodi (cioè delle member function definite all'interno della struttura). Grazie alle strutture si possono raggruppare i dati e lavorare con essi interagendo con un interfaccia che nasconde il contenuto della struttura stessa: si possono utilizzare i metodi senza sapere come sono definiti (push, pop ...). Per realizzare appunto questa information hiding sono state definite in C++ delle parole chiave che limitano la visibilità dei campi della struttura:
"public:" :tutti gli attributi e i metodi definiti dopo questa istruzione sono visibili a tutte le funzioni del programma, siano esse member o no.
"private:" :tutti gli attributi e i metodi definiti dopo questa direttiva sono accessibili solo alle member functions della struct.
Queste direttive sono in accordo con i principi di Parna: chi implementa una ADT (una classe o una struct, cioè dei tipi di dato astratti) deve avere solo le informazioni strettamente necessarie per realizzare la struct; chi utilizza una ADT deve avere tutte le informazioni necessarie per usarla e nient'altro. Definendo la struct con limitazioni di accesso si evita che chiunque possa accedere liberamente alla struct. Ecco un esempio:
struct data{ | ||
private: | ||
int giorno; | ||
int mese; | ||
int anno; | ||
public: | ||
void cambia_data(int,int,int); | ||
void print_data(); | ||
}; |
Gli attributi della struct sono visibili solo alle member functions della struct. Non è possibile quindi definire una funzione esterna che lavori con i campi della struct. Nemmeno le member functions di strutture diverse possono accedere ai campi private della struct data. Esiste però in C++ una direttiva che permette ad una funzione esterna alla struct (oppure a tutte le funzioni di un'altra struct) di accedere ai dati privati: la direttiva friend. Ad esempio, scrivendo:
friend int funesterna (...);
all'interno della struttura data, si permette alla funzione esterna funesterna di accedere ai campi privati della struttura data. La stessa cosa si può fare per una member function di un'altra struct:
friend struct compleanno;
permettendo a tutti i metodi della struct compleanno di accedere agli attributi della struct data. Si può fare lo stesso anche con una salo member function di compleanno usando l'operatore "::". Non è consigliabile abusare della direttiva friend per non aumentare troppo la complessità del codice.
Se in una struct non vengono specificate private e public, gli attributi e i metodi vengono considerati tutti public. In una class invece tutto ciò che non è specificato come public è automaticamente private. Questa è l'unica differenza tra le struct e le class: sebbene sia una piccola differenza pratica, è stato introdotto il nome class per indicare il diverso modo di affrontare il problema e per sottolineare l'uso di una programmazione ad oggetti. Ecco la definizione di una classe equivalente alla struct data:
class data{ | ||
int giorno; | ||
int mese; | ||
int anno; | ||
public: | ||
void cambia_data(int,int,int); | ||
void print_data(); | ||
}; |
Ora si può vedere che la classe comprende degli attributi e dei metodi. Una volta definita una classe si possono definire le sue istanze cioè gli oggetti:
data Gennaio,Febbraio;
Gennaio e Febbraio sono due oggetti definiti a partire dalla classe data. Tutti gli oggetti definiti a partire dalla stessa classe hanno logicamente tutti le stesse caratteristiche: cioè gli stessi attributi e gli stessi metodi.
Vedi esempi:
Header file | ||
File con definizione dei metodi | ||
File sorgente | ||
Grazie alle classi è possibile quindi realizzare una incapsulazione del codice: un oggetto permette di nascondere tutti i dettagli di implementazione e allo stesso tempo "mostra" solo ciò che interessa all'utente. Ogni oggetto può interagire con gli altri solo con dei messaggi (ad esempio una chiamata di funzione) e rispondere ai messaggi con i metodi che gli sono attribuiti. In questo modo è possibile suddividere il problema in parti distinte, rendere il lavoro più semplice e più logico, ed è anche più semplice modificare il codice, dato che gli oggetti sono distinti e possono essere quindi modificati distintamente.
In C++ si può dichiarare la classe in un file classe.h che verrà poi incluso in tutti i file di codice che utilizzano la classe; invece la definizione vera e propria dei metodi della classe può essere fatta all'interno di un file classe.cpp che include il file classe.h. Il file header diventa così una sorta di definizione di tipo, mentre il file .cpp contiene il codice che permette ai metodi della classe di funzionare. Il file classe.cpp verrà poi compilato e linkato al file sorgente del programma. In questo modo il file sorgente del programma viene semplificato e chi lo crea non ha bisogno di conoscere come sono fatte le classi utilizzate ma solo come funzionano.
Per evitare che qualcuno possa vedere come una parte della classe è definita, è possibile agire in questo modo nel file .h che contiene la classe:
class classe{ | ||
struct nascosta; | ||
nascosta *puntatore; | ||
public: | ||
...}; |
In questo modo la struct nascosta non è definita nel file header ma viene definita solo in seguito nel file .cpp:
struct nascosta{ | ||
int data; | ||
... | ||
}; |
Così, quando il file .cpp che contiene la struct nascosta viene compilato non è più possibile risalire al codice che definisce la struct nascosta. Si può però accedere alla struct nascosta tramite il puntatore definito nel file .h. Così solo il programmatore che ha creato la classe può sapere come è definita la struct nascosta.
I metodi get() e getline() possono essere associati a cin:
char c; | ||
cin.get(c); //legge un carattere da tastiera e lo mette in c | ||
char buffer[100]; | ||
cin.get(buffer,100,'\n'); | ||
cin.getline(buffer,100,'\n'); |
cin.get(buffer,100,'\n') e cin.getline(buffer,100,'\n') leggono 100 caratteri da tastiera e li mettono in buffer. Si fermano con il '\n'.
Si possono associare a cout i metodi put() e write():
char c; | ||
c = 'a'; | ||
cout.put(c); //scrive il carattere contenuto in c su video | ||
char stringa[]="abcdefghilmno"; | ||
cout.write(stringa,4); //stampa quattro caratteri di stringa senza \n |