trad. italiana e adattamento a cura di Giacomo Grande
Inventare nomi è un'attività fondamentale nella programmazione, e quando un progetto diventa molto grande il numero di nomi puo' diventare opprimente.
Il C++ fornisce una gran quantità di controlli sulla creazione e visibiltà dei nomi, sulla loro posizione in memoria e sul linkage. La parola chiave static è stata sovraccaricata (di significato) in C prima ancora che la gente conoscesse il significato del termine "overload" e il C++ ha aggiunto anche un altro significato. Il concetto base per tutti gli usi che si fanno della parola static sembra essere "qualcosa che ricorda la sua posizione" (come l'elettricità statica), che si tratti della posizione fisica in memoria o della visibilità all'interno di un file. In questo capitolo impareremo come la parola chiave static controlla l'allocazione e la visibilità, vedremo un modo migliore per controllare l'accesso ai nomi, attraverso il concetto di namespace (spazio dei nomi) proprio del C++. Scopriremo anche come usare le funzioni scritte e compilate in C.
Quando creiamo una variabile locale all'interno di una funzione, il compilatore le alloca memoria sullo stack ogni volta che la funzione viene chiamata, spostando opportunamente in avanti lo stack pointer. Se c'è una sequenza di inizializzazione per la variabile, questa viene eseguita ad ogni chiamata della funzione. A volte, tuttavia, si vuole mantenere il valore di una variabile tra una chiamata e l'altra di una funzione. Questo lo si puo' ottenere utilizzando una variabile globale, ma questa sarebbe visibile a tutti e non solo alla funzione in esame. Il C e il C++ permettono di creare oggetti static all'interno di funzioni; l'allocazione di memoria per questi oggetti non avviene sullo stack, ma all'interno dell'area dati statica del programma. L'oggetto viene inizializzato una sola volta, la prima volta che la funzione viene chiamata, e poi mantiene il suo valore tra una chiamata e l'altra della funzione. Per esempio, la seguente funzione restituisce il carattere successivo nell'array ogni volta che la funzione viene chiamata:
La variabile static char* s mantiene il suo valore tra le chiamate a unChar( ) perchè la sua locazione di memoria non è parte dello spazio di stack della funzione, ma è dentro l'area statica del programma. Quando viene chiamata la funzione unChar( ) con un argomemto di tipo char *, all'argomento viene assegnato s e viene restituito il primo carattere dell'array. Tutte le chiamate successive alla funzione unChar(), fatte senza argomento, producono il valore di default zero per charArray, il che indica alla funzione che si stanno ancora estraendo caratteri dal valore precedentemente inizializzato di s. La funzione continua a produrre caratteri fino a quando non incontra il terminatore null dell'array di caratteri, momento in cui smette di incrementare il puntatore in modo da non sfondare il limite dell'array. Ma cosa succede se chiamiamo la funzione unChar() senza argomenti e senza preventivamente inizializzare il valore di s? Nella definizione di s potremmo fornire un valore iniziale,//: C10:VariabiliStaticheInFunzioni.cpp #include "../require.h" #include <iostream> using namespace std; char unChar(const char* charArray = 0) { static const char* s; if(charArray) { s = charArray; return *s; } else require(s, "s non-inizializzata"); if(*s == '\0') return 0; return *s++; } char* a = "abcdefghijklmnopqrstuvwxyz"; int main() { // unChar(); // require() fallisce unChar(a); // Inizializza s con il valore di a char c; while((c = unChar()) != 0) cout << c << endl; } ///:~
ma anche se non effettuiamo l'inizializzazione di una variabile statica di un tipo predefinito, il compilatore garantisce che questa variabile sarà inizializzata a zero (convertita ad un tipo appropriato) allo start-up del programma. Cosicchè, la prima volta che viene chiamata la funzione unChar(), s è zero. In questo caso, l'istruzione condizionale if(!s) lo proverebbe. L'inizializzazione di s fatta sopra è molto semplice, ma in generale l'inizializzazione di oggetti statici (come per tutti gli altri) puo' essere fatta con espressioni che coinvolgono costanti, variabili dichiarate precedentemente e funzioni. Bisogna essere consapevoli che la funzione di sopra è molto vulnerabile ai problemi del multithreading; ogni qualvolta si introducono funzioni con variabili statiche bisogna stare attenti al multithreading.static char* s = 0;
Gli oggetti statici di tipo X all'interno di f() possono essere inizializzati sia con una lista di argomenti passati al costruttore, sia con il costruttore di default. Questa costruzione avviene la prima volta che il controllo passa attraverso la definizione, e solo la prima volta.//: C10:OggettiStaticiInFunzioni.cpp #include <iostream> using namespace std; class X { int i; public: X(int ii = 0) : i(ii) {} // Default ~X() { cout << "X::~X()" << endl; } }; void f() { static X x1(47); static X x2; // Richiesto il costruttore di Default } int main() { f(); } ///:~
La
distruzione di oggetti statici, come per quelli ordinari, avviene nell'ordine
inverso rispetto all'inizializzazione. Tuttavia, solo gli oggetti costruiti
vengono distrutti. Fortunatamente i tools di sviluppo del C++ tengono traccia
dell'ordine di inizializzazione e degli oggetti che sono stati costruiti
(per i quali, cioè, è stato chiamato un costruttore). Gli oggetti globali
vengono sempre costruiti prima di entrare nel
main() e distrutti
all'uscita del main(), ma se una funzione che contiene un oggetto
statico locale non viene mai chiamata, il costruttore di tale oggetto non
viene mai eseguito e quindi neanche il distruttore sarà eseguito. Per
esempio,
In Obj la variabile char c funge da identificatore, in modo tale che il costruttore e il distruttore possono stampare informazioni riguardo all'oggetto su cui stanno agendo. La variabile Obj a è un oggetto globale, per cui il suo costruttore viene sempre chiamato prima di entrare nel main(), ma i costruttori di static Obj b dentro f() e di static Obj c dentro g() vengono eseguiti solo se queste funzioni vengono chiamate. Per dimostrare quali costruttori e distruttori vengono eseguiti, è stata chiamata solo la funzione f(). L'output del programma è//: C10:DistruttoriStatici.cpp // Distruttori di oggetti statici #include <fstream> using namespace std; ofstream out("statdest.out"); // file di tracciamento class Obj { char c; // Identificatore public: Obj(char cc) : c(cc) { out << "Obj::Obj() per " << c << endl; } ~Obj() { out << "Obj::~Obj() per " << c << endl; } }; Obj a('a'); // Globale (memorizzazione statica) // Costruttore & distruttore sempre chiamati void f() { static Obj b('b'); } void g() { static Obj c('c'); } int main() { out << "dentro il main()" << endl; f(); // Chiama il costruttore statico per b // g() non chiamata out << "all'uscita del main()" << endl; } ///:~
Il costruttore per a viene chiamato prima di entrare nel main(), mentre il costruttore per b viene chiamato solo perchè viene chiamata la funzione f(). Quando si esce dal main(), i distruttori degli oggetti per i quali è stato eseguito il costruttore vengono eseguiti nell'ordine inverso rispetto alla costruzione. Questo significa che se viene chiamata anche g(), l'ordine di distruzione di b e c dipende dall'ordine di chiamata di f() e g(). Notare che anche l'oggetto out, di tipo ofstream, è statico, in quanto definito all'esterno di qualsiasi funzione, e vive nell'area di memoria statica. E' importante che la sua definizione (al contrario di una dichiarazione extern) appaia all'inizio del file, prima che out possa essere usata. Altrimenti si potrebbe usare un oggetto prima che questo venga opportunamente inizializzato. In C++ il costruttore di un oggetto statico globale viene chiamato prima di entrare nel main(), cosicchè abbiamo un modo molto semplice e portabile per eseguire del codice prima di entrare nel main() e di eseguire codice con il distruttore all'uscita dal main(). In C questo richiede di mettere le mani al codice di start-up in linguaggio assembler, fornito insieme al compilatore.Obj::Obj() per a dentro il main() Obj::Obj() per b all'uscita del main() Obj::~Obj() per b Obj::~Obj() per a
Un nome di oggetto o di funzione con visibilità a livello di file esplicitamente dichiarata con static è locale alla sua unità di compilazione (nell'accezione di questo libro vuol dire il file .cpp dove avviene la dichiarazione). Questo nome ha un linkage interno. Questo significa che si puo' usare lo stesso nome in altre unità di compilazione senza creare conflitto. Un vantaggio del linkage interno è il fatto che i nomi possono essere piazzati in un file di intestazione senza temere conflitti durante il link. I nomi che vengono comunemente messi nei file di intestazione, come le definizioni const e le funzioni inline, hanno per default un linkage interno (tuttavia, const ha un linkage interno solo in C++, mentre per il C il default è esterno). Notare, inoltre, che il concetto di linkage si applica solo a quegli elementi per i quali l'indirizzo viene calcolato a tempo di link o di load; ad esempio alle dichiarazioni di classi e alle variabili locali non si applica il concetto di linkage.
Qui c'è un esempio di come le due accezioni del termine >static possano intrecciarsi l'un l'altra. Tutti gli oggetti globali hanno implicitamente una classe di memorizzazione statica, così, se scriviamo (a livello di file),
int a = 0;
Ma se scriviamo,extern int a = 0;
tutto quello che abbiamo fatto è di alterare la visibilità di a, così da conferirle un linkage interno. La classe di memorizzazione non è cambiata, in quanto gli oggetti risiedono nell'area dati statica sia che la visibilità sia static che extern. Quando passiamo alle variabili locali, la parola static non agisce più sulla visibilità, che è già limitata, ma altera la classe di memorizzazione. Se dichiariamo come extern una variabile che altrimenti sarebbe locale, significa che questa è già stata definita da qualche parte (quindi la variabile è di fatto globale alla funzione). Per esempio:static int a = 0;
Con i nomi di funzione (non funzioni membro di classi) static ed extern possono alterare solo la visibilità, così la dichiarazione//: C10:LocaleExtern.cpp //{L} LocaleExtern2 #include <iostream> int main() { extern int i; std::cout << i; } ///:~ //: C10:LocaleExtern2.cpp {O} int i = 5; ///:~
è equivalente aextern void f();
e la dichiarazionevoid f();
significa che f() è visibile solo all'interno di questa unità di compilazione - detta a volte file statico.static void f();
Questa definizione crea un nuovo spazio dei nomi che contiene le dichiarazioni racchiuse tra parentesi. Ma ci sono differenze significative rispetto a class, struct, union ed enum://: C10:MiaLib.cpp namespace MiaLib { // Dichiarazioni } int main() {} ///:~
//: C10:Header1.h #ifndef HEADER1_H #define HEADER1_H namespace MiaLib { extern int x; void f(); // ... }
#endif // HEADER1_H ///:~ //: C10:Header2.h #ifndef HEADER2_H #define HEADER2_H #include "Header1.h" // Aggiungere più nomi a MiaLib namespace MiaLib { // NON è una ridefinizione! extern int y; void g(); // ... }
#endif // HEADER2_H ///:~ //: C10:Continuazione.cpp #include "Header2.h" int main() {} ///:~
//: C10:BobsSuperDuperLibreria.cpp namespace BobsSuperDuperLibreria { class Widget { /* ... */ }; class Poppit { /* ... */ }; // ... } // Troppo lungo da digitare! Usiamo un alias (pseudonimo): namespace Bob = BobsSuperDuperLibreria; int main() {} ///:~
I nomi in questo spazio sono automaticamente disponibili in questa unità di compilazione senza qualificazione. E' garantito che uno spazio senza nome è unico per ogni unità di compilazione. Ponendo nomi locali in namespace senza nome si evita la necessità di usare la parola static per conferirgli un linkage interno. Il C++ depreca l'uso dei file statici a favore dei namespace senza nome.//: C10:NamespaceSenzaNome.cpp namespace { class Braccio { /* ... */ }; class Gamba { /* ... */ }; class Testa { /* ... */ }; class Robot { Braccio braccio[4]; Gamba gamba[16]; Testa testa[3]; // ... } xanthan; int i, j, k; } int main() {} ///:~
In questo modo la funzione tu() è un membro del namespace Me. Se si introduce un friend all'interno di una classe in un namespace globale, il friend viene inserito globalmente.//: C10:IniezioneFriend.cpp namespace Me { class Noi { //... friend void tu(); }; } int main() {} ///:~
//: C10:RisoluzioneDiScope.cpp namespace X { class Y { static int i; public: void f(); }; class Z; void funz(); } int X::Y::i = 9;
class X::Z { int u, v, w; public: Z(int i); int g(); };
X::Z::Z(int i) { u = v = w = i; } int X::Z::g() { return u = v = w = 0; }
Notare che la definizione X::Y::i potrebbe essere tranquillamente riferita a un dato membro della classe Y annidata nella classe X invece che nel namespace X. Quindi, i namespace si presentano molto simili alle classi.void X::funz() { X::Z a(1); a.g(); } int main(){} ///:~
Un uso della direttiva using è quello di includere tutti i nomi definiti in Int dentro un altro namespace, lasciando che questi nomi siano annidati dentro questo secondo namespace://: C10:NamespaceInt.h #ifndef NAMESPACEINT_H #define NAMESPACEINT_H namespace Int { enum segno { positivo, negativo }; class Intero { int i; segno s; public: Intero(int ii = 0) : i(ii), s(i >= 0 ? positivo : negativo) {} segno getSegno() const { return s; } void setSegno(segno sgn) { s = sgn; } // ... }; } #endif // NAMESPACEINT_H ///:~
Si possono anche includere tutti i nomi definiti in Int dentro una funzione, ma in questo modo i nomi sono annidati nella funzione://: C10:NamespaceMat.h #ifndef NAMESPACEMAT_H #define NAMESPACEMAT_H #include "NamespaceInt.h" namespace Mat { using namespace Int; Intero a, b; Intero divide(Intero, Intero); // ... } #endif // NAMESPACEMAT_H ///:~
Senza la direttiva using, tutti i nomi di un namespace hanno bisogno di essere completamente qualificati. Un aspetto della direttiva using potrebbe sembrare controintuitiva all'inizio. La visibilità dei nomi introdotti con una direttiva using corrisponde allo scope in cui la direttiva è posizionata. Ma si possono nascondere i nomi provenienti da una direttiva using, come se fossero dichiarati a livello globale!//: C10:Aritmetica.cpp #include "NamespaceInt.h" void aritmetica() { using namespace Int; Intero x; x.setSegno(positivo); } int main(){} ///:~
Supponiamo di avere un secondo namespace che contiene alcuni dei nomi definiti nel namespace Mat://: C10:SovrapposizioneNamespace1.cpp #include "NamespaceMat.h" int main() { using namespace Mat; Intero a; // Nasconde Mat::a; a.setSegno(negativo); // Adesso è necessaria la risoluzione di scope // per selezionare Mat::a : Mat::a.setSegno(positivo); } ///:~
Siccome con la direttiva using viene introdotto anche questo namespace, c'è la possibilità di una collisione. Tuttavia l'ambiguità si presenta soltanto nel punto in cui si usa il nome e non a livello di direttiva using://: C10:SovrapposizioneNamespace2.h #ifndef SOVRAPPOSIZIONENAMESPACE2_H #define SOVRAPPOSIZIONENAMESPACE2_H #include "NamespaceInt.h" namespace Calcolo { using namespace Int; Intero divide(Intero, Intero); // ... } #endif // SOVRAPPOSIZIONENAMESPACE2_H ///:~
Così, è possibile scrivere direttive using per introdurre tanti namaspace con nomi che confliggono senza provocare mai ambiguità.//: C10:AmbiguitaDaSovrapposizione.cpp #include "NamespaceMat.h" #include "SovrapposizioneNamespace2.h" void s() { using namespace Mat; using namespace Calcolo; // Tutto ok finchè: //! divide(1, 2); // Ambiguità } int main() {} ///:~
//: C10:DichiarazioniUsing.h #ifndef DICHIARAZIONIUSING_H #define DICHIARAZIONIUSING_H namespace U { inline void f() {} inline void g() {} } namespace V { inline void f() {} inline void g() {} } #endif // DICHIARAZIONIUSING_H ///:~
La dichiarazione using fornisce il nome di un identificatore completamente specificato, ma non da informazioni sul suo tipo. Questo vuol dire che se il namespace contiene un set di funzioni sovraccaricate con lo stesso nome, la dichiarazione using dichiara tutte le funzioni del set sovraccaricato. Si puo' mettere una dichiarazione using dovunque è possibile mettere una normale dichiarazione. Una dichiarazione using agisce come una normale dichiarazione eccetto per un aspetto: siccome non gli forniamo una lista di argomenti, è possibile che la dichiarazione using causi un sovraccaricamento di funzioni con lo stesso tipo di argomenti (cosa che non è permessa con un normale sovraccaricamento). Questa ambiguità, tuttavia, non si evidenzia nel momento della dichiarazione, bensì nel momento dell'uso. Una dichiarazione using puo' apparire anche all'interno di un namespace, e ha lo stesso effetto della dichiarazione dei nomi all'interno del namespace://: C10:DichiarazioneUsing1.cpp #include "DichiarazioniUsing.h" void h() { using namespace U; // Direttiva using using V::f; // Dichiarazione using f(); // Chiama V::f(); U::f(); // Bisogna qualificarla completamente per chiamarla } int main() {} ///:~
Una dichiarazione using è un alias, e permette di dichiarare le stesse funzioni in namespace diversi. Se si finisce per ridichiarare la stessa funzione importando namespace diversi, va bene, non ci saranno ambiguità o duplicazioni.//: C10:DichiarazioneUsing2.cpp #include "DichiarazioniUsing.h" namespace Q { using U::f; using V::g; // ... } void m() { using namespace Q; f(); // Chiama U::f(); g(); // Chiama V::g(); } int main() {} ///:~
Per
gli header file le cose sono diverse. Non si dovrebbe mai introdurre una
direttiva using all'interno di un header file, perchè questo significherebbe
che tutti i file che lo includono si ritrovano il namespace aperto (e un
header file puo' includere altri header file).
Così,
negli header file si potrebbero usare sia qualificazioni esplicite o direttive
using
a risoluzione di scope che dichiarazioni using. Questa è la pratica
che si usa in questo libro e seguendola non si rischia di "inquinare" il
namespace globale e cadere nel mondo C++ precedente all'introduzione dei
namespace.
Membri Statici in C++
A volte si presenta la necessità per tutti i membri
di una classe di usare un singolo blocco di memoria. In C si puo' usare
una variabile globale, ma questo non è molto sicuro. I dati globali possono
essere modificati da chiunque, e i loro nomi possono collidere con altri
nomi identici in un progetto grande. L'ideale sarebbe che il dato venisse
allocato come se fosse globale, ma fosse nascosto all'interno di una classe
e chiaramente associato a tale classe. Questo
è ottenuto con dati membro static all'interno di una classe. C'è
un singolo blocco di memoria per un dato membro static, a prescindere
da quanti oggetti di quella classe vengono creati. Tutti gli oggetti condividono
lo stesso spazio di memorizzione static per quel dato membro, percio'
questo è un modo per loro di "comunicare" l'un l'altro. Ma il dato static
appartiene alla classe; il suo nome è visibile solo all'interno della
classe e puo' essere public,
private, o protected.
Definire lo spazio di memoria
per i dati membri statici
Siccome i dati membri static hanno un solo
spazio di memoria a prescindere da
quanti oggetti sono stati creati, questo spazio di memoria deve essere
definito in un solo punto. Il compilatore non alloca memoria. Il linker
riporta un errore se un dato membro static viene dichiarato ma non
definito. La
definizione deve essere fatta al di fuori della classe (non sono permesse
definizioni inline), ed è permessa solo una definizione. Percio' è usuale
mettere la definizione nel file di implementazione della classe. La sintassi
a volte crea dubbi, ma in effetti è molto logica. Per esempio, se si crea
un dato membro statico dentro una classe, come questo:
Bisogna definire spazio di memoria per questo dato
membro statico nel file di definizione, così:
class A {
static int i;
public:
//...
};
Se vogliamo definire una variabile globale ordinaria,
dobbiamo scrivere:
int A::i = 1;
ma qui per specificare
A::i vengono usati l'operatore
di risoluzione di scope e il nome della classe. Alcuni
provano dubbi all'idea che A::i sia private e che qui c'è
qualcosa che sembra manipolarlo portandolo allo scoperto. Non è che questo
rompe il meccanismo di protezione? E' una pratica completamente sicura
per due motivi. Primo, l'unico posto in cui l'inizializzazione è legale
è nella definizione. In effetti se il dato static fosse stato un
oggetto con un costruttore, avremmo potuto chiamare il costruttore invece
di usare l'operatore = (uguale). Secondo, una volta che la definizione
è stata effettuata, l'utente finale non puo' effettuarne una seconda, il
linker riporterebbe un errore. E il creatore della classe è forzato a
creare una definizione, altrimenti il codice non si linka durante il test.
Questo assicura che la definizione avvenga una sola volta ed è gestita
dal creatore della classe. L'intera
espressione di inizializzazione per un membro statico ricade nello scope
della classe. Per esempio,
int i = 1;
Qui la qualificazione ConStatic:: estende
lo scope di ConStatic all'intera definizione.
//: C10:Statinit.cpp
// Scope di inizializzatore static
#include <iostream>
using namespace std;
int x = 100;
class ConStatic {
static int x;
static int y;
public:
void print() const {
cout << "ConStatic::x = " << x << endl;
cout << "ConStatic::y = " << y << endl;
}
};
int ConStatic::x = 1;
int ConStatic::y = x + 1;
// ConStatic::x NON ::x
int main() {
ConStatic cs;
cs.print();
} ///:~
Inizializzazione di array
static
Il capitolo 8 ha introdotto la variabile static
const che permette di definire un valore costante all'interno del corpo
di una classe. E' anche possibile creare array di oggetti static,
sia const che non-const. La sintassi è ragionevolmente consistente:
Con static consts di tipi integrali si possono
fornire definizioni all'interno della classe, ma per qualsiasi altra cosa
(incluso array di tipi integrali, anche se sono static const) bisogna
fornire un'unica definizione esterna per il membro. Queste definizioni
hanno un linkage interno, cosicchè essi possono essere messi negli header
file. La sintassi per inizializzare gli array statici è la stessa di qualunque
altro aggregato, incluso il conteggio automatico. Si
possono creare anche oggetti static
const di tipi di classi e array di questi oggetti. Tuttavia non si
possono inizializzare con la "sintassi
inline" permessa per i tipi integrali static consts predefiniti.
//: C10:ArrayStatico.cpp
// Inizializzazione di array statici nelle classi
class Valori {
// static consts vengono inizializzati sul posto:
static const int scSize = 100;
static const long scLong = 100;
// Il conteggio automatico funziona con gli array statici.
// Gli Array statici, sia non-integrali che non-const,
// vanno inizializzati esternamente:
static const int scInteri[];
static const long scLongs[];
static const float scTabella[];
static const char scLettere[];
static int size;
static const float scFloat;
static float tabella[];
static char lettere[];
};
int Valori::size = 100;
const float Valori::scFloat = 1.1;
const int Valori::scInteri[] = {
99, 47, 33, 11, 7
};
const long Valori::scLongs[] = {
99, 47, 33, 11, 7
};
const float Valori::scTabella[] = {
1.1, 2.2, 3.3, 4.4
};
const char Valori::scLettere[] = {
'a', 'b', 'c', 'd', 'e',
'f', 'g', 'h', 'i', 'j'
};
float Valori::tabella[4] = {
1.1, 2.2, 3.3, 4.4
};
char Valori::lettere[10] = {
'a', 'b', 'c', 'd', 'e',
'f', 'g', 'h', 'i', 'j'
};
int main() { Valori v; } ///:~
L'inizializzazione di array di oggetti di classi sia const che non-const static deve essere effettuata allo stesso modo, seguendo la tipica sintassi della definizione static.//: C10:ArrayDiOggettiStatici.cpp // Array static di oggetti di classi class X { int i; public: X(int ii) : i(ii) {} }; class Stat { // Questo non funziona: //! static const X x(100); // Gli oggetti di classi statiche, sia const che non-const, // vanno inizializzati esternamente: static X x2; static X xTabella2[]; static const X x3; static const X xTabella3[]; }; X Stat::x2(100); X Stat::xTabella2[] = { X(1), X(2), X(3), X(4) }; const X Stat::x3(100); const X Stat::xTabella3[] = { X(1), X(2), X(3), X(4) }; int main() { Stat v; } ///:~
Si puo' notare subito un problema con i membri static in una classe locale: Come descrivere un dato membro a livello di file allo scopo di definirlo? Nella pratica le classi locali sono usate raramente.//: C10:Locale.cpp // Membri statici & classi locali #include <iostream> using namespace std; // Classi nidificate POSSONO avere dati membri statici: class Esterna { class Interna { static int i; // OK }; }; int Esterna::Interna::i = 47; // Classi Locali NON possono avere dati membri statici: void f() { class Locale { public: //! static int i; // Errore // (Come si potrebbe definire i?) } x; } int main() { Esterna x; f(); } ///:~
Quando si vedono funzioni membro statiche in una classe, bisogna ricordarsi che il progettista aveva in mente che la funzione fosse concettualmente associata alla classe nella sua interezza. Una funzione membro static non puo' accedere ai dati membro ordinari, ma solo i membri static. Puo' chiamare solo altre funzioni membro static. Normalmente, quando una funzione membro viene chiamata le viene tacitamente passato l'indirizzo dell'oggetto corrente (this), ma un membro static non ha un puntatore this, e questa è la ragione per cui una funzione membro static non puo' chiamare i membri ordinari. Si ottiene anche un leggero aumento nella velocità di esecuzione, come per le funzioni globali, per il fatto che le funzioni membro static non hanno l'extra overhead di passare il parametro this. Nello stesso tempo si sfrutta il beneficio di avere la funzione dentro la classe. Per i dati membro, static indica che esiste un solo spazio di memoria per i dati membri per tutti gli oggetti di una classe. Questo parallelizza l'uso di static per definire oggetti dentro una funzione, per indicare che viene usata una sola copia di una variabile locale per tutte le chiamate alla funzione. Qui c'è un esempio che mostra membri dati static e funzioni membro static usati insieme://: C10:FunzioneSempliceMembroStatico.cpp class X { public: static void f(){}; }; int main() { X::f(); } ///:~
Per il fatto di non disporre del puntatore this, le funzioni membro static nè possono accedere ai dati membri non-static nè possono chiamare funzioni membro non-static. Notare in main( ) che un membro static puo' essere selezionato utilizzando l'usuale sintassi con il punto o con la freccia, associando la funzione con un oggetto, ma anche senza nessun oggetto (perchè un membro static è associato alla classe e non ad un particolare oggetto), usando il nome della classe e l'operatore di risoluzione di scope. Qui c'è un'importante caratteristica: a causa del modo in cui avviene l'inizializzazione di un oggetto membro static, si puo' mettere un dato membro static della stessa classe all'interno della classe. Qui c'è un esempio che permette ad un solo oggetto di tipo Uovo di esistere rendendo il costruttore private. Si puo' accedere all'oggetto, ma non si puo' creare un nuovo oggetto di tipo Uovo://: C10:FunzioniMembroStatiche.cpp class X { int i; static int j; public: X(int ii = 0) : i(ii) { // Funzioni membro non-static possono accedere a // funzioni membro o dati statici: j = i; } int val() const { return i; } static int incr() { //! i++; // Errore: le funzioni membro statiche // non possono accedere ai dati membro non-static return ++j; } static int f() { //! val(); // Errore: le funzioni membro static // non possono accedere alle funzioni membro non-static return incr(); // OK -- chiama una funzione static } }; int X::j = 0; int main() { X x; X* xp = &x; x.f(); xp->f(); X::f(); // Funziona solo con membri static } ///:~
L'inizializzazione di u avviene dopo che è stata completata la dichiarazione della classe, così il compilatore ha tutte le informazioni di cui ha bisogno per allocare memoria ed effettuare la chiamata al costruttore. Per prevenire del tutto la creazione di un qualsiasi altro oggetto, è stato aggiundo qualcos'altro: un secondo costruttore private, detto copy-constructor (costruttore di copia). A questo punto del libro non possiamo sapere perchè questo è necessario, perchè il costruttore di copia sarà introdotto nel prossimo capitolo. Tuttavia, come piccola anticipazione, se rimuoviamo il costruttore di copia definito nell'esempio precedente, potremmo creare un oggetto Uovo come questo://: C10:UnicoEsemplare.cpp // Membri statici di un qualche tipo, assicurano che // esiste solo un oggetto di questo tipo. // Viene detto anche "unico" esemplare ("singleton" pattern). #include <iostream> using namespace std; class Uovo { static Uovo u; int i; Uovo(int ii) : i(ii) {} Uovo(const Uovo&); // Previene la copy-costruzione public: static Uovo* istanza() { return &u; } int val() const { return i; } }; Uovo Uovo::u(47); int main() { //! Uovo x(1); // Errore -- non puo' creare un Uovo // Si puo' accedere alla singola istanza: cout << Uovo::istanza()->val() << endl; } ///:~
Entrambe le istruzioni di sopra usano il costruttore di copia, così per escludere questa possibilità il costruttore di copia è dichiarato come private (nessuna definizione è necessaria, perchè esso non sarà mai chiamato). Una larga parte del prossimo capitolo è dedicata al costruttore di copia e così sarà molto più chiaro.Uovo u = *Uovo::istanza(); Uovo u2(*Uovo::istanza());
e un altro file usa l'oggetto out in uno dei suoi inizializzatori//: C10:Out.cpp {O} // Primo file #include <fstream> std::ofstream out("out.txt"); ///:~
//: C10:Oof.cpp // Secondo file //{L} Out #include <fstream> extern std::ofstream out; class Oof { public: Oof() { std::out << "ahi"; } } oof;
il programma potrebbe funzionare, ma potrebbe anche non funzionare. Se l'ambiente di programmazione costruisce il programma in modo che il primo file viene inizializzato prima del secondo, non ci sarà nessun problema. Tuttavia se il secondo file viene inizializzato prima del primo file, il costruttore di Oof fa affidamento sull'esistenza di out, il quale pero' non è stato ancora costruito e questo provoca un caos. Questo problema succede soltanto con gli inizializzatori di oggetti statici che dipendono l'un l'altro. Gli oggetti statici in un'unità di compilazione vengono inizializzati prima che venga invocata qualsiasi funzione in tale unità - ma potrebbe essere dopo il main( ). Non si puo' essere sicuri dell'ordine di inizializzazione di oggetti statici che risiedono su file diversi. Un esempio subdolo puo' essere trovato in ARM.[47] In un file si ha, a livello globale:int main() {} ///:~
e in un secondo file si ha, sempre a livello globale:extern int y; int x = y + 1;
Per tutti gli oggetti statici, il meccanismo di linking-loading garantisce l'inizializzazione a zero prima che abbia luogo l'inizializzazione dinamica del programmatore. Nell'esempio precedente, l'azzeramento della memoria occupata dall'oggetto fstream out non ha un significato particolare, percio' essa è semplicemente indefinita fino a quando non viene chiamato il costruttore. Tuttavia, per i tipi predefiniti l'inizializzazione a zero ha sempre senso e se i file sono inizializzazti nell'ordine mostrato sopra, y ha valore iniziale zero e così x diventa uno e y diventa dinamicamente due. Ma se i file vengono inizializzati nell'ordine inverso, x viene staticamente inizializzato a zero, y viene dinamicamente inizializzato a uno e quindi x diventa due. I programmatori devono essere consapevoli di questo, altrimenti potrebbero creare un programma con dipendenze da inizializzazioni statiche che possono funzionare su una piattaforma, ma spostandoli su un'altra piattaforma potrebbero misteriosamente non funzionare.extern int x; int y = x + 1;
Le dichiarazioni di x e y annunciano solo che questi oggetti esistono, ma non allocano memoria per essi. Tuttavia, la definizione di Inizializzatore init alloca memoria per questo oggetto in tutti i file in cui l'header file è incluso. Ma siccome il nome è static (che controlla la visibilità, non il modo in cui viene allocata la memoria; la memorizzazione è a livello di file per default), esso è visibile solo all'interno dell'unità di compilazione e il linker non si "arrabbia" per questa definizione multipla. Qui c'è il file per le definizioni di x, y, e initCount://: C10:Inizializzatore.h // Tecnica di inizializzazione di oggetti static #ifndef INIZIALIZZATORE_H #define INIZIALIZZATORE_H #include <iostream> extern int x; // Dichiarazioni, non definizioni extern int y; class Inizializzatore { static int initCount; public: Inizializzatore() { std::cout << "Inizializzatore()" << std::endl; // Inizializza solo la prima volta if(initCount++ == 0) { std::cout << "effettua l'inizializzazione" << std::endl; x = 100; y = 200; } } ~Inizializzatore() { std::cout << "~Inizializzatore()" << std::endl; // Cancella solo l'ultima volta if(--initCount == 0) { std::cout << "effettua la cancellazione" << std::endl; // Qualsiasi altra pulizia qui } } }; // L'istruzione seguente crea un oggetto in ciascun file // in cui Inizializzatore.h viene incluso, ma questo oggetto è visibile solo in questo file: static Inizializzatore init; #endif // INIZIALIZZATORE_H ///:~
//: C10:DefinInizializzatore.cpp {O} // Definizioni per Inizializzatore.h #include "Inizializzatore.h" // L'inizializzazione Static forza // tutti questi valori a zero: int x; int y; int Inizializzatore::initCount; ///:~
//: C10:Inizializzatore.cpp {O} // Inizializzazione Static #include "Inizializzatore.h" ///:~
Adesso non ha importanza quale unità di compilazione viene inizializzata per prima. La prima volta che una unità di compilazione contenente Inizializzatore.h viene inizializzata, initCount sarà posto a zero e così l'inizializzazione sarà effettuata (questo dipende pesantemente dal fatto che l'area di memoria statica viene inizializzata a zero prima che qualsiasi inizializzazione dinamica abbia luogo). Per tutte le altre unità di compilazione, initCount sarà diverso da zero e l'inizializzazione viene saltata. La cancellazione avviene nell'ordine inverso e ~Inizializzatore( ) assicura che cio' avvenga una volta sola. Questo esempio ha usato tipi predefiniti come oggetti statici globali. La tecnica funziona anche con le classi, ma questi oggetti devono essere inizializzati dinamicamente dalla classe Inizializzatore. Un modo per fare questo è di creare classi senza costruttori e distruttori, ma con funzioni membro per l'inizializzazione e la cancellazione che usano nomi diversi. Un approccio più comune, comunque, è quello di avere puntatori ad oggetti e di crearli usando new dentro Inizializzatore( ).//: C10:Inizializzatore2.cpp //{L} DefinInizializzatore // Inizializzazione Static #include "Inizializzatore.h" using namespace std; int main() { cout << "dentro il main()" << endl; cout << "all'uscita del main()" << endl; } ///:~
Il costruttore, inoltre, si annuncia quando viene chiamato e quindi possiamo stampare, con print( ), lo stato dell'oggetto per scoprire se è stato inizializzato. La seconda classe viene inizializzata da un oggetto della prima classe, che è quello che causa la dipendenza://: C10:Dipendenza1.h #ifndef DIPENDENZA1_H #define DIPENDENZA1_H #include <iostream> class Dipendenza1 { bool init; public: Dipendenza1() : init(true) { std::cout << "Costruzione di Dipendenza1" << std::endl; } void print() const { std::cout << "Init di Dipendenza1: " << init << std::endl; } }; #endif // DIPENDENZA1_H ///:~
Il costruttore annuncia se stesso e stampa lo stato dell'oggetto d1 cosicchè possiamo vedere se è stato inizializzato nel momento in cui il costruttore viene chiamato. Per dimostrare cosa puo' andare storto, il file seguente pone dapprima le definizioni degli oggetti statici nell'ordine sbagliato, come potrebbe succedere se il linker inizializzasse dapprima l'oggetto Dipendenza2 rispetto all'oggetto Dipendenza1. Successivamente l'ordine di definizione viene invertito per mostrare come funziona correttamente se l'ordine di inizializzazione è "giusto." Per ultimo, viene dimostrata la tecnica numero due. Per fornire un output più leggibile, viene creata la funzione separatore( ). Il trucco è quello di non permettere di chiamare una funzione globalmente a meno che la funzione non è usata per effettuare l'inizializzazione di una variabile, così la funzione separatore( ) restituisce un valore fittizio che viene usato per inizializzare una coppia di variabili globali.//: C10:Dipendenza2.h #ifndef DIPENDENZA2_H #define DIPENDENZA2_H #include "Dipendenza1.h" class Dipendenza2 { Dipendenza1 d1; public: Dipendenza2(const Dipendenza1& dip1): d1(dip1){ std::cout << "Costruzione di Dipendenza2"; print(); } void print() const { d1.print(); } }; #endif // DIPENDENZA2_H ///:~
Le funzioni d1( ) e d2( ) inglobano istance statiche degli oggetti Dipendenza1 e Dipendenza2. A questo punto, l'unico modo che abbiamo per ottenere gli oggetti statici è quello di chiamare le funzioni e quindi forzare l'inizializzazione degli oggetti statici alla prima chiamata delle funzioni. Questo significa che l'inizializzazione è garantita essere corretta, come si puo' vedere facendo girare il programma e osservando l'output. Qui è mostrato come organizzare effettivamente il codice per usare la tecnica esposta. Ordinariamente gli oggetti statici dovrebbero essere definiti in file separati (perchè si è costretti per qualche motivo; ma va ricordato che la definizione degli oggetti in file separati è cio' che causa il problema), qui invece definiamo le funzioni contenitrici in file separati. Ma è necessario definirle in header file://: C10:Tecnica2.cpp #include "Dipendenza2.h" using namespace std; // Restituisce un valore cosicchè puo' essere chiamata // come inizializzatore globale: int separatore() { cout << "---------------------" << endl; return 1; } // Simula il problema della dipendenza: extern Dipendenza1 dip1; Dipendenza2 dip2(dip1); Dipendenza1 dip1; int x1 = separatore(); // Ma se avviene in questo ordine funziona correttamente: Dipendenza1 dip1b; Dipendenza dip2b(dip1b); int x2 = separatore(); // Seguono oggetti static inglobati all'interno di funzioni Dipendenza1& d1() { static Dipendenza1 dip1; return dip1; } Dipendenza2& d2() { static Dipendenza2 dip2(d1()); return dip2; } int main() { Dipendenza2& dip2 = d2(); } ///:~
In realtà lo specificatore "extern" è ridondante per la dichiarazione di una funzione. Qui c'è il secondo header file://: C10:Dipendenza1StatFun.h #ifndef DIPENDENZA1STATFUN_H #define DIPENDENZA1STATFUN_H #include "Dipendenza1.h" extern Dipendenza1& d1(); #endif // DIPENDENZA1STATFUN_H ///:~
A questo punto, nei file di implementazione dove prima avremmo messo la definizione degli oggetti statici, mettiamo la definizione delle funzioni contenitrici://: C10:Dipendenza2StatFun.h #ifndef DIPENDENZA2STATFUN_H #define DIPENDENZA2STATFUN_H #include "Dipendenza2.h" extern Dipendenza2& d2(); #endif // DIPENDENZA2STATFUN_H ///:~
Presumibilmente andrebbe messo altro codice in questo file. Segue l'altro file://: C10:Dipendenza1StatFun.cpp {O} #include "Dipendenza1StatFun.h" Dipendenza1& d1() { static Dipendenza1 dip1; return dip1; } ///:~
Così adesso ci sono due file che potrebbero essere linkati in qualsiasi ordine e se contenessero oggetti statici ordinari potrebbero dar luogo ad un ordine di inizializzazione qualsiasi. Ma siccome essi contengono funzioni che fanno da contenitore, non c'è nessun pericolo di inizializzazione errata://: C10:Dipendenza2StatFun.cpp {O} #include "Dipendenza1StatFun.h" #include "Dipendenza2StatFun.h" Dipendenza2& d2() { static Dipendenza2 dip2(d1()); return dip2; } ///:~
Quando facciamo girare questo programma possiamo vedere che l'inizializzazione dell'oggetto statico Dipendenza1 avviene sempre prima dell'inizializzazione dell'oggetto statico Dipendenza2. Possiamo anche notare che questo approccio è molto più semplice della tecnica numero uno. Si puo' essere tentati di scrivere d1( ) e d2( ) come funzioni inline dentro i rispettivi header file, ma questa è una cosa che dobbiamo evitare. Una funzione inline puo' essere duplicata in tutti i file in cui appare- e la duplicazione include la definizione dell' oggetto statico. Siccome le funzioni inline hanno per default un linkage interno, questo puo' significare avere oggetti statici multipli attraverso le varie unità di compilazione, che potrebbe sicuramente creare problemi. Percio' dobbiamo assicurare che ci sia una sola definizione di ciascuna funzione contenitrice, e questo significa non costruire tali funzioni inline.//: C10:Tecnica2b.cpp //{L} Dipendenza1StatFun Dipendenza2StatFun #include "Dipendenza2StatFun.h" int main() { d2(); } ///:~
il compilatore C++ decorerà questo nome con qualcosa tipo _f_int_char per supportare l' overloading di funzioni (e il linkage tipo-sicuro). Tuttavia il compilatore C che ha compilato la libreria certamente non ha decorato il nome della funzione, così il suo nome interno sarà _f. Percio' il linker non sarà in grado di risolvere in C++ la chiamata a f( ). La scappatoia fornita dal C++ è la specificazione di linkage alternativo, che si ottiene con l'overloading della parola chiave extern. La parola extern viene fatta seguire da una stringa che specifica il linkage che si vuole per la dichiarazione, seguita dalla dichiarazione stessa:float f(int a, char b);
Questo dice al compilatore di conferire un linkage tipo C ad f( ) così il compilatore non decora il nome. Gli unici due tipi di specificazioni di linkage supportati dallo standard sono "C" e "C++," ma i fornitori di compilatori hanno l'opzione di supportare altri linguaggi allo stesso modo. Se si ha un gruppo di dichiarazioni con linkage alternativi, bisogna metterle tra parentesi graffe, come queste:extern "C" float f(int a, char b);
Oppure, per un header file,extern "C" { float f(int a, char b); double d(int a, char b); }
Molti fornitori di compilatori C++ gestiscono le specificazioni di linkage alternativi all'interno dei loro header file che funzionano sia con il C che con il C++, in modo che non ci dobbiamo preoccupare al riguardo.extern "C" { #include "Mioheader.h" }