Variabili in C++

Concetti Chiave
  • Una variabile è uno spazio di memoria a cui è associata un'etichetta che i nostri programmi possono manipolare.
  • Ogni variabile in C++ ha un tipo che determina la dimensione, la disposizione in memoria, l'intervallo di valori e le operazioni applicabili alla variabile.
  • Una semplice definizione di variabile consiste in uno specificatore di tipo, seguito da un elenco di nomi di variabili separati da virgole, e termina con un punto e virgola.
  • Una variabile può essere inizializzata al momento della definizione con un valore specificato.
  • L'inizializzazione non è assegnazione; sono operazioni diverse in C++.
  • Le variabili hanno un ambito che determina dove possono essere usate nel codice.
  • Le variabili globali sono definite al di fuori di qualsiasi funzione e hanno ambito globale.
  • Le variabili locali sono definite all'interno di una funzione o di un blocco e hanno ambito locale.
  • Le variabili locali possono nascondere le variabili globali con lo stesso nome.
  • Riusare i nomi delle variabili rende il codice più difficile da leggere e può portare a errori sottili.

Variabili

Una variabile rappresenta uno spazio di memoria a cui è associata un'etichetta che i nostri programmi possono manipolare.

Ogni variabile in C++ ha un tipo. Il tipo determina la dimensione e la disposizione in memoria della variabile, l'intervallo di valori che possono essere memorizzati in quella memoria e l'insieme di operazioni che possono essere applicate alla variabile.

I programmatori C++ tendono a riferirsi alle variabili come "variabili" o "oggetti" in modo intercambiabile.

Definizioni di Variabili

Una semplice definizione di variabile consiste in uno specificatore di tipo, seguito da un elenco di uno o più nomi di variabili separati da virgole, e termina con un punto e virgola.

La sintassi è la seguente:

specificatore_di_tipo nome1, nome2, ... ;

Ogni nome nell'elenco ha il tipo definito dallo specificatore di tipo. Una definizione può (opzionalmente) fornire un valore iniziale per uno o più dei nomi che definisce:

// somma, valore e elementi_venduti hanno tipo int
// somma e elementi_venduti hanno valore iniziale 0
int somma = 0, valore,
    elementi_venduti = 0;

// articolo ha tipo ArticoloVendite
ArticoloVendite articolo;

// string è un tipo di libreria,
// che rappresenta una sequenza di caratteri di lunghezza variabile
// libro inizializzato da letterale stringa
std::string libro("0-201-78345-X");

La definizione di libro usa il tipo di libreria std::string. Come iostream, string è definito nel namespace std. Studieremo in dettaglio il tipo string nelle prossime lezioni.

Per ora, ciò che è utile sapere è che una string è un tipo che rappresenta una sequenza di caratteri di lunghezza variabile. La libreria string ci fornisce diversi modi per inizializzare oggetti string. Uno di questi modi è l'inizializzazione attraverso la copia di un letterale stringa. Quindi, libro è inizializzato per contenere i caratteri 0-201-78345-X.

Consiglio

Terminologia: Cos'è un oggetto?

I programmatori C++ tendono ad essere disinvolti nell'uso del termine oggetto. Più in generale, un oggetto è una regione di memoria che può contenere dati e ha un tipo.

Alcuni usano il termine oggetto solo per riferirsi a variabili o valori di tipi classe. Altri distinguono tra oggetti nominati e non nominati, usando il termine variabile per riferirsi a oggetti nominati. Altri ancora distinguono tra oggetti e valori, usando il termine oggetto per i dati che possono essere modificati dal programma e il termine valore per i dati che sono di sola lettura.

In questo corso, seguiremo l'uso più generale secondo cui un oggetto è una regione di memoria che ha un tipo. Useremo liberamente il termine oggetto indipendentemente dal fatto che l'oggetto abbia tipo built-in o classe, sia nominato o non nominato, o possa essere letto o scritto.

Inizializzatori

Un oggetto che è inizializzato ottiene il valore specificato nel momento in cui viene creato. I valori usati per inizializzare una variabile possono essere espressioni arbitrariamente complicate.

Quando una definizione definisce due o più variabili, il nome di ogni oggetto diventa visibile immediatamente. Quindi, è possibile inizializzare una variabile al valore di una definita precedentemente nella stessa definizione.

