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:

  1. bool
  2. char
  3. wchar_t
  4. short int
  5. int
  6. long int
  7. signed char
  8. signed short int
  9. signed int
  10. signed long int
  11. unsigned char
  12. unsigned short int
  13. unsigned int
  14. unsigned long int
  15. float
  16. double
  17. 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.