PROGRAMMAZIONE AD OGGETTI 2001-2002
7-1-2002
Se in una classe vengono definiti due costruttori dei quali uno dei due è senza parametri e l'altro ha invece un solo parametro di default, c'è ambiguità nel caso in cui venga chiamato il costruttore senza parametri: il compilatore non può decidere quale dei due costruttori usare, poiché entrambi possono essere chiamati senza parametri. Ecco un esempio:
Header file "Casa.h":
class Casa
{int piani;
mquadri;
public:
Casa (int metri=0);
Casa ();};
File con definizioni:
# include "Casa.h"
Casa::Casa()
{...
}
Casa::Casa(int metri)
{mquadri = metri;
...}
File con il main:
#include <iostream.h>
#include "Casa.h"int main ()
{Casa condominio(4);
Casa villetta;
/* Il compilatore segnala l'errore quando arriva a questa istruzione */}
Come si può vedere la classe Casa ha due costruttori. Entrambi possono venire chiamati senza nel main alla creazione di un oggetto, di tipo Casa, senza parametri. Come si può vedere il compilatore non segnala l'errore nella compilazione del file con le definizioni, ma solo quando nel main viene creato un oggetto che genera ambiguità (ad esempio l'oggetto "condominio" non genera ambiguità perché la chiamata viene effettuata con un parametro; l'oggetto "villetta" invece non specifica nessun parametro e quindi non si può capire quale dei due costruttori deve essere chiamato).
vedi esempi: | ||
Header file cambig.h | ||
File di libreria cambig.cpp | ||
File sorgente testambig.cpp | ||
File esempio motore.cpp |
Le liste linkate sono state affrontate anche nella lezione 11-12-2001.
Per ottenere una lista linkata si deve definire una struct (o una classe) che ha al suo interno uno spazio per i dati e un puntatore ad un oggetto della stessa struct (classe). Il caso della struct è già stato trattato.
Si può quindi definire una classe di questo tipo:
class Lista
{structlista* testa;
public:Lista(...);
inserisci(...);
rimuovi(...);
...};
dove "structlista" è una struttura così definita:
struct structlista
{int dato;
structlista* next;};
Così, creando un oggetto di tipo Lista, questo contiene un puntatore ad elementi di tipo structlista. A loro volta gli elementi di tipo structlista possono essere utilizzati per generare una lista, poiché contengono un puntatore ad elementi structlista. Inoltre all'interno della classe sono definite delle member functions che lavorano sulla lista di struct; essendo member functions non c'è bisogno di passar loro li puntatore Testa.
E' possibile a qualunque funzione accedere ad una lista di questo tipo. Per avere una lista più protetta si può definire una classe che svolge la stessa funzione della struct ma che ha il puntatore next definito come private. Ecco un esempio:
class Lista
{class classlista
{private:
classlista* next;
public:
int dato;
friend class Lista;};
classlista* testa;
public:
Lista(...);
~Lista();
insert(...);
remove(...);
...};
La classe classlista serve per creare la lista (ha la stessa funzione della struct dell'esempio precedente); poiché la classe classlist è definita come private della classe Lista, il puntatore next non è accessibile a nessuna funzione esterna alla classe Lista, che è definita friend. In questo modo la lista è più protetta poiché può essere modificata solo dalle funzioni di Lista; nell'esempio precedente invece qualunque funzione poteva utilizzare il puntatore next.
Può essere comodo usare il costruttore o il distruttore di una classe per creare o distruggere automaticamente una lista alla creazione o alla distruzione di un oggetto.
Il costruttore non deve far altro che inizializzare il puntatore al primo elemento della lista (nel caso precedente "testa") a 'NULL': in questo modo viene creata una lista vuota (infatti il puntatore dell'ultimo elemento di una lista è solitamente inizializzato a NULL).
Il distruttore invece deve liberare la memoria occupata dalla lista con una serie di delete: tante quanti sono gli elementi memorizzati nella lista. E' possibile ottenere questo risultato usando un metodo remove() (trattato in seguito) che ritorna FALSE se la lista è già vuota oppure ritorna TRUE ogni volta che cancella il primo elemento della lista; allora si risolva con un ciclo while:
while (remove());
Così viene eseguito il metodo remove() fino a quando la lista non è vuota (cioè fino a quando remove() non ritorna un valore FALSE).
CONTROLLO DEL CONTENUTO E AGGIUNTA O RIMOZIONE DI ELEMENTI
Per controllare se la lista è vuota si può utilizzare un metodo listIsEmpty() che controlla il valore del puntatore testa e ritorna 0 se testa punta a NULL, altrimenti ritorna 1:
int listIsEmpty ()
{if (testa == NULL) return (0);
return (1)}
Per inserire nella lista un nuovo elemento in testa (ciò all'inizio della lista: l'ultimo elemento inserito è quello puntato da testa) si può utilizzare il metodo insert (...), che prende in ingresso il dato da inserire, crea il nuovo elemento con una new e poi scambia i puntatori:
int insert(int elemento)
{classlista *tmp = new classlista;
tmp->dato = elemento;
tmp->next = testa;
testa = tmp;
if (testa->dato == elemento) return (1);
return (0);}
Questa funzione fa inoltre un controllo ritornando 1 se l'operazione è stata eseguita correttamente, zero altrimenti. "tmp" è un puntatore temporaneo che serve a scambiare opportunamente i valori di testa e di testa->next (attenzione all'ordine, si rischia di perdere il puntatore testa). E' possibile anche inserire un elemento in fondo alla lista: basta scorrere la lista fino all'ultimo elemento (controllando quando next assume il valore NULL) e poi fare una new di un elemento classlista e assegare il valore del puntatore ritornato dalla new al puntatore next dell'ultimo elemento:
puntUltimoEl->next = new classlista;
Dove puntUltimoEl è un puntatore all'ultimo elemento della lista.
Per cancellare un elemento dalla cima lista si può utilizzare il metodo int remove(), che controlla che la lista non sia vuota e poi cancella il primo elemento della lista, cambiando il valore di testa e liberando la memoria occupata dal primo elemento:
int remove()
{if(listIsEmpty()==0) return(0);
classlista *tmp = testa;
testa = testa->next;
/*testa punta al secondo elemento*/
delete tmp;
/*libera la memoria occupata dal primo elemento*/
return (1);}
Questo metodo ritorna 0 se la lista è già vuota mentre ritorna 1 se è stata eseguita la cancellazione dell'elemento in cima.
Per prendere il valore del primo elemento della lista si può usare una funzione int leggi(int& puntPrimoEl) che scrive il contenuto di testa->dato nell'indirizzo associato alla variabile che viene passata alla funzione:
int leggi(int& puntPrimoEl)
{if(listIsEmpty==0) return(0);
puntPrimoEl = testa->dato;
/* non serve l'asterisco perchè c'è un passaggio per riferimento */
return (1);}
Il metodo ritorna 0 se la lista è vuota e 1 dopo aver copiato il valore del primo elemento nella variabile passata alla funzione.
Può essere molto utile creare una lista i cui elementi sono ordinati in base al valore contenuto nei dati: se i dati sono numeri possono essere inseriti nella lista in ordine crescente, se son lettere in ordine alfabetico ecc.... Si può usare (per l'esempio precedente) una funzione insord (int elemento) che cerca il punto della lista in cui inserire l'elemento (considerando anche il caso particolare della lista vuota o il caso in cui il nuovo elemento venga prima dell'elemento in testa) e poi lo aggiunge giocando con i puntatori. Servono due puntatori ausiliari, uno che punta all'elemento corrente e uno che punta al precedente:
insord (int elemento)
{classlista* corrente = testa;
classlista* precedente = testa;
if ((corrente == NULL) || (elemento <= corrente->dato))
{classlista* nuovoel = new classlista;
/*questo if controlla i due casi particolari*/
nuovoel->dato = elemento;
nuovoel->next = corrente;
testa = nuovoel;
return();}
while ((corrente != NULL) && (elemento > corrente->dato))
{precedente = corrente;
/*con questo while viene trovata la posizione corretta in cui inserire il nuovo elemento. Se corrente diventa NULL allora l'elemento viene inserito in fondo alla lista*/
corrente = corrente->next;
}
classlista* nuovoel = new classlista;
nuovoel->dato = elemento;
nuovoel->next = corrente;
precedente->next = nuovoel;
return ();}
Le liste linkate hanno il grosso vantaggio di essere allocate dinamicamente e quindi possono essere create runtime e ordinate con estrema semplicità. E' un po' meno comodo l'accesso agli elementi poichè bisogna scorrerli in sequenza.
Gli array invece hanno una dimensione prestabilita e sono difficili da ordinare (per inserire un elemento all'inizio bisogna spostare tutti gli elementi successivi). Hanno però il vantaggio di un accesso molto veloce agli elementi permettendo una ricerca semplice e veloce.
Serve per eseguire delle istruzioni in modo veloce e con certi vincoli. E' utile per ricompilare un programma che è stato modificato senza dover ripetere manualmente tutte le istruzioni. E' possibile stabilire delle regole per l'esecuzione delle istruzioni (per compilare ad esempio solo una parte di un programma il cui codice è suddiviso in più files); queste regole sono conservate in un file che di default è makefile oppure può essere un file diverso (da chiamare con make -f nomefile)
La sintassi generale di make prevede dei file obiettivo (targets) che sono quelli sui quali il make può compiere delle azioni. I file obiettivo dipendono da altri files chiamati appunto dipendenze. Ad esempio un file che contiene il main può dipendere da un header file con la dichiarazione delle classi e da un file .cpp con la definizione dei metodi. Se alcuni files dipendenza sono più recenti del file obiettivo corrispondente allora vengono eseguite le azioni corrispondenti a quell'obiettivo. Ecco un esempio di sintassi:
obiettivo: dipendenze
'tab' azioni
All'interno di makefile:
eseguibile: main.o
cc -o eseguibile main.o
main.o: main.cpp header.h
cc -c main.cpp
In questo esempio, alla chiamata make, viene controllato il primo target (eseguibile) che dipende da main.o; quindi viene controllato main.o che a sua volta dipende da main.cpp e dal file header.h; se main.cpp o header.h sono più recenti di main.o allora main.o viene ricompilato utilizzando appunto l'azione specificata (cc -c main.cpp); dopo viene ricontrollato il primo target: se main.o è stato modificato più recentemente di eseguibile, allora viene eseguita l'azione corrispondente (cc -o eseguibile main.o). In questo modo vengono eseguite solo le azioni che effettivamente comportano delle modifiche ai file target.
Se un target non ha dipendenze, allora l'azione associata viene eseguita sempre alla chiamata di make.
C'è la possibilità di definire delle macro per semplificare la scrittura del file makefile, oltre al possibile utilizzo di parecchie macro predefinite.
Provare per credere!