// OK:
// prezzo è definito e inizializzato prima
// di essere usato per inizializzare sconto
double prezzo = 109.99, sconto = prezzo * 0.16;

// OK:
// chiama applicaSconto e usa il valore di ritorno
// per inizializzare prezzoScontato
double prezzoScontato = applicaSconto(prezzo, sconto);

L'inizializzazione in C++ è un argomento sorprendentemente complicato e uno a cui torneremo più e più volte.

Molti programmatori sono confusi dall'uso del simbolo = per inizializzare una variabile. Si potrebbe essere tentati di pensare all'inizializzazione come a una forma di assegnazione, ma inizializzazione e assegnazione sono operazioni diverse in C++. Questo concetto è particolarmente confuso perché in molti linguaggi la distinzione è irrilevante e può essere ignorata. Inoltre, anche in C++ la distinzione spesso non ha importanza. Tuttavia, è un concetto cruciale e uno che ribadiremo in tutto il corso.

Nota

In C++, L'inizializzazione non è la stessa cosa dell'assegnazione

L'inizializzazione non è assegnazione. L'inizializzazione avviene quando a una variabile viene dato un valore quando viene creata. L'assegnazione cancella il valore corrente di un oggetto e sostituisce quel valore con uno nuovo.

Le Liste di Inizializzazione

Un motivo per cui l'inizializzazione è un argomento complicato è che il linguaggio C++ definisce diverse forme diverse di inizializzazione.

Per esempio, possiamo usare uno qualsiasi dei seguenti quattro modi diversi per definire una variabile int chiamata articoli_venduti e inizializzarla a 0:

int articoli_venduti = 0;
int articoli_venduti = {0};
int articoli_venduti{0};
int articoli_venduti(0);

L'uso generalizzato delle parentesi graffe per l'inizializzazione è stato introdotto come parte dello standard C++11.

Questa forma di inizializzazione era stata precedentemente consentita solo in modi più ristretti. Per ragioni che impareremo nelle prossime lezioni, questa forma di inizializzazione è detta inizializzazione con liste. Le liste di inizializzatori tra parentesi graffe possono ora essere usate ogni volta che inizializziamo un oggetto e in alcuni casi quando assegniamo un nuovo valore a un oggetto.

Quando usata con variabili di tipo built-in, questa forma di inizializzazione ha una proprietà importante: Il compilatore non ci permetterà di inizializzare con liste variabili di tipo built-in se l'inizializzatore potrebbe portare alla perdita di informazioni:

long double ld = 3.1415926536;

// ERRORE:
// Il passaggio di un long double a un int
// comporta la perdita della parte frazionaria
int a{ld}, b = {ld};

// VALIDO:
// Tuttavia il valore sarà troncato
int c(ld), d = ld;

Il compilatore rifiuta le inizializzazioni di a e b perché usare un long double per inizializzare un int comporta molto probabilmente una perdita di dati. Come minimo, la parte frazionaria di ld sarà troncata. Inoltre, la parte intera in ld potrebbe essere troppo grande per entrare in un int.

Come presentato qui, la distinzione potrebbe sembrare banale, dopotutto, è improbabile che inizializziamo direttamente un int da un long double. Tuttavia, come vedremo nelle prossime lezioni, tali inizializzazioni potrebbero accadere involontariamente. Diremo di più su queste forme di inizializzazione in futuro.

Inizializzazione Predefinita

Quando definiamo una variabile senza un inizializzatore, la variabile è inizializzata per default. A tali variabili viene dato il valore "predefinito". Quale sia quel valore predefinito dipende dal tipo della variabile e può anche dipendere da dove la variabile è definita.

Il valore di un oggetto di tipo built-in che non è esplicitamente inizializzato dipende da dove è definito. Le variabili definite fuori da qualsiasi corpo di funzione sono inizializzate a zero. Con un'eccezione, che vedremo, le variabili di tipo built-in definite dentro una funzione sono non inizializzate. Il valore di una variabile non inizializzata di tipo built-in è indefinito. È un errore copiare o altrimenti provare ad accedere al valore di una variabile il cui valore è indefinito.

Ogni classe controlla come inizializziamo oggetti di quel tipo classe. In particolare, spetta alla classe se possiamo definire oggetti di quel tipo senza un inizializzatore. Se possiamo, la classe determina quale valore avrà l'oggetto risultante.

