Ad eccezione delle etichette, ogni identificatore che il programmatore intende utilizzare in un programma C++, sia esso per una variabile, una costante simbolica, di tipo o di funzione, va dichiarato prima di essere utilizzato. Ci sono diversi motivi che giustificano la necessita` di una dichiarazione; nel caso di variabili, costanti o tipi:
- consente di stabilire la quantita` di memoria necessaria alla memorizzazione di un oggetto;
- determina l'interpretazione da attribuire ai vari bit che compongono la regione di memoria utilizzata per memorizzare l'oggetto, l'insieme dei valori che puo` assumere e le operazioni che possono essere fatte su di esso;
- permette l'esecuzione di opportuni controlli per determinare errori semantici;
- fornisce eventuali suggerimenti al compilatore;
nel caso di funzioni, invece una dichiarazione:
- determina numero e tipo dei parametri e il tipo del valore restituito;
- consente controlli per determinare errori semantici;
Le dichiarazioni hanno anche altri compiti che saranno chiariti in seguito.
Tipi primitivi
Un tipo e` una coppia < V, O >, dove V e` un insieme di valori e O e` un insieme di operazioni per la creazione e la manipolazione di elementi di V.
In un linguaggio di programmazione i tipi rappresentano le categorie di informazioni che il linguaggio consente di manipolare. Il C++ fornisce sei tipi fondamentali (o primitivi):
- bool
- char
- wchar_t
- int
- float
- double
Abbiamo gia visto (vedi "Vero e falso") il tipo bool e sappiamo che esso serve a rappresentare i valori di verita`; su di esso sono definite sostanzialmente le usuali operazioni logiche (&& per l'AND, || per l'OR, ! per la negazione...) e non ci soffermeremo oltre su di esse, solo si faccia attenzione a distinguerle dalle operazioni logiche su bit (rispettivamente &, |, ~...).
Il tipo char e` utilizzato per rappresentare piccoli interi (e quindi su di esso possiamo eseguire le usuali operazioni aritmetiche) e singoli caratteri; accanto ad esso troviamo anche il tipo wchar_t che serve a memorizzare caratteri non rappresentabili con char (ad esempio i caratteri unicode).
int e` utilizzato per rappresentare interi in un intervallo piu` grande di char.
Infine float e double rappresentano entrambi valori in virgola mobile, float per valori in precisione semplice e double per quelli in doppia precisione.
Ai tipi fondamentali e` possibile applicare i qualificatori signed (con segno), unsigned (senza segno), short (piccolo) e long (lungo) per selezionare differenti intervalli di valori; essi tuttavia non sono liberamente applicabili a tutti i tipi: short si applica solo a int, signed e unsigned solo a char e int e infine long solo a int e double. In definitiva sono disponibili i tipi:
- bool
- char
- wchar_t
- short int
- int
- long int
- signed char
- signed short int
- signed int
- signed long int
- unsigned char
- unsigned short int
- unsigned int
- unsigned long int
- float
- double
- long double
Il tipo int e` per default signed e quindi e` equivalente a tipo signed int, invece i tipi char, signed char e unsigned char sono considerate categorie distinte. I vari tipi sopra elencati, oltre a differire per l'intervallo dei valori rappresentabili, differiscono anche per la quantita` di memoria richiesta per rappresentare un valore di quel tipo (che pero` puo` variare da implementazione a implementazione). Il seguente programma permette di conoscere la dimensione di alcuni tipi come multiplo di char (di solito rappresentato su 8 bit), modificare il codice per trovare la dimensione degli altri tipi e` molto semplice e viene lasciato per esercizio:
#include <iostream>
using namespace std;
int main(int, char* []) {
cout << "bool: " << sizeof(bool) << endl;
cout << "char: " << sizeof(char) << endl;
cout << "short int: " << sizeof(short int) << endl;
cout << "int: " << sizeof(int) << endl;
cout << "float:" << sizeof(float) << endl;
cout << "double: " << sizeof(double) << endl;
return 0;
}
Una veloce spiegazione sul listato: le prime due righe permettono di utilizzare una libreria (standard) per eseguire l'output su video; la libreria iostream dichiara l'oggetto cout il cui compito e` quello di visualizzare l'output che gli viene inviato tramite l'operatore di inserimento <<.
L'operatore sizeof(<Tipo>) restituisce la dimensione di Tipo, mentre endl inserisce un ritorno a capo e forza la visualizzazione dell'output. L'ultima istruzione serve a terminare il programma. Infine main e` il nome che identifica la funzione principale, ovvero il corpo del programma, parleremo in seguito e piu` in dettaglio di main().
Tra i tipi fondamentali sono definiti gli operatori di conversione, il loro compito e` quello di trasformare un valore di un tipo in un valore di un altro tipo. Non esamineremo per adesso l'argomento, esso verra` ripreso in una apposita appendice.
Variabili e costanti
Siamo ora in grado di dichiarare variabili e costanti. La sintassi per la dichiarazione delle variabili e`
< Tipo > < Lista Di Identificatori > ;
Ad esempio:
int a, b, B, c;
signed char Pippo;
unsigned short Pluto; // se omesso si intende int
Innanzi tutto ricordo che il C++ e` case sensitive, cioe` distingue le lettere maiuscole da quelle minuscole, infine si noti il punto e virgola che segue sempre ogni dichiarazione.
La prima riga dichiara quattro variabili di tipo int, mentre la seconda una di tipo signed char. La terza dichiarazione e` un po' particolare in quanto apparentemente manca la keyword int, in realta` poiche` il default e` proprio int essa puo` essere omessa; in conclusione la terza dichiarazione introduce una variabile di tipo unsigned short int. Gli identificatori che seguono il tipo sono i nomi delle variabili, se piu` di un nome viene specificato essi devono essere separati da una virgola.
E` possibile specificare un valore con cui inizializzare ciascuna variabile facendo seguire il nome dall'operatore di assegnamento = e da un valore o una espressione che produca un valore del corrispondente tipo:
int a = -5, b = 3+7, B = 2, c = 1;
signed char Pippo = 'a';
unsigned short Pluto = 3;
La dichiarazione delle costanti e` identica a quella delle variabili eccetto che deve sempre essere specificato un valore e la dichiarazione inizia con la keyword const:
const a = 5, c = -3; // int e` sottointeso
const unsigned char d = 'a', f = 1;
const float g = 1.3;
Scope e lifetime
La dichiarazione di una variabile o di un qualsiasi altro identificatore si estende dal punto immediatamente successivo la dichiarazione (e prima dell'eventuale inizializzazione) fino alla fine del blocco di istruzioni in cui e` inserita (un blocco di istruzioni e` racchiuso sempre tra una coppia di parentesi graffe). Cio` vuol dire che quella dichiarazione non e` visibile all'esterno di quel blocco, mentre e` visibile in eventuali blocchi annidati dentro quello dove la variabile e` dichiarata.
Il seguente schema chiarisce la situazione:
// Qui X non e` visibile
{
... // Qui X non e` visibile
int X = 5; // Da ora in poi esiste una variabile X
... // X e` visibile gia` prima di =
{ // X e` visibile anche in questo blocco
...
}
...
} // X ora non e` piu` visibile
All'interno di uno stesso blocco non e` possibile dichiarare piu` volte lo stesso identificatore, ma e` possibile ridichiararlo in un blocco annidato; in tal caso la nuova dichiarazione nasconde quella piu` esterna che ritorna visibile non appena si esce dal blocco ove l'identificatore viene ridichiarato:
{
... // qui X non e` ancora visibile
int X = 5;
... // qui e` visibile int X
{
... // qui e` visibile int X
char X = 'a'; // ora e` visibile char X
... // qui e` visibile char X
} // qui e` visibile int X
...
} // X ora non piu` visibile
All'uscita dal blocco piu` interno l'identificatore ridichiarato assume il valore che aveva prima di essere ridichiarato:
{
...
int X = 5;
cout << X << endl; // stampa 5
while (--X) { // riferisce a int X
cout << X << ' '; // stampa int X
char X = '-';
cout << X << ' '; // ora stampa char X
}
cout << X << endl; // stampa di nuovo int X
}
Una dichiarazione eseguita fuori da ogni blocco introduce un identificatore globale a cui ci si puo` riferire anche con la notazione ::<ID>. Ad esempio:
int X = 4; // dichiarazione esterna ad ogni blocco
int main(int, char* []) {
int X = -5, y = 0;
/* ... */
y = ::X; // a y viene assegnato 4
y = X; // assegna il valore -5
return 0;
}
Abbiamo appena visto che per assegnare un valore ad una variabile si usa lo stesso metodo con cui la si inizializza quando viene dichiarata. L'operatore :: e` detto risolutore di scope e, utilizzato nel modo appena visto, permette di riferirsi alla dichiarazione globale di un identificatore.
Ogni variabile oltre a possedere uno scope, ha anche un propria durata (lifetime), viene creata subito dopo la dichiarazione (e prima dell'inizializzazione! ndr) e viene distrutta alla fine del blocco dove e` posta la dichiarazione; fanno eccezione le variabili globali che vengono distrutte alla fine dell'esecuzione del programma. Da cio` si deduce che le variabili locali (ovvero quelle dichiarate all'interno di un blocco) vengono create ogni volta che si giunge alla dichiarazione, e distrutte ogni volta che si esce dal blocco; e` tuttavia possibile evitare che una variabile locale (dette anche automatiche) venga distrutta all'uscita dal blocco facendo precedere la dichiarazione dalla keyword static:
void func() {
int x = 5; // x e` creata e
// distrutta ogni volta
static int c = 3; // c si comporta in modo diverso
/* ... */
}
La variabile x viene creata e inizializzata a 5 ogni volta che func() viene eseguita e viene distrutta alla fine dell'esecuzione della funzione; la variabile c invece viene creata e inizializzata una sola volta, quando la funzione viene chiamata la prima volta, e distrutta solo alla fine del programma. Le variabili statiche conservano sempre l'ultimo valore che viene assegnato ad esse e servono per realizzare funzioni il cui comportamento e` legato a computazioni precedenti (all'interno della stessa esecuzione del programma) oppure per ragioni di efficenza. Infine la keyword static non modifica lo scope.