Variabili in C++
- 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
.
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.
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.
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.
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++.
-
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 definirevalore_input
in una dichiarazione separata:int valore_input; std::cin >> valore_input;
-
Definizione (b):
int i = { 3.14 };
Questa definizione non è legale. Stiamo cercando di inizializzare una variabile di tipo
int
con un valoredouble
. 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 inizializzarei
:int i = { 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. Sesalario
non è stata definita in precedenza, il compilatore genererà un errore. Assumendo chesalario
sia stata definita come una variabile di tipodouble
, questa definizione è corretta. -
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 valoredouble
. In questo caso, il valore3.14
verrà troncato a3
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 tipostd::string
. Poiché è definita fuori da qualsiasi funzione, viene inizializzata alla stringa vuota.global_int
è una variabile globale di tipoint
. Essendo anch'essa definita fuori da qualsiasi funzione, viene inizializzata a0
.local_int
è una variabile locale di tipoint
, definita all'interno della funzionemain()
. Non è inizializzata esplicitamente, quindi il suo valore è indefinito.local_str
è una variabile locale di tipostd::string
, definita all'interno della funzionemain()
. Anche se non è inizializzata esplicitamente, la classestd::string
fornisce un valore predefinito, quindilocal_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.
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.
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 variabileix
e le assegna il valore1024
. -
Riga 2:
int iy;
Questa è una definizione. La riga definisce la variabile
iy
di tipoint
. 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 tipoint
senza definirla. Non viene allocata memoria periz
in questa riga; si presume cheiz
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 |
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 |
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
, nonIndice
oINDICE
. - 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
oprestitoStudente
, nonprestitostudente
.
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
.
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
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 funzionemain
, la variabile localei
(che è inizializzata a100
) nasconde la variabile globalei
(che è inizializzata a42
). Quindi, quandoj
viene inizializzato coni
, 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 ciclofor
è una variabile locale che nasconde la variabile globalei
all'interno del ciclo. Tuttavia, dopo il ciclo, quando si stampai
, si fa riferimento alla variabile globale, che ha il valore100
. La variabilesum
accumula la somma dei numeri da0
a9
, che è45
.