La maggior parte delle classi ci permette di definire oggetti senza inizializzatori espliciti. Tali classi forniscono un valore predefinito appropriato per noi. Per esempio, come abbiamo appena visto, la classe string della libreria dice che se non forniamo un inizializzatore, allora la string risultante è la stringa vuota:

// vuota implicitamente inizializzato alla stringa vuota
std::string vuota;

// oggetto ArticoloVendite inizializzato per default
ArticoloVendite articolo;

Alcune classi richiedono che ogni oggetto sia esplicitamente inizializzato. Il compilatore si lamenterà se proviamo a creare un oggetto di tale classe senza inizializzatore.

Nota

Variabili non Inizializzate nel Corpo di una Funzione

Gli oggetti non inizializzati di tipo built-in definiti dentro un corpo di funzione hanno valore indefinito. Gli oggetti di tipo classe che non inizializziamo esplicitamente hanno un valore che è definito dalla classe.

Nota

Le Variabili Non Inizializzate Possono causare problemi a tempo di esecuzione

Una variabile non inizializzata ha un valore indeterminato.

Provare ad usare il valore di una variabile non inizializzata è un errore che è spesso difficile da scovare. Inoltre, il compilatore non è tenuto a rilevare tali errori, sebbene la maggior parte avviserà almeno su alcuni usi di variabili non inizializzate.

Cosa succede quando usiamo una variabile non inizializzata è indefinito. A volte siamo fortunati e il nostro programma va in crash non appena accediamo all'oggetto. Una volta rintracciata la posizione del crash, di solito è facile vedere che la variabile non è stata inizializzata correttamente.

Altre volte, il programma si completa ma produce risultati errati. Ancora peggio, i risultati potrebbero apparire corretti in un'esecuzione del nostro programma ma fallire in un'esecuzione successiva. Inoltre, aggiungere codice al programma in una posizione non correlata può causare che quello che pensavamo fosse un programma corretto inizi a produrre risultati errati.

Raccomandiamo, quindi, di inizializzare ogni oggetto di tipo built-in. Non è sempre necessario, ma è più facile e sicuro fornire un inizializzatore finché non si può essere certi che sia sicuro omettere l'inizializzatore.

Esercizi

Esaminiamo alcune definizioni di variabili e vediamo se sono legali o meno in C++.

  1. Definizione (a):

    std::cin >> int valore_input;
    

    Questa definizione non è legale. Non possiamo definire una variabile all'interno di un'espressione. La parte int valore_input è una definizione di variabile, ma non può apparire come parte dell'operazione di input. Per correggere questo errore, dobbiamo definire valore_input in una dichiarazione separata:

    int valore_input;
    std::cin >> valore_input;
    
  2. Definizione (b):

    int i = { 3.14 };
    

    Questa definizione non è legale. Stiamo cercando di inizializzare una variabile di tipo int con un valore double. Poiché l'inizializzazione con liste non consente la perdita di informazioni, il compilatore rifiuta questa definizione. Per correggere questo errore, dobbiamo usare un valore intero per inizializzare i:

    int i = { 3 };
    
  3. Definizione (c):

    double stipendio = salario = 9999.99;
    

    Questa definizione è legale. Tuttavia, è importante notare che salario deve essere definita prima di questa linea di codice. Se salario non è stata definita in precedenza, il compilatore genererà un errore. Assumendo che salario sia stata definita come una variabile di tipo double, questa definizione è corretta.

  4. Definizione (d):

    int i = 3.14;
    

    Questa definizione è legale, ma il compilatore emetterà un avviso. Stiamo cercando di inizializzare una variabile di tipo int con un valore double. In questo caso, il valore 3.14 verrà troncato a 3 durante l'inizializzazione.

Analizziamo, adesso, il seguente codice. Quali sono i tipi e i valori iniziali delle variabili global_str, global_int, local_int e local_str?

std::string global_str;
int global_int;
int main()
{
    int local_int;
    std::string local_str;
}

