Se dichiariamo una variabile globale static vogliamo che essa sia ``interna'' al file, cioè che non possa essere acceduta da un file che non sia quello contenente la dichiarazione. Vediamo i seguenti files di esempio:
// ex10_2_1.cpp int a; static int b; void main() { a = 1; b = 2; } // ex10_2_2.cpp #include <iostream.h> extern int a; extern int b; void stampa() { cout << a; cout << b; }
Il compilatore non torna nessun messaggio di errore: le variabili a
e
b
sono dichiarate extern in ex10_2_2.cpp
, per cui esse
verranno risolte in fase di collegamento; tuttavia, quando il linker va a
cercare la variabile b
nel file ex10_2_1.cpp
, trova che essa non
può essere utilizzata esternamente a tale file, per cui torna un messaggio
di errore del tipo ``undefined reference to `b''' (riferimento a `b' non
risolto). Per cui una variabile static va utilizzata quando si
ritiene che l'informazione in essa contenuta abbia significato solo ed
esclusivamente nel contesto del file nel quale è dichiarata.
Dichiarando una variabile static all'interno di una funzione, l'effetto è ben diverso: la variabile viene creata una ed una sola volta durante la vita del programma; è come se la variabile appartenesse alla funzione vera e propria, piuttosto che alla singola chiamata del processo. Naturalmente l'inizializzazione delle variabili statiche 10.2 avviene una sola volta, nella prima chiamata alla funzione: nelle successive chiamate la definizione della variabile viene ignorata dal compilatore. Vediamo un esempio:
// ex10_2_3.cpp #include <iostream.h> void f() { // il seguente statement viene valutato una solta volta static int contatore = 0; contatore++; int contatore_sbagliato = 0; contatore_sbagliato++; cout << "contatore:\t\t" << contatore << "\n"; cout << "contatore_sbagliato:\t" << contatore_sbagliato << "\n"; } void main() { f(); // prima chiamata for (int i = 0; i < 2; i++) f(); // chiamate successive }
output:
contatore: | 1 | |
contatore_sbagliato: | 1 | |
contatore: | 2 | |
contatore_sbagliato: | 1 | |
contatore: | 3 | |
contatore_sbagliato: | 1 |
La differenza tra una variabile statica ed una locale in una funzione è
evidente: la inizializzazione di contatore_sbagliato
(int)
avviene tutte le volte che la funzione f()
viene chiamata, mentre
contatore
)static int viene inizializzata una ed una sola
volta, nella prima chiamata della funzione. In questa maniera possiamo, come
nel nostro esempio, avere una variabile che porti il conto delle chiamate di
una funzione. Le variabili statiche nelle funzioni servono dunque a condivere
un dato tra tutte le chiamate della stessa funzione; naturalmente avremmo
potuto ottenere lo stesso risultato con una variabile globale:
// ex10_2_4.cpp #include <iostream.h> int contatore; void f() { contatore++; cout << "contatore: " << contatore << "\n"; } void main() { contatore = 0; f(); for (int i = 0; i < 2; i++) f(); }
output:
contatore: 1
contatore: 2
contatore: 3
L'inconveniente è notevole: se dichiariamo una variabile globale, essa è condivisibile da qualunque funzione del nostro programma, mentre noi vogliamo che la nostra variabile sia propria della funzione, ossia possa perdere coerenza se utilizzata impropriamente.
Il concetto di variabile static in una classe, infine, è del tutto diverso da quello delle variabili statiche globali e locali a funzioni: una variabile statica in una classe è propria della classe stessa, non delle singole istanze create nel programma. Non è un concetto semplice; fino ad ora abbiamo sempre visto una classe come una dichiarazione, la quale viene poi utilizzata per creare degli oggetti, ciascuno dei quali ha i propri campi dati del tutto indipendenti:
// ex10_2_5.cpp #include <iostream.h> class A { int i; public: A () { i = 0; } A& f() { i++; return *this; } // permette la concatenazione void stampa() { cout << i << "\n"; } }; void main() { A x, y; x.f().f().f(); x.stampa(); y.f(); y.stampa(); }
output:
3
1
I campi dati privati x.i
e y.i
corrispondono a due variabili
intere del tutto indipendenti tra di loro, separate sia a livello concettuale
che logico (cioè esse risiedono in due zone di memoria le quali,
teoricamente, non hanno alcun legame tra di esse); tuttavia, entrambe le
variabili appartengono ad oggetti della medesima classe. Se vogliamo che un
campo dati appartenga esso stesso a nessun oggetto in particolare, bensì alla classe medesima, allora dobbiamo dichiararlo static. Ovviamente
l'inizializzazione non dovrà avvenire nel costruttore o in una
qualunque delle funzioni membro, ma a livello globale, tramite la sintassi che
vediamo nell'esempio che segue.
// ex10_2_6.cpp #include <iostream.h> class A { static int i; public: A& f() { i++; return *this; } // permette la concatenazione void stampa() { cout << i << "\n"; } }; // inizializzazione della variabile static i int A::i = 0; void main() { /* corpo principale identico a ex10_2_5.cpp */ }
output:
3
4
La variabile statica i
viene inizializzata a livello globale con il
valore 0
; ogni volta che la funzione membro f()
viene chiamata,
i
viene incrementata di una unità: siccome essa appartiene alla
classe A
, non importa se la chiamata avviene su x
o su y
,
il risultato sarà lo stesso. Proprio in quanto le variabili statiche non
appartengono a nessun oggetto in particolare, esse possono essere modificate
anche senza fare riferimento ad un particolare oggetto classe allocato.
// ex10_2_7.cpp #include <iostream.h> class B { public: static int s; }; int B::s = 0; void main() { // nessun oggetto di tipo B e` stato creato B::s = 5; cout << B::s << "\n"; }
output:
5
Si capisce allora come abbia senso definire all'interno di una classe anche
funzioni static; esse, come le variabili, appartengono alla classe
stessa e non alle singole istanze via via create. Naturalmente le funzioni
statiche non possono che accedere membri statici (attenzione: abbiamo già
visto come le funzioni non statiche possano accedere membri statici), per esse
non è cioè definito l'oggetto *
this. Vediamo un semplice
esempio:
// ex10_2_8.cpp #include <iostream.h> class C { static int s; int ns; public: C() { ns = 0; } void stampans() { cout << "s: " << s << "\n"; // ok. membro statico cout << "ns: " << ns << "\n"; // ok. membro non statico } static void stampas(); }; int C::s = 0; void C::stampas() { cout << "s: " << s << "\n"; // ok. membro statico // cout << "ns: " << ns << "\n"; // NO: membro non statico } void main() { C x; C::stampas(); // ok. funzione statica // C::stampans(); // NO: funzione non statica x.stampas(); // ok. funzione statica x.stampans(); // ok. funzione non statica }
La classe C
contiene due campi dati interi, uno statico e uno non
statico, e due funzioni di stampa, anch'esse una statica e una non
statica. Come abbiamo detto, la funzione stampas()
non può
accedere i campi non statici, in quanto l'oggetto *
this non è
definito; ricordiamo che all'interno di una funzione membro quando una
variabile membro viene acceduta, è automaticamente premessa ad essa
l'espressione this->
. Tuttavia, mentre dal corpo principale possiamo
effettuare la chiamata a stampas()
, non possiamo fare lo stesso
ovviamente per stampans()
, la quale è istanziata singolarmente per
ogni oggetto creato. Si noti che le funzioni statiche non possono essere
definite all'interno della classe.
Utilizzando membri statici possiamo, ad esempio, costruire un contatore per gli oggetti creati:
// ex10_2_9.cpp #include <iostream.h> class D { static int contatore; public: D() { contatore++; } static int n_oggetti(); ~D() { contatore--; } }; int D::contatore = 0; int D::n_oggetti() { return contatore; } void main() { D* array = new D[5]; cout << D::n_oggetti() << "\n"; D* d = new D; cout << D::n_oggetti() << "\n"; delete[] array; cout << D::n_oggetti() << "\n"; }
output:
5
6
1
Si tratta di una tecnica molto semplice che può essere utilizzata in fase di debug di un programma, in modo tale da avere sempre sotto controllo il numero di oggetti che si allocano. Un altro esempio potrebbe essere il seguente:
// ex10_2_10.cpp #include <iostream.h> class Aspirapolvere { static int nds; // numero di serie /* tanti campi dati */ public: Aspirapolvere() { /* tante operazioni */ } static void nuovo(int n = 0); static int totale(); static void distruggi(int n); ~Aspirapolvere() { /* ... */ } }; int Aspirapolvere::nds = 0; void Aspirapolvere::nuovo (int n) { if (n >= 0) nds += n; } int Aspirapolvere::totale () { return nds; } void Aspirapolvere::distruggi (int n) { if (n >= 0) nds -= n; if (nds < 0) nds = 0; } void main() { Aspirapolvere::nuovo (5); cout << Aspirapolvere::totale() << "\n"; // 5 Aspirapolvere::nuovo (-1); // nessun effetto Aspirapolvere::distruggi (2); cout << Aspirapolvere::totale() << "\n"; // 3 Aspirapolvere::distruggi (100); cout << Aspirapolvere::totale() << "\n"; // 0 }
Si noti come l'intero statico nds
non possa assumere un valore
qualunque, ma resti comunque positivo; infatti è ben difficile che si riesca
a produrre aspirapolveri negativi.
f()
(una seconda funzione non
statica) viene chiamata;
classeXXX
, ove al posto di XXX
ci sia il numero di oggetti
instanziati di tale classe