18-12-2001
COSTRUTTORE |
DISTRUTTORE |
SCOPE E INIZIALIZZAZIONE |
ALLOCAZIONE STATICA |
INIZIALIZZAZIONE DI AGGREGATI |
NAME DECORATION |
ARGOMENTI DI DEFAULT |
ESERCIZIO PER CASA (SVOLTO) |
Solitamente quando si definisce una classe si definisce una funzione di inizializzazione che deve venir richiamata ogni volta che viene dichiarato un nuovo oggetto. Nel C++ c'è la possibilità di utilizzare un metodo chiamato costruttore per svolgere la funzione di inizializzazione in modo più semplice e veloce. Un costruttore è un metodo pubblico della classe che ha lo stesso nome della classe a cui appartiene. Può ricevere dei parametri ma non restituisce nulla. Viene chiamato automaticamente ogni volta che viene creato un oggetto della classe. La sua funzione è appunto quella di inizializzare l'oggetto e renderlo "pronto all'uso". Ogni volta che viene creato un oggetto la cui classe prevede un costruttore è necessario specificare i parametri che vengono passati al costruttore. Ecco un esempio di sintassi:
Header file
class poligono
} |
||
File con definizioni
...
} |
||
Codice sorgente (nel main)
poligono pentagono(5); |
||
Nell'header file viene dichiarato il costruttore e viene definito nel file con le definizioni delle funzioni. Il costruttore della classe poligono inizializza la variabile lati con il parametro che gli viene passato alla creazione dell'oggetto. Quindi quando viene creato un oggetto nel main di classe poligono, è necessario indicare tra parentesi il parametro da passare al costruttore (pentagono(5)) che viene chiamato automaticamente e inizializza la variabile lati dell'oggetto. Se si vuole inizializzare un puntatore ad un oggetto di classe poligono è necessario passare il parametro per il costruttore come nell'esempio. In questo caso l'attributo dell'oggetto puntquadrato->lati viene inizializzata a 4.
Il distruttore è a sua volta un metodo pubblico della classe che ha lo stesso nome della classe a cui appartiene preceduto dal simbolo "~" (tilde). Viene chiamato automaticamente quando un oggetto della classe a cui appartiene viene "distrutto" (cioè viene liberata la memoria dove risiedeva; questo succede ad esempio con la chiusura di un blocco di istruzioni). Ci può essere al massimo un distruttore nella classe e questo non può ricevere parametri ne ritornare nulla. Riprendendo l'esempio precedente:
Header file
class poligono
}; |
||
File con definizioni
...
} ...
} |
||
Codice sorgente (nel main)
{
}
|
||
Il distruttore, come il costruttore, viene dichiarato nell'header file e definito nel file con tutte le definizioni. Nel caso della classe poligono il distruttore non fa nulla di speciale: stampa a video un messaggio che informa della cancellazione di un elemento. Nel main viene dichiarato un oggetto poligono di nome triangolo, inizializzato dal costruttore, all'interno di un blocco di istruzioni; quando il blocco di istruzioni viene chiuso (dove si chiude la graffa) viene chiamato automaticamente il distruttore che stampa il messaggio "Elemento di classe poligono cancellato" a video.
vedi esempio Stash con costruttore e distruttore: |
|||
L'ambito di validità di una variabile si chiama scope. Una variabile può essere dichiarata in ogni punto del programma, all'interno di un blocco (sempre che non sia una variabile globale). Lo scope di una variabile locale va quindi dalla dichiarazione della variabile alla chiusura del blocco in cui è definita. Lo stesso vale per un oggetto (che è una variabile di tipo class): il suo scope inizia quando viene dichiarato e inizializzato dal costruttore, a quando viene chiuso il blocco in cui è stato dichiarato e viene chiamato il distruttore. Si può dire quindi che il costruttore viene chiamato all'inizio dello scope e il distruttore alla fine dello scope.
E' consigliabile ridurre il più possibile lo scope di una variabile dichiarandola in un blocco subito prima di utilizzarla. Per questo motivo non è bene dichiarare variabili globali le quali hanno uno scope che dura in tutto il file dove è definita.
Non si può inserire una dichiarazione di variabile (non si può nemmeno creare un oggetto) all'interno di un blocco con condizione: c'è il rischio che la condizione non sia vera e che perciò la variabile contenuta nel blocco non venga dichiarata; così si genera un errore quando si cerca di utilizzare la variabile nel seguito del programma (è il caso dello switch).
Quando incontra una definizione di variabile, il compilatore alloca memoria sufficiente a contenere il valore della variabile: questa è l'allocazione statica della memoria che viene eseguita una volta per tutte quando il codice del programma viene compilato (compiling time). Allo stesso modo, quando viene creato un oggetto e viene effettuata la chiamata al costruttore, il compilatore riserva memoria sufficiente per contenere gli attributi dell'oggetto. Il nome della variabile o dell'oggetto è associato direttamente alla memoria allocata.
E' possibile anche allocare della memoria durante l'esecuzione del programma. Questo è molto utile quando non si può sapere in precedenza la quantità di memoria necessaria: ad esempio per costruire una lista di elementi dei quali non si conosce il numero. Per allocare memoria dinamicamente (run time) si utilizza l'operatore "new" seguito dal tipo dell'elemento che si vuole allocare. L'operatore new ritorna un puntatore alla zona di memoria allocata. Ecco un esempio di sintassi con gli oggetti:
poligono *puntquadrato; |
||
puntquadrato è un puntatore ad un oggetto di classe poligono e viene inizializzato con una new ad un indirizzo di memoria che individua appunto la memoria allocata. Allora puntquadrato punta ad un oggetto poligono che è tra l'altro inizializzato a 4 dal costruttore. Attenzione: per accedere agli attributi o alle funzioni di un oggetto allocato dinamicamente è necessario usare l'operatore "->"; con l'allocazione dinamica infatti, non si ha un nome associato ad una zona di memoria (come nel caso dell'allocazione statica) ma un puntatore.
E' necessario liberare esplicitamente la memoria allocata con l'uso dell'operatore delete seguito dal nome del puntatore alla zona di memoria da deallocare. Ecco un esempio di sintassi:
delete puntquadrato;
Il puntatore puntquadrato punta ancora allo stesso indirizzo di memoria ma quello che vi era memorizzato è stato cancellato (la memoria è stata liberata).
Può essere utile inserire la delete nel distruttore, per non dover fare ogni volta una delete dei puntatori all'interno dell'oggetto, prima che questo venga cancellato.
vedi esempi: |
|||
Ci sono diversi modi per inizializzare un vettore senza dover assegnare uno alla volta il valore ad ogni elemento. Ecco un esempio:
int a[3] = {1, 2, 3};
Il vettore a contiene 3 elementi che sono gli interi 1, 2, 3, nell'ordine in cui sono elencati all'interno delle graffe: a[0]=1, a[1]=2, a[2]=3.
Si può anche scrivere:
int b[3] = {0};
Stavolta il vettore b contiene tre elementi che sono tutti e tre inizializzati a 0.
E' possibile anche omettere il numero di elementi di cui è costituito il vettore se tra parentesi graffe si specificano gli elementi contenuti dal vettore; è possibile definire e inizializzare a in questo modo:
int a[]= {1, 2, 3};
Questa scrittura è equivalente a quella del primo esempio. Si può utilizzare questo tipo di notazione anche per definire un vettore di oggetti senza e con l'utilizzo del costruttore:
ACLASS x[] = {{1,2,'a'},{3,4,'b'}};
x è un vettore di due oggetti di tipo ACLASS. I due oggetti di tipo ACLASS sono a loro volta inizializzati con {1,2,'a'} e {3,4,'b'}: il primo elemento è assegnato al primo attributo, il secondo al secondo attributo ecc.... Se esiste un costruttore allora:
BCLASS y[] = {BCLASS(2,'a'),BCLASS(3,4,'b')};
y contiene due oggetti di tipo BCLASS inizializzati con il costruttore.
E' possibile sapere di quanti elementi è costituito il vettore con l'istruzione sizeof:
Nelementi = sizeof a/sizeof *a;
In questo modo "sizeof a" ritorna il numero di byte occupati dall'intero vettore a, mentre "sizeof *a" ritorna il numero di byte occupati da un solo elemento di a. Facendo la divisione si calcola il numero di elementi di cui a è costituito.
Non sempre è utile utilizzare questo tipo di inizializzazione del vettore: si può rendere il codice scomodo e difficile da leggere.
Il compilatore (allo scopo di distinguere i nomi delle varie funzioni senza conflitti) completa (decora) i nomi dati dal programmatore alle varie funzioni aggiungendovi il campo di validità in cui sono definite e il tipo dei parametri che prendono. Ecco come:
void f(); --> _f //funzione
globale void CL::f(); --> _CL_f //funzione member di CLASSE void f(int); --> _f_int //stavolta c'è il parametro void CL::f(int); --> _CL_f_int void f(int,int); --> _f_int_int //due parametri void f(int,float); --> _f_int_float |
||
Le varie funzioni f hanno tutte lo stesso nome per il programmatore ma assumono nomi diversi per il compilatore (che include nel nome anche l'ambito e i parametri). Se un nome viene usato per due funzioni che differiscono solo per gli argomenti si dice che il nome è sovradefinito o overloaded.
Questa caratteristica è molto utile per definire più costruttori, cioè per fare un overloading di costruttori. Poter definire diversi costruttori per la stessa classe significa poter inizializzare gli oggetti in modi differenti. Poiché i costruttori devono avere tutti lo stesso nome (cioè quello della classe a cui appartengono), si possono distinguere solo per il numero dei parametri. Al momento della creazione viene chiamato il costruttore che ha tanti parametri quanti sono quelli specificati nella dichiarazione:
classe x
}; |
||
Nell'header file vengono dichiarati quattro costruttori. Ogni costruttore è diverso dagli altri perchè riceve un numero diverso di argomenti. I costruttori possono essere definiti in modo da assegnare i valori ricevuti in ordine ai tre attributi. Per i costruttori che ricevono meno di tre argomenti, si può decidere (nella definizione di ogni costruttore) di inizializzare gli ultimi attributi ad esempio a zero. Così il primo costruttore assegna 0 ad a,b e c; il secondo assegna il valore ricevuto ad a e assegna 0 a b e c; l'ultimo costruttore assegna tutti i valori ricevuti in ordine ad a,b e c. Ecco come chiamare ,nell'ordine, i diversi costruttori:
x primo;
//a=b=c=0 x secondo(3); //a=3,b=c=0 x terzo(7,8); //a=7,b=9,c=0 x quarto(3,4,5); //a=3,b=4,c=5 |
||
Vedi esempio di stash con più costruttori: |
|||
Vedi esempio di mem: |
|||
E' possibile dichiarare dei costruttori che assegnano dei valori di default ad alcuni argomenti. Ecco un esempio:
class y
}; |
||
Il costruttore y può venir chiamato con uno, due o tre argomenti: se viene chiamato con un argomento solo questo viene assegnato ad x mentre w e z vengono inizializzati a 0; se viene chiamato con due argomenti allora vengono inizializzati x e w con i valori passati mentre z viene inizializzata a 0; se viene chiamato con tre argomenti allora sia x che w che z vengono inizializzati con i valori passati. E' obbligatorio che gli argomenti di default siano gli ultimi. Con questo sistema è possibile ottenere lo stesso risultato che si avrebbe definendo tre diversi costruttori che sono definiti in modo molto simile (ad esempio quelli dell'esempio precedente).
In definitiva ci sono due strade per inizializzare l'oggetto in modi diversi: o con costruttori diversi (con un numero differente di argomenti) o utilizzando argomenti di default. Non sempre queste due strade sono equivalenti. In generale si usano gli argomenti di default se si vogliono definire costruttori molto simili tra loro; si usa invece l'overloading di costruttori se questi sono molto diversi tra loro.
Vedi esempio con argomenti di default: |
|||
Header file:
#ifndef TEXT_H
}; |
||
File con definizioni:
#include "Text.H"
}
}
} |
||
File sorgente:
#include "Text.H"
} |
||