Andiamo per ordine:

  • global_str è una variabile globale di tipo std::string. Poiché è definita fuori da qualsiasi funzione, viene inizializzata alla stringa vuota.
  • global_int è una variabile globale di tipo int. Essendo anch'essa definita fuori da qualsiasi funzione, viene inizializzata a 0.
  • local_int è una variabile locale di tipo int, definita all'interno della funzione main(). Non è inizializzata esplicitamente, quindi il suo valore è indefinito.
  • local_str è una variabile locale di tipo std::string, definita all'interno della funzione main(). Anche se non è inizializzata esplicitamente, la classe std::string fornisce un valore predefinito, quindi local_str viene inizializzata alla stringa vuota.

Dichiarazioni e Definizioni di Variabili

Per consentire ai programmi di essere suddivisi in parti logiche, C++ supporta ciò che è comunemente noto come compilazione separata. La compilazione separata ci permette di dividere i nostri programmi in diversi file, ognuno dei quali può essere compilato indipendentemente.

Quando separiamo un programma in più file, abbiamo bisogno di un modo per condividere il codice tra quei file. Per esempio, il codice definito in un file potrebbe aver bisogno di usare una variabile definita in un altro file. Come esempio concreto, consideriamo std::cout e std::cin. Questi sono oggetti definiti da qualche parte nella libreria standard, eppure i nostri programmi possono usare questi oggetti.

Per supportare la compilazione separata, C++ distingue tra dichiarazioni e definizioni. Una dichiarazione rende un nome noto al programma. Un file che vuole usare un nome definito altrove include una dichiarazione per quel nome. Una definizione crea l'entità associata.

Una dichiarazione di variabile specifica il tipo e il nome di una variabile. Una definizione di variabile è una dichiarazione. Oltre a specificare il nome e il tipo, una definizione alloca anche memoria e può fornire alla variabile un valore iniziale.

Per ottenere una dichiarazione che non è anche una definizione, dobbiamo aggiungere la parola chiave extern e possiamo anche non fornire un inizializzatore esplicito:

extern int i; // dichiara ma non definisce i
int j;       // dichiara e definisce j

Qualsiasi dichiarazione che include un inizializzatore esplicito è una definizione. Possiamo fornire un inizializzatore su una variabile definita come extern, ma così facendo sovrascriviamo extern. Un extern che ha un inizializzatore è una definizione:

// Nonostante la presenza di extern,
// l'inizializzatore rende questa una definizione
extern double pi = 3.1416;

È un errore fornire un inizializzatore su un extern dentro una funzione.

Consiglio

Una variabile va definita una e una sola volta

Una variabile deve essere definita esattamente una volta in ogni programma. Tuttavia, può essere dichiarata più volte.

La distinzione tra una dichiarazione e una definizione può sembrare oscura a questo punto ma è effettivamente importante. Per usare una variabile in più di un file sono necessarie dichiarazioni che sono separate dalla definizione della variabile. Per usare la stessa variabile in più file, dobbiamo definire quella variabile in uno e un solo file. Gli altri file che usano quella variabile devono dichiarare, ma non definire, quella variabile.

Vedremo nelle prossime lezioni come funziona la compilazione separata e come usare le dichiarazioni e le definizioni per condividere il codice tra i file.

Definizione

Tipizzazione Statica e Controllo di Tipo

C++ è un linguaggio tipizzato staticamente, il che significa che i tipi sono controllati a tempo di compilazione. Il processo attraverso cui i tipi sono controllati è detto type checking.

Come abbiamo visto, il tipo di un oggetto vincola le operazioni che l'oggetto può eseguire. In C++, il compilatore controlla se le operazioni che scriviamo sono supportate dai tipi che usiamo. Se proviamo a fare cose che il tipo non supporta, il compilatore genera un messaggio di errore e non produce un file eseguibile.

Man mano che i nostri programmi diventano più complicati, vedremo che il controllo di tipo statico può aiutare a trovare bug. Tuttavia, una conseguenza del controllo statico è che il tipo di ogni entità che usiamo deve essere noto al compilatore. Come esempio, dobbiamo dichiarare il tipo di una variabile prima di poter usare quella variabile.

Esercizi

