Dal C al C++/Utilizzo basilare di librerie/Dichiarazioni e definizioni
Le variabili riferimento
modificaNel linguaggio C, si possono definire variabili che non sono puntatori, come in "int a;", oppure variabili che sono puntatori a oggetti che non sono puntatori, come in "int * a;", oppure variabili che sono puntatori a oggetti che sono puntatori a oggetti, i quali non sono puntatori, come in "int * * a;", e così via.
In C++ rimangono tutte queste possibilità, ma si aggiunge una nuova tipologia di variabile, il "riferimento". Consideriamo le seguenti righe, di cui le prime due sono valide anche in C:
int a = 34;
int * b = & a;
int & c = a;
La prima riga definisce una variabile di nome "a", di tipo "int", inizializzata al valore intero "34". La seconda riga definisce una variabile di nome "b", di tipo puntatore a "int", inizializzata all'indirizzo dell'oggetto rappresentato dalla variabile "a". La terza riga, non valida in linguaggio C, definisce una variabile di nome "c", di tipo riferimento a "int", inizializzata all'oggetto rappresentato dalla variabile "a".
La variabile "a" rappresenta un oggetto, nel senso che dichiarando la variabile "a" si alloca un oggetto di tipo "int" contenente il valore 34, e anche nel senso che quando in seguito si usa la variabile "a" in un punto in cui è richiesta un'espressione, tale variabile costituisce un'espressione avente come valore il valore dell'oggetto associato alla variabile.
Anche la variabile "b" rappresenta un oggetto, che questa volta è un puntatore. Tale puntatore viene allocato quando si dichiara la variabile "b", e ha un valore che viene utilizzato nelle espressioni che usano la variabile "b".
La variabile "c", invece, non rappresenta un proprio oggetto, ma rappresenta lo stesso oggetto rappresentato dalla variabile "a". Tale oggetto coincide anche con quello rappresentato dall'espressione "* b". Dichiarando la variabile "c", non è stato allocato nessun oggetto, ma è stato dato un nuovo nome a un oggetto già esistente. Tale variabile, quando viene usata in un'espressione, ha come valore il valore dell'oggetto a cui fa riferimento.
In seguito, si può usare la variabile "c" con lo stesso significato della variabile "a". Tale fatto si esprime dicendo che "c" è un alias di "a". Per esempio, eseguendo il seguente codice.
c = 7;
cout << a;
Viene stampato il numero "7", in quanto l'oggetto riferito da "c" è lo stesso oggetto riferito da "a".
Tale associazione è permanente per la vita della variabile. Come non si può fare in modo che "a" rappresenti un oggetto diverso da quello iniziale, così anche "c", da quando viene inizializzata, è vincolata a rappresentare sempre lo stesso oggetto.
Si considerino le seguenti righe, supposte all'interno di una funzione:
int a2;
int * b2;
int & c2; // Illegale
Le prime due continuano a definire variabili a cui corrispondono oggetti, anche se tali oggetti non sono inizializzati e quindi hanno valori indefiniti. La terza riga è invece illegale sia in C che in C++. Infatti, tenterebbe di dichiarare una variabile che costituisca un riferimento alternativo a un oggetto, ma non specifica a quale oggetto. Siccome il collegamento di una variabile riferimento al suo oggetto può avvenire solo all'atto della definizione della variabile, tale variabile rimarrebbe per sempre non legata a un oggetto, e quindi completamente inutile. Pertanto, il linguaggio considera illegale dichiarare variabili riferimento senza inizializzarle.
L'uso principale delle variabili riferimento, è però per dichiarare parametri di funzione. Si consideri il seguente programma:
#include <iostream>
using namespace std;
double doppio1(double a) { return a * 2; }
double doppio2(double * a) { return *a * 2; }
double doppio3(double & a) { return a * 2; }
int main() {
double x = 1;
cout << doppio1(x);
cout << doppio2(&x);
cout << doppio3(x);
}
Le funzioni "doppio1", "doppio2", e "doppio3" sono sostanzialmente equivalenti, tanto che il programma stamperà "222". Tali funzioni moltiplicano per due il valore della variabile "x", e rendono il risultato della moltiplicazione. In realtà, mentre la funzione "doppio1" prende una copia del valore di "x", le funzioni "doppio2" e "doppio3" prendono esattamente l'oggetto rappresentato dalla variabile "x".
Per comprendere tale differenza, si consideri il seguente programma:
#include <iostream>
using namespace std;
void raddoppia1(double a) { a *= 2; }
void raddoppia2(double * a) { *a *= 2; }
void raddoppia3(double & a) { a *= 2; }
int main() {
double x = 1;
raddoppia1(x);
cout << x;
raddoppia2(&x);
cout << x;
raddoppia3(x);
cout << x;
}
Le funzioni "raddoppia1", "raddoppia2", e "raddoppia3" non sono equivalenti, tanto che il programma stamperà "124".
Chiamando la funzione "raddoppia1", il valore dell'oggetto riferito dalla variabile "x" viene copiato sullo stack. All'interno della funzione, tale valore viene riferito dal parametro "a". La variabile "a" rappresenta un oggetto distinto da quello rappresentato dalla variabile "x", anche se inizialmente ha lo stesso valore. Il corpo della funzione modifica tale oggetto allocato sullo stack, e, quando la funzione termina, l'oggetto modificato viene deallocato dallo stack, senza che ci siano effetti sul mondo esterno. Quindi l'oggetto rappresentato dalla variabile "x" continua a valere "1".
Chiamando la funzione "raddoppia2", è l'indirizzo dell'oggetto riferito dalla variabile "x" ad essere copiato sullo stack. All'interno della funzione, tale valore viene riferito dal parametro "a". Il corpo della funzione modifica l'oggetto puntato da tale valore allocato sullo stack, che è poi lo stesso oggetto associato a "x", e, quando la funzione termina, l'oggetto puntatore viene deallocato dallo stack. Quindi l'oggetto rappresentato dalla variabile "x" è stato raddoppiato, passando dal valore "1" al valore "2".
Chiamando la funzione "raddoppia3", si comunica alla funzione che il parametro "a" dovrà rappresentare lo stesso oggetto rappresentato dalla variabile "x". Questa comunicazione può avvenire copiando sullo stack l'indirizzo dell'oggetto rappresentato dalla variabile "x". Il corpo della funzione modifica l'oggetto puntato da tale valore allocato sullo stack, e, quando la funzione termina, l'oggetto puntatore viene deallocato dallo stack. Siccome la variabile "a" è solo un alias della variabile "x", le modifiche apportate ad "a" risultano applicate anche a "x". Quindi l'oggetto rappresentato dalla variabile "x" è stato raddoppiato, passando dal valore "2" al valore "4".
Come si vede, il corpo della funzione "raddoppia3" è uguale a quello della funzione "raddoppia1", ma il comportamento è uguale a quello della funzione "raddoppia2". Questi fatti si esprimono con la frase: I riferimenti hanno la sintassi dei valori, ma la stessa semantica dei puntatori. Questa frase significa: I riferimenti hanno lo stesso aspetto dei valori, ma lo stesso comportamento dei puntatori.
Il modificatore "const"
modificaIn C++ esiste la parola-chiave "const" (abbreviazione della parola inglese "constant", cioè "costante"), che serve a modificare la dichiarazione di variabili e funzioni. Ecco alcuni esempi di utilizzo:
const int x1 = 0;
const int * x2;
int * const x3 = 0;
const int * const x4 = 0;
La variabile "x1" rappresenta un oggetto di tipo "int", e non può essere usata per modificare tale oggetto.
La variabile "x2" rappresenta un oggetto di tipo puntatore a "int". Tale variabile può essere usata per modificare l'oggetto puntatore, rendendolo nullo o facendolo puntare a un altro oggetto, ma non può essere usata per modificare l'oggetto puntato, qualunque esso sia.
La variabile "x3" rappresenta un oggetto di tipo puntatore a "int". Tale variabile non può essere usata per modificare l'oggetto puntatore, che quindi dovrà sempre puntare allo stesso oggetto, ma può essere usata per modificare l'oggetto puntato.
Infine, anche la variabile "x4" rappresenta un oggetto di tipo puntatore a "int". Tale variabile non può essere usata per modificare l'oggetto puntatore, che quindi dovrà sempre puntare allo stesso oggetto, e non può essere usata neanche per modificare l'oggetto puntato.
L'inizializzazione di "x1", di "x3", e di "x4" è obbligatoria, in quanto si tratta di variabili non modificabili, e quindi non gli si potrà assegnare un valore in seguito. La variabile "x2" invece può non essere inizializzata, in quanto, essendo modificabile, le si potrà assegnare un valore in seguito. Quello che non sarà modificabile è l'oggetto puntato da "x2", qualunque esso sia.
Ecco quali sono gli utilizzi delle precedenti variabili che il linguaggio ammette e quali quelli che vieta:
x1 = 1; // Illegale
x2 = 0; // OK
x3 = 0; // Illegale
x4 = 0; // Illegale
Solo la variabile "x2" è "non-const", e quindi solo lei è modificabile.
*x1 = 1; // Illegale
*x2 = 1; // Illegale
*x3 = 1; // OK
*x4 = 1; // Illegale
La variabile "x1" non è un puntatore, e quindi è ovviamente illegale dereferenziarla come se lo fosse. Le variabili "x2", "x3" e "x4" sono dei puntatori, quindi si può tentare di assegnare un valore all'oggetto puntato. Tuttavia "x2" e "x4" hanno il tipo di puntatori a "const", e quindi è illegale usare tali puntatori per modificare l'oggetto puntato.
Si noti che il modificatore "const" non si applica agli oggetti, ma alle variabili, come mostrato dal seguente esempio:
int main() {
int x = 1;
int * p1 = &x;
const int * p2 = &x;
x = 2;
*p1 = 3;
*p2 = 4; // Illegale
}
Sia la variabile "p1" che la variabile "p2" sono puntatori inizializzati a puntare allo stesso oggetto, associato anche alla variable "x". Tale oggetto in sé è modificabile, infatti lo si può modificare sia assegnandogli il valore "2" tramite la variabile "x", sia assegnandogli il valore "3" tramite il puntatore "p1". Tuttavia, il tentativo di assegnargli il valore "4" tramite il puntatore "p2" è un errore sintattico, in quanto la variabile "p2" ha il tipo di puntatore a oggetto non modificabile.
Il modificatore "const" si può applicare anche ai parametri di qualunque funzione, e alle funzioni membro stesse. Per esempio:
void f1(const int& x5) {
x5 = 1; // Illegale
}
class C {
int x6;
void f2() const {
x6 = 2; // Illegale
}
};
Il parametro "x5" è di tipo riferimento a un "int" non modificabile, e quindi è illegale assegnare a tale variabile.
Un riferimento non può mai essere reindirizzato a un altro oggetto, pertanto la seguente riga è erronea:
void f1(int & const x5) { } // Illegale
Nella classe "C", la funzione membro "f2" non prende argomenti, ma presenta la parola "const" appena prima del corpo. Tale specificazione asserisce che quella funzione non può modificare le variabili membro dell'oggetto su cui è chiamata. In questo caso la parola "const" andrebbe letta come "che non modifica l'oggetto".
Gli scopi del modificatore "const" sono i seguenti:
- Rendere il codice autodocumentante, e quindi più comprensibile. Il programmatore potrebbe scrivere un commento come "questa routine si limita a leggere l'oggetto", ma tale tecnica è più verbosa e non standard.
- Evidenziare errori di programmazione. Se una variabile non deve essere modificata e invece lo è, si tratta di un errore logico che verrà segnalato dal compilatore.
- Rendere il codice più efficiente. Se il compilatore sa che una variabile ha un valore costante, può propagare il suo valore ovunque è usata la variabile. Per variabili locali, questo viene già fatto dai compilatori ottimizzanti, ma normalmente non viene fatto per le variabili globali. Questo fatto rende inutile e sconsigliato l'uso delle macro del preprocessore (la direttiva "#define") per definire costanti.
L'Overloading o Sovraccaricamento
modificaUn concetto introdotto dal C++ è il sovraccaricamento (in inglese "overload"), che si applica alle funzioni e agli operatori.
L'Overloading delle funzioni
modificaIl seguente programma è errato in C, ma valido in C++:
double f(int x) { return (double)x; }
double f(float x) { return (double)x * 3; }
int main() {
double a = f(7);
double b = f(7.f);
}
Alla variabile "a" viene assegnato il valore "7.0", mentre alla variabile "b" viene assegnato il valore "21.0".
Infatti, in linguaggio C, la firma (in inglese, "signature") di una funzione è costituita dal semplice nome: se il compilatore incontra due funzioni con lo stesso nome ma parametri non dello stesso tipo, le considera uguali. E normalmente genera un warning per una redifinizione. Invece, in C++, la firma di una funzione è costituita dal nome insieme alla lista dei tipi dei parametri. I nomi dei parametri e il tipo di ritorno non fanno parte della firma.
Se il compilatore incontra due funzioni aventi la stessa firma, le considera uguali, mentre se la firma differisce le considera funzioni distinte.
Quando il compilatore C++ analizza una chiamata di funzione, non si limita a cercare se è già stata definita una funzione con lo stesso nome, ma una con la stessa firma, e, in caso affermativo, la chiamata viene fatta puntare a tale definizione.
Nell'esempio, la prima istruzione della funzione "main" chiama una funzione avente come firma "f(int)", mentre la seconda una funzione avente come firma "f(float)". Si tratta di due funzioni distinte, e quindi il comportamento in fase di esecuzione sarà distinto.
Il sovraccaricamento può dare origine ad ambiguità, come nel seguente programma errato:
void f(double x) { }
void g(float x) { }
void g(double x) { }
int main() {
f(7);
g(7); // Illegale
}
La prima istruzione chiama la funzione "f" passandole un valore di tipo "int". Non esistono funzioni di nome "f" e con un parametro di tipo "int", ma ne esiste una con nome "f" e un parametro di tipo "double", ed esiste una conversione standard da "int" a "double". Pertanto il compilatore associa tale definizione di funzione alla chiamata "f(7)".
La seconda istruzione chiama la funzione "g" passandole un valore di tipo "int". Non esistono funzioni di nome "g" e con un parametro di tipo "int", ma ne esistono due con nome "g", una delle quali con un parametro di tipo "float" e l'altra con un parametro di tipo "double". Esistono anche le conversioni standard da "int" a "float" e da "int" a "double". Siccome non c'è una funzione che corrisponda esattamente e c'è più di una funzione che corrisponde in modo approssimato, il compilatore non sa decidere quale delle due funzioni chiamare, e genera un errore.
Per evitare tale errore, in presenza di più funzioni sovraccaricate, si deve passare una parametro avente uno dei tipi per cui esiste la funzione sovraccaricata. Per esempio, quell'istruzione avrebbe dovuto essere una delle due seguenti:
g(7.f); // Chiama g(float)
g(7.); // Chiama g(double)
oppure una delle due seguenti:
g((float)7); // Chiama g(float)
g((double)7); // Chiama g(double)
L' Overloading degli operatori
modificaIn C gli operatori hanno un significato prestabilito e immutabile.
In C++, questo è vero per i tipi fondamentali, ma è possibile definire nuovi operatori per i tipi definiti dall'utente.
Si consideri il seguente programma:
#include <iostream>
#include <complex>
using namespace std;
int main() {
complex<double> c1, c2;
cout << (c1 + c2);
}
Nell'ultima riga si usano l'operatore di scorrimento a sinistra "<<" e l'operatore di somma "+".
La libreria standard ha ridefinito tali operatori per la classe "complex<double>", pur mantenendo il vecchio significato per i tipi predefiniti nel linguaggio.
Quando il compilatore incontra il segno "+" valuta il tipo dell'espressione alla sua sinistra e il tipo dell'espressione alla sua destra. In questo caso si tratta di due oggetti di tipo "complex<double>". Siccome nella libreria standard è definito un operatore di nome "+" e avente parametri di tale tipo, a fronte di questo segno viene generata una chiamata a tale operatore.
Come si possano definire operatori sovraccaricati è un argomento avanzato, descritto in un altro capitolo.
L'inizializzazione degli oggetti
modificaSia in C che in C++, si può scrivere la seguente porzione di codice:
int a = 1; // Variabile globale inizializzata esplicitamente
static int b = 2; // Variabile di modulo inizializzata esplicitamente
int c; // Variabile globale inizializzata implicitamente
static int d; // Variabile di modulo inizializzata implicitamente
int funz() {
int e = 1; // Variabile locale temporanea inizializzata esplicitamente
static int f = 2; // Variabile locale statica inizializzata esplicitamente
int g; // Variabile locale temporanea non inizializzata
static int h; // Variabile locale statica inizializzata implicitamente
}
In questo codice, vengono allocate staticamente le variabili "a", "b", "c", "d", "f", "h", mentre vengono allocate sullo stack le variabili "e" e "g".
Le variabili allocate staticamente sono sempre inizializzate, al valore indicato dopo il segno "=", se si esegue un'inizializzazione esplicita, al valore zero binario altrimenti. Lo zero binario coincide con il valore "0" per numeri interi, al carattere '\0' per i caratteri, al numero a virgola mobile "0.0" per i numeri a virgola mobile, al puntatore nullo per i puntatori. L'inizializzazione delle variabili statiche avviene prima che inizi l'esecuzione della funzione "main".
La variabile "e" viene inizializzata esplicitamente quando viene creata, cioè ogni volta che inizia l'esecuzione della funzione "funz".
La variabile "g" è l'unica delle otto variabili che non viene inizializzata, né implicitamente né esplicitamente. L'oggetto rappresentato da tale variabile, appena dopo che è stato creato, cioè all'inizio della routine "funz", ha un valore indefinito, ossia il suo utilizzo come r-value ha effetto impredicibile. Trattandosi di un numero intero, siccome nelle tipiche architetture hardware qualunque combinazione di bit rappresenta un intero valido, possiamo essere ragionevolmente sicuri che usando il valore della variabile "g" usiamo un intero valido, anche se non sappiamo quale. Se fosse però stata una variabile di tipo "float" o di tipo puntatore, l'oggetto rappresentato dalla variabile potrebbe contenere una sequenza di bit che non rappresenta, rispettivamente, un valido numero a virgola mobile, o un puntatore a un'area di memoria valida.
La dichiarazione di variabili non inizializzate è pericolosa e in generale andrebbe evitata. Alcuni hanno proposto di vietarle in C++, ma siccome questo avrebbe reso non validi molti vecchi programmi, sono state mantenute. Inoltre, in alcuni casi l'inizializzazione di una variabile introduce una minima inefficienza. Per fortuna, molti compilatori emettono un avvertimento quando si usa come r-value una variabile non inizializzata e a cui non è ancora stato assegnato un valore.
Tutto quanto detto finora è valido sia in C che in C++.
Il C++ introduce una nuova sintassi per inizializzare oggetti. Ecco un esempio:
int a(1); // Variabile globale inizializzata esplicitamente
static int b(2); // Variabile di modulo inizializzata esplicitamente
int funz() {
int e(1); // Variabile locale temporanea inizializzata esplicitamente
static int f(2); // Variabile locale statica inizializzata esplicitamente
}
Invece di scrivere "= espressione", si scrive "( espressione )". L'effetto è identico, cambia solo la sintassi, che somiglia a quella della chiamata di funzione. La motivazione di questa nuova sintassi sta nel seguente programma d'esempio:
#include <complex>
#include <iostream>
using namespace std;
complex<double> a(3, 4);
int main() {
cout << a;
}
Lanciando questo programma viene stampato sulla console "(3,4)", che è la rappresentazione testuale del numero complesso rappresentato dalla variabile "a".
Per inizializzare un numero complesso è necessario o un altro numero complesso, o due numeri reali. L'operatore "=" richiede una sola espressione alla sua destra, mentre ne sono necessari due. Per risolvere questo problema, è stata adottata la notazione funzionale, che consente un numero arbitrario di espressioni, separate da virgole.
Ovviamente il numero di parametri dell'inizializzatore dipende dal tipo dell'oggetto da inizializzare. Le due righe seguenti sono errate:
int a(3, 4); // Illegale: un parametro di troppo
complex<double> b(3, 4, 5); // Illegale: un parametro di troppo
Le funzioni possono avere più di un parametro, ma possono anche non averne nessuno. Che cosa significano le due espressioni seguenti?
int a();
complex<double> b();
Purtroppo non sono affatto dichiarazioni di variabili, ma prototipi di funzione, in quanto lo erano già in linguaggio C. La funzione "a" non prende parametri e rende un "int". La funzione "b" non prende parametri e rende un "complex<double>".
Quindi, l'inizializzazione con la notazione funzionale richiede che l'inizializzatore utilizzi almeno un parametro.
Ma che cosa stampa questo programma?
#include <complex>
#include <iostream>
using namespace std;
int main() {
int a;
complex<double> b;
cout << a << " " << b;
}
Verrà stampato il valore di un numero intero, seguito da uno spazio, seguito dalla rappresentazione di un numero complesso. Ma quali sono questi valori?
La variabile "a" non è stata inizializzata, quindi come avviene in linguaggio C, il suo valore è impredicibile. Come si suol dire, contiene spazzatura (in inglese "garbage"), nel senso che contiene il valore che è stato lasciato in quella cella di memoria l'ultima volta che è stata usata.
Invece la variabile "b" contiene sicuramente il numero complesso in cui sia la parte reale che la parte immaginaria sono zero, e quindi, dopo lo spazio, verrà stampato "(0,0)".
Ma chi ha inizializzato la variabile "b"? È stata la classe "complex<double>" stessa, che ha la funzionalità di inizializzare al valore di default "(0,0)" ogni nuovo oggetto, in assenza di una inizializzazione esplicita.
Come vedremo, questa funzionalità è tipica di gran parte delle classi della libreria standard, ed è tipica anche delle classi di altre librerie, anche se il linguaggio non forza tale comportamento.
Per fare altri esempi, il seguente programma stampa sicuramente "[0,]":
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
vector<int> v;
string s;
cout << "[" << v.size() << "," << s << "]";
}
L'espressione "v.size()" genera il numero di elementi contenuti nel vettore "v"; il fatto che venga stampato "0" è segno del fatto che la variabile "v" è stata inizializzata ad uno stato in cui non contiene nessun elemento.
Il fatto poi che venga stampato ",]" significa che la stringa s è stata inizializzata a stringa vuota.
L'allocazione degli oggetti
modificaNel linguaggio C, si possono allocare oggetti nei seguenti modi:
- Definendo variabili statiche, che sono quelle definite con il qualificatore "static" o definite all'esterno da qualunque funzione. Tali oggetti sono creati prima che inizi la funzione "main", e sono deallocati dopo che il main è terminato.
- Chiamando una funzione che richiede dei parametri. Per ogni parametro, viene allocato un oggetto sullo stack all'atto della chiamata di funzione; tale oggetto viene deallocato quando la funzione termina.
- Facendo entrare il flusso di controllo del programma in un blocco delimitato da parentesi graffe, che può essere un'intera funzione o una sua parte, all'inizio della quale sono definite delle variabili locali non precedute dal qualificatore "static". Per ogni variabile locale, viene allocato un oggetto sullo stack all'atto dell'entrata nel blocco; tale oggetto viene deallocato quando il controllo esce dal blocco.
- Implicitamente, all'interno di espressioni. Tali oggetti temporanei vengono creati durante la valutazione dell'espressione e distrutti quando è finita la valutazione dell'intera espressione.
- Quando si chiama la funzione di libreria "malloc". Tale funzione alloca un array di caratteri. Tale array viene deallocato chiamando la funzione di libreria "free".
Si noti che non occorre fare discorsi particolari per strutture e array, in quanto tali oggetti composti sono comunque allocati in uno dei suddetti casi.
In C++, vale tutto quanto detto sopra, ma con le seguenti aggiunte.
All'interno di un blocco racchiuso tra parentesi graffe, è possibile definire variabili locali non solo all'inizio del blocco, ma in qualunque punto si possa inserire un'istruzione. La variabile potrà essere utilizzata solo dopo il punto in cui è stata definita, fino alla chiusa del blocco più interno nel quale è stata definita. Vediamo un esempio:
void f() {
int a;
a = 0;
int b; // Valido
b = 0;
c = 0; // Illegale, perché precede la definizione
int c;
{
int d;
d = 0;
static int e;
}
d = 0; // Illegale, qui "d" è già stata deallocata
e = 0; // Illegale, qui "e" esiste ancora, ma non è più accessibile
}
All'interno di una struttura o di una classe, è possibile usare il qualificatore "static" per una variabile membro. Per esempio:
#include <iostream>
using namespace std;
struct C {
int a;
static int b;
};
int C::b;
int main() {
C c1;
c1.a = 1;
c1.b = 2;
C c2;
c2.a = 3;
c2.b = 4;
cout << c1.a << " " << c1.b << " " << c2.a << " " << c2.b;
}
Questo programma non stampa "1 2 3 4", come avrebbe fatto se la variabile membro "b" non fosse stata statica. Stampa invece "1 4 3 4", perché dopo che la variabile membro "b" ha ricevuto il valore "2" come membro della struttura "c1", riceve il valore "4" come membro della struttura "c2".
Dichiarare statica una variabile membro di una struttura o classe significa dichiarare una variabile per cui viene istanziato un oggetto una sola volta, qualunque sia il numero di oggetti che vengono creati in esecuzione (anche nessuno). Tale oggetto statico viene creato prima che sia chiamata la funzione "main", e viene distrutto dopo la fine della funzione "main".
Si noti che dopo la definizione della struttura C, c'è la seguente riga:
int C::b;
Questa è la "definizione" della variabile membro "b", che era stata solo "dichiarata" nella struttura C.
Questa distinzione tra il concetto di "dichiarazione" e quello di "definizione" esiste anche in linguaggio C. In quest'ultimo linguaggio è frequente, e in C++ un po' meno frequente, avere variabili globali accessibili da più unità di compilazione (che sono i file con estensione ".c").
Tutte le unità di compilazione che vogliono utilizzare una variabile globale la devono dichiarare, ma siccome ci deve essere una sola istanza condivisa di tale variabile, una sola unità di compilazione la deve definire. Le dichiarazioni si distinguono dalle definizioni in quanto precedute dalla parola-chiave "extern". Inoltre, tipicamente, le dichiarazioni sono contenute in file separati (cosiddetti "header", cioè "intestazioni"), inclusi nell'unità di compilazione con la direttiva "#include". Le definizioni sono invece poste direttamente nell'unità di compilazione. Se si omette di definire una variabile dichiarata e non utilizzata, tale variabile è semplicemente ignorata. Se invece si omette di definire una variabile dichiarata e utilizzata, non ci sono problemi solo in fase di compilazione, ma viene generato un errore in fase di collegamento (in inglese, "linking").
Una definizione di una struttura contiene le dichiarazioni delle sue variabili membro. Le variabili membro non statiche, non hanno bisogno di definizione, in quanto vengono istanziate quando si istanzia la struttura stessa. Invece, le variabili membro statiche devono essere definite analogamente alle variabili globali, in quanto si tratta di variabili che devono essere inserite dal linker nell'area statica.
Per definire una variabile membro statica, si usa una sintassi analoga a quella usata per definire una variabile globale, con la differenza che prima del nome della variabile si deve porre il nome della classe o struttura, separata dall'operatore "::".
L'operatore "::" non esiste in C. Si tratta dell'operatore di "risoluzione dell'ambito" (in inglese, "scope resolution"). Il suo fine è indicare al compilatore dove cercare il nome indicato alla sua destra. Alla sinistra di tale operatore ci può essere il nome di una struttura o classe, oppure il nome di un namespace (come "std"), oppure anche niente, come nella seguente istruzione.
::a = 3;
Questa istruzione assegna il valore 3 alla variabile globale di nome "a". L'operatore serve a indicare che tale il variabile non deve essere confusa con eventuali variabili locali o parametri di funzione con lo stesso nome. Se esiste una variabile globale e una variabile locale con lo stesso nome, e non si usa l'operatore di risoluzione dell'ambito, il compilatore intente che si intenda riferirsi alla variabile locale.