Analizziamo le seguenti righe di codice. Vediamo quali sono dichiarazioni e quali sono definizioni:

  • Riga 1:

    extern int ix = 1024;
    

    Questa è una definizione. Anche se include la parola chiave extern, la presenza dell'inizializzatore = 1024 indica che questa riga definisce la variabile ix e le assegna il valore 1024.

  • Riga 2:

    int iy;
    

    Questa è una definizione. La riga definisce la variabile iy di tipo int. Poiché non è fornito un inizializzatore, iy viene inizializzata a zero se è una variabile globale o statica, o rimane non inizializzata (con valore indefinito) se è una variabile locale.

  • Riga 3:

    extern int iz;
    

    Questa è una dichiarazione. La riga dichiara la variabile iz di tipo int senza definirla. Non viene allocata memoria per iz in questa riga; si presume che iz sia definita altrove nel programma.

Identificatori

Gli identificatori in C++ possono essere composti da lettere, cifre e il carattere underscore. Il linguaggio non impone limiti sulla lunghezza del nome. Gli identificatori devono iniziare con una lettera o un underscore. Gli identificatori sono sensibili alle maiuscole; lettere maiuscole e minuscole sono distinte:

// Definisce quattro variabili distinte
int qualchenome, qualcheNome, QualcheNome, QUALCHENOME;

Il linguaggio C++ riserva un insieme di nomi per il proprio uso.

Questi nomi riservati sono detti parole chiave o parole riservate. Le parole chiave non possono essere usate come nomi di variabili, funzioni, classi o altri identificatori definiti dall'utente. Tali parole sono riportate nella tabella che segue:

Parola chiave Descrizione
alignas specifica l'allineamento di un oggetto
alignof restituisce l'allineamento di un tipo
asm inserisce codice assembly
auto specifica la deduzione automatica del tipo
bool tipo booleano (vero/falso)
break esce da un ciclo o switch
case etichetta per un caso in uno switch
catch cattura eccezioni
char tipo carattere
char16_t tipo carattere a 16 bit
char32_t tipo carattere a 32 bit
class definisce una classe
const specifica che un valore non può essere modificato
constexpr specifica che un valore è costante a tempo di compilazione
const_cast esegue il cast di un tipo const
continue salta alla prossima iterazione di un ciclo
decltype deduce il tipo di un'espressione
default specifica il comportamento predefinito in uno switch o per un costruttore di default
delete dealloca memoria
do inizia un ciclo do-while
double tipo a doppia precisione
dynamic_cast esegue il cast di tipi polimorfici
else parte alternativa di un'istruzione if
enum definisce un tipo enumerato
explicit specifica che un costruttore non può essere usato per conversioni implicite
export (non più usato) esporta moduli
extern dichiara una variabile o funzione definita altrove
false valore booleano falso
float tipo a precisione singola
for inizia un ciclo for
friend dichiara una funzione o classe amica
goto salta a un'etichetta
if inizia un'istruzione condizionale
inline suggerisce l'inlining di una funzione
int tipo intero
long tipo intero lungo
mutable specifica che un membro di una classe può essere modificato anche se l'oggetto è const
namespace definisce uno spazio dei nomi
new alloca memoria
noexcept specifica che una funzione non lancia eccezioni
nullptr rappresenta un puntatore nullo
operator definisce un operatore
private specifica l'accesso privato in una classe
protected specifica l'accesso protetto in una classe
public specifica l'accesso pubblico in una classe
register suggerisce che una variabile sia memorizzata in un registro
reinterpret_cast esegue il cast di un tipo a un altro tipo arbitrario
return restituisce un valore da una funzione
short tipo intero corto
signed specifica un tipo intero con segno
sizeof restituisce la dimensione di un tipo o oggetto
static specifica durata di vita statica o legame interno
static_assert esegue un'asserzione a tempo di compilazione
static_cast esegue il cast di un tipo a un altro tipo in modo sicuro
struct definisce una struttura
switch inizia un'istruzione switch
template definisce una funzione o classe template
this puntatore all'oggetto corrente in una classe
thread_local specifica che una variabile è locale al thread
throw lancia un'eccezione
true valore booleano vero
try inizia un blocco di codice che può lanciare eccezioni
typedef definisce un alias di tipo
typeid restituisce informazioni sul tipo di un oggetto
typename specifica un nome di tipo in un template
union definisce una unione
unsigned specifica un tipo intero senza segno
using introduce uno spazio dei nomi o un alias di tipo
virtual specifica che una funzione è virtuale
void tipo vuoto
volatile specifica che una variabile può essere modificata in modi imprevisti
wchar_t tipo carattere largo
while inizia un ciclo while
Tabella 1: Parole chiave del C++

Inoltre, oltre alle parole chiave, lo standard permette di utilizzare dei nomi alternativi per alcuni operatori. Questi nomi alternativi sono riportati nella tabella che segue:

Operatore Nome Alternativo
&& and
& bitand
~ compl
!= not_eq
|| or
|= or_eq
^= xor_eq
&= and_eq
| bitor
! not
^ xor
Tabella 2: Nomi Alternativi per Alcuni Operatori C++

Tutti questi nomi non possono essere usati come identificatori.

Lo standard riserva anche un insieme di nomi per l'uso nella libreria standard. Gli identificatori che definiamo nei nostri programmi non possono contenere due underscore consecutivi, né un identificatore può iniziare con un underscore seguito immediatamente da una lettera maiuscola. Inoltre, gli identificatori definiti fuori da una funzione non possono iniziare con un underscore.

Convenzioni per i Nomi delle Variabili

Ci sono un certo numero di convenzioni generalmente accettate per nominare le variabili. Seguire queste convenzioni può migliorare la leggibilità di un programma.

  • Un identificatore dovrebbe dare qualche indicazione del suo significato.
  • I nomi delle variabili normalmente sono in minuscolo, ad esempio indice, non Indice o INDICE.
  • Come ArticoloVendita, le classi che definiamo di solito iniziano con una lettera maiuscola.
  • Gli identificatori con più parole dovrebbero distinguere visivamente ogni parola, per esempio, prestito_studente o prestitoStudente, non prestitostudente.
Consiglio

Consistenza nei Nomi

Le convenzioni di denominazione sono più utili quando seguite in modo coerente.

Scegli una convenzione e usala in tutto il programma.

Esercizi

Quali, tra questi identificatori, sono legali in C++? Per quelli che non lo sono, spiega perché.

  • Identificatore 1:

    int double = 3.14;
    

    Questo identificatore non è legale. La parola double è una parola chiave riservata in C++ e non può essere usata come nome di variabile.

  • Identificatore 2:

    int _ = 42;
    

    Questo identificatore è legale. Inizia con un underscore, che è consentito, e non viola alcuna delle regole di denominazione.

    Tuttavia, una variabile chiamata semplicemente _ può creare confusione. È generalmente una buona idea usare nomi più descrittivi per le variabili.

  • Identificatore 3:

    int catch-22 = 22;
    

    Questo identificatore non è legale. Il carattere - non è consentito negli identificatori. Gli identificatori possono contenere solo lettere, cifre e underscore.

  • Identificatore 4:

    int 1_or_2 = 1;
    

    Questo identificatore non è legale. Gli identificatori non possono iniziare con una cifra. Devono iniziare con una lettera o un underscore.

  • Identificatore 5:

    double Double = 3.14;
    

    Questo identificatore è legale. Anche se Double inizia con una lettera maiuscola, non è una parola chiave riservata in C++. Tuttavia, è importante notare che l'uso di nomi che differiscono solo per la capitalizzazione può portare a confusione.

Scope

In un particolare punto di un programma, ogni nome che è in uso si riferisce a un'entità specifica, che si tratti di una variabile, funzione, tipo, e così via. Tuttavia, un dato nome può essere riutilizzato per riferirsi a entità diverse in punti diversi del programma.

Lo scope o ambito di validità è una parte del programma in cui un nome ha un particolare significato. La maggior parte degli ambiti in C++ sono delimitati da parentesi graffe.

Lo stesso nome può riferirsi a entità diverse in ambiti diversi. I nomi sono visibili dal punto in cui sono dichiarati fino alla fine dell'ambito in cui appare la dichiarazione.

Come esempio, consideriamo il programma che segue:

#include <iostream>

int main()
{
    int somma = 0;

    // somma i valori da 1 a 10 inclusi
    for (int val = 1; val <= 10; ++val)
        // equivalente a somma = somma + val
        somma += val;

    std::cout << "Somma da 1 a 10 inclusi è "
              << somma << std::endl;

    return 0;
}

Questo programma definisce tre nomi: main, somma e val. Inoltre, usa il nome di namespace std, insieme a due nomi da quel namespace: cout e endl.

Il nome main è definito fuori da qualsiasi parentesi graffa. Il nome main, come la maggior parte dei nomi definiti fuori da una funzione, ha ambito globale. Una volta dichiarati, i nomi nell'ambito globale sono accessibili in tutto il programma. Il nome somma è definito nell'ambito del blocco che è il corpo della funzione main. È accessibile dal suo punto di dichiarazione per tutto il resto della funzione main ma non fuori di essa. La variabile somma ha ambito di blocco. Il nome val è definito nell'ambito dell'istruzione for. Può essere usato in quella istruzione ma non altrove in main.

Consiglio

Definire le variabili nel punto in cui vengono adoperate la prima volta

È generalmente una buona idea definire un oggetto vicino al punto in cui l'oggetto viene usato per la prima volta. Così facendo si migliora la leggibilità rendendo facile trovare la definizione della variabile. Più importante, è spesso più facile dare alla variabile un valore iniziale utile quando la variabile è definita vicino a dove viene usata per la prima volta.

Scope Annidati

Gli ambiti possono contenere altri ambiti. L'ambito contenuto (o annidato) è detto ambito interno, l'ambito contenitore è l'ambito esterno.

Una volta che un nome è stato dichiarato in un ambito, quel nome può essere usato dagli ambiti annidati in quell'ambito. I nomi dichiarati nell'ambito esterno possono anche essere ridefiniti in un ambito interno:

#include <iostream>

// Programma solo a scopo illustrativo:
// È cattivo stile per una funzione
// usare una variabile globale e
// anche definire una variabile locale con lo stesso nome

// riusato ha ambito globale
int riusato = 42;

int main()
{
    // unico ha ambito di blocco
    // è visibile solo dentro main
    int unico = 0;

    // output #1:
    // usa riusato globale;
    // stampa 42 0
    std::cout << riusato << " " << unico << std::endl;

    // nuovo oggetto locale chiamato riusato
    // nasconde riusato globale
    int riusato = 0;

    // output #2:
    // usa riusato locale;
    // stampa 0 0
    std::cout << riusato << " " << unico << std::endl;

    // output #3:
    // richiede esplicitamente riusato globale;
    // stampa 42 0
    std::cout << ::riusato << " " << unico << std::endl;

    return 0;
}

L'output numero 1 appare prima della definizione locale di riusato. Pertanto, questa istruzione di output usa il nome riusato che è definito nell'ambito globale. Questa istruzione stampa:

42 0

L'output numero 2 si verifica dopo la definizione locale di riusato. Il riusato locale è ora in ambito. Quindi, questa seconda istruzione di output usa l'oggetto locale chiamato riusato piuttosto che quello globale e stampa:

0 0

L'output numero 3 usa l'operatore di ambito per sovrascrivere le regole di scoping predefinite. L'ambito globale non ha nome. Quindi, quando l'operatore di ambito ha un lato sinistro vuoto, è una richiesta di recuperare il nome sul lato destro dall'ambito globale. Quindi, questa espressione usa riusato globale e stampa:

42 0
Nota

Evitare di riutilizzare i nomi delle variabili

È quasi sempre una cattiva idea definire una variabile locale con lo stesso nome di una variabile globale che la funzione usa o potrebbe usare.

Riusare i nomi delle variabili rende il codice più difficile da leggere e può portare a errori sottili.

Esercizi

  • Qual è il valore di j nel seguente programma?

    int i = 42;
    int main()
    {
        int i = 100;
        int j = i;
    }
    

    Il valore di j è 100. All'interno della funzione main, la variabile locale i (che è inizializzata a 100) nasconde la variabile globale i (che è inizializzata a 42). Quindi, quando j viene inizializzato con i, si riferisce alla variabile locale.

  • Analizziamo il seguente programma. Si tratta di codice valido? Se sì, qual è l'output?

    int i = 100, sum = 0;
    for (int i = 0; i != 10; ++i)
        sum += i;
    std::cout << i << " " << sum << std::endl;
    

    Si, questo è codice valido. L'output sarà:

    100 45
    

    Infatti, la variabile i definita nel ciclo for è una variabile locale che nasconde la variabile globale i all'interno del ciclo. Tuttavia, dopo il ciclo, quando si stampa i, si fa riferimento alla variabile globale, che ha il valore 100. La variabile sum accumula la somma dei numeri da 0 a 9, che è 45.