MindView Inc.

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[ Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

trad. italiana e adattamento a cura di Giacomo Grande

10: Controllo dei Nomi

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.

Elementi statici dal C

Sia in C che in C++ la parola chiave static assume due significati di base, che sfortunatamente spesso si intrecciano:
  1. Allocato una volta per tutte ad un indirizzo fisso; cioè l'oggetto viene creato in una speciale area di dati statica piuttosto che nello stack ogni volta che viene chiamata una funzione. Questo è il concetto di allocazione statica.
  2. Locale ad una particolare unità di compilazione (e locale allo scope di una classe in C++, come vedremo). Qui la parola static controlla la visibilità del nome, cosicchè il nome non puo' essere visto al di fuori dell'unità di compilazione o della classe. Questo descrive anche il concetto di linkage, che determina quali nomi sono visibili al linker.
Questa sezione spiega i due precedenti significati della parola static così come sono stati ereditati dal C.

variabili statiche all'interno di funzioni

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:

//: 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;
} ///:~
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,
static char* s = 0;
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.

oggetti di classi statiche all'interno di funzioni

Le regole sono le stesse di quelle per oggetti statici di tipi definiti dall'utente, compreso il fatto che l'oggetto richiede una qualche inizializzazione. Tuttavia l'assegnamento del valore zero è significativo solo per tipi predefiniti; i tipi definiti dall'utente necessitano di un costruttore per essere inizializzati. Percio', se non si specificano gli argomenti per il costruttore in fase di definizione di un oggetto statico, viene usato un costruttore di default, che ogni classe deve fornire. Per esempio,
//: 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();
} ///:~
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.

Distruttori di oggetti statici

I distruttori per oggetti statici (cioè tutti quelli con locazione statica, non solo quelli statici locali come nell'esempio sopra) vengono chiamati quando si esce dal main() o quando viene chiamata esplicitamente la funzione exit() della libreria standard del C. In molte implementazioni la funzione exit() viene chiamata all'uscita del main(). Questo significa che è dannoso chiamare la funzione exit() all'interno di un distruttore, in quanto si potrebbe cadere in un loop infinito. I distruttori di oggetti statici non vengono chiamati se si esce dal programma usando la funzione di libreria abort(). Si possono specificare le azioni che il programma deve svolgere all'uscita del main() (o alla chiamata di exit()) usando la funzione della libreria standard del C atexit(). In questo caso le funzioni registrate con atexit() possono essere chiamate prima dei distruttori per tutti gli oggetti costruiti, prima di uscire dal main() (o della chiamata ad exit()).

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,

//: 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;
} ///:~
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 è
Obj::Obj() per a
dentro il main()
Obj::Obj() per b
all'uscita del main()
Obj::~Obj() per b
Obj::~Obj() per a
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.

Il controllo del linkage

Ordinariamente, qualsiasi nome con scope a livello di file (cioè non annidato all'interno di una classe o di una funzione) è visibile attraverso tutte le unità di compilazione in un programma. Questo spesso è chiamato linkage esterno, in quanto durante il link il nome è visibile al linker dovunque, esternamente a tale unità di compilazione. Le variabili globali e le funzioni ordinarie hanno questo tipo di linkage. Ci sono casi in cui si vuole limitare la visibilità di un nome. Si potrebbe volere che una variabile sia visibile a livello di file, in modo che tutte le funzioni definite in quel file possano accedervi, ma si vuole impedire l'accesso alle funzioni al di fuori del file o evitare una collisione di nomi con identificatori definiti in altri file.

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.

Confusione

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;
l'allocazione di a avviene nell'area dati statica del programma, e l'inizializzazione di a avviene una sola volta, prima di entrare nel main(). In più, la visibilità di a è globale rispetto a tutte le unità di compilazione. In termini di visibilità l'opposto di static (visibile solo in questa unità di compilazione) è extern, che asserisce esplicitamente che la visibiltà è trasversale a tutte le unità di compilazione. Percio' la definizione precedente è equivalente alla seguente:
extern int a = 0;
Ma se scriviamo,
static 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:
//: C10:LocaleExtern.cpp
//{L} LocaleExtern2
#include <iostream>

int main() {
  extern int i;
  std::cout << i;
} ///:~

//: C10:LocaleExtern2.cpp {O}
int i = 5;
///:~
Con i nomi di funzione (non funzioni membro di classi) static ed extern possono alterare solo la visibilità, così la dichiarazione
extern void f();
è equivalente a
void f();
e la dichiarazione
static void f();
significa che f() è visibile solo all'interno di questa unità di compilazione - detta a volte file statico.

Altri specificatori di classi di memorizzazione

Gli specificatori static ed extern si usano abitualmente. Ma ci sono altri due specificatori di classi di memorizzazione che si vedono meno frequentemente. Lo specificatore auto non è quasi mai usato, in quanto esso istruisce il compilatore che una variabile è locale. auto, infatti, è una forma abbreviata che sta per "automatic" e si riferisce al modo in cui il compilatore automaticamente alloca memoria per una variabile. Il compilatore è sempre in grado di determinare questo fatto dal contesto, per cui l'uso di auto è ridontante. L'altro specificatore è register. Una variabile register è di tipo locale (auto). Con register si avvisa il compilatore che questa variabile potrebbe essere usata pesantemente e che quindi se puo' la deve memorizzare in un registro. In questo senso è un aiuto all'ottimizzazione. Compilatori diversi rispondono in maniera differente a questo suggerimento; essi hanno anche la possibilità di ignorarlo. Se ad esempio si prende l'indirizzo di una variabile, lo specificatore register è del tutto inutile e quindi quasi certamente viene ignorato. Si dovrebbe evitare l'uso eccessivo di register, in quanto è probabile che il compilatore effettui l'ottimizzazione meglio di noi.

Spazio dei nomi (namespaces)

Anche se i nomi possono essere annidati all'interno di classi, i nomi di funzioni globali, di variabili globali e di classi sono comunque in un unico spazio di nomi. La parola chiave static ci permette un certo controllo su questo, permettendoci di conferire alle variabili e alle funzioni un linkage interno (cioè rendendoli file statici). Ma in un progetto vasto, la perdita di controllo sullo spazio dei nomi globale puo' causare problemi. Per risolvere questo problema per le classi, spesso i fornitori creano lunghi nomi complicati che difficilmente possono confliggere, ma ci costringono poi a digitare questi nomi (Si puo' usare typedef per semplificarli). Non è una soluzione elegante, supportata dal linguaggio. Possiamo suddividere lo spazio dei nomi globale in tanti pezzi più maneggevoli, usando una caratteristica propria del C++, ottenuta con la parola chiave namespace. La parola chiave namespace, come class, struct, enum ed union, pone i nomi dei suoi membri in uno spazio distinto.

Creazione di uno spazio dei nomi

La creazione di uno spazio dei nomi è simile a quella di una class:
//: C10:MiaLib.cpp
namespace MiaLib {
  // Dichiarazioni
}
int main() {} ///:~
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: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() {} ///:~

Namespace senza nome

Ogni unità di compilazione contiene un namespace senza nome che si puo' aggiungere utilizzando la parola chiave "namespace" senza identificatore:
//: 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() {} ///:~
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.

Friends

Si puo' iniettare una dichiarazione friend in un namespace inserendola dentro una classe:
//: C10:IniezioneFriend.cpp
namespace Me {
  class Noi {
    //...
    friend void tu();
  };
} 
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.

Usare uno spazio dei nomi

Ci si puo' riferire ad un nome all'interno di un namespace in tre modi: specificando il nome usando l'operatore di risoluzione di scope, con una direttiva using per introdurre tutti i nomi nel namespace, oppure una dichiarazione using per introdurre nomi uno alla volta.

Risoluzione di scope

Ogni nome in un namespace puo' essere esplicitamente specificato usando l'operatore di risoluzione di scope allo stesso modo in cui ci si puo' riferire ad un nome all'interno di una classe:
//: 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; }
void X::funz() {
  X::Z a(1);
  a.g();
}
int main(){} ///:~
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.

La direttiva using

Siccome digitare l'intero qualificatore per un nome in un namespace potrebbe presto diventare tedioso, la parola chiave using ci permette di importare un intero namespace in un colpo solo. Quando questa viene usata insieme alla parola chiave namespace si ottiene quella che si chiama direttiva using . La direttiva using fa si che i nomi appaiano come se appartenessero al namespace più vicino che la racchiude, cosicchè possiamo usare convenientemente nomi senza qualificatori. Consideriamo un semplice 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 ///:~
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: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 ///:~
Si possono anche includere tutti i nomi definiti in Int dentro una funzione, ma in questo modo i nomi sono annidati nella funzione:
//: C10:Aritmetica.cpp
#include "NamespaceInt.h"
void aritmetica() {
  using namespace Int;
  Intero x;
  x.setSegno(positivo);
}
int main(){} ///:~
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: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);
} ///:~
Supponiamo di avere un secondo namespace che contiene alcuni dei nomi definiti nel namespace Mat:
//: C10:SovrapposizioneNamespace2.h
#ifndef SOVRAPPOSIZIONENAMESPACE2_H
#define SOVRAPPOSIZIONENAMESPACE2_H
#include "NamespaceInt.h"
namespace Calcolo {
  using namespace Int;
  Intero divide(Intero, Intero);
  // ...
} 
#endif // SOVRAPPOSIZIONENAMESPACE2_H ///:~
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: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() {} ///:~
Così, è possibile scrivere direttive using per introdurre tanti namaspace con nomi che confliggono senza provocare mai ambiguità.

La dichiarazione using

Si possono inserire uno alla volta dei nomi dentro lo scope corrente con una dichiarazione using. A differenza della direttiva using, che tratta i nomi come se fossero dichiarati a livello globale, una dichiarazione using è una dichiarazione interna allo scope corrente. Questo significa che puo' sovrapporre nomi provenienti da una direttiva using:
//: 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 ///:~
//: 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() {} ///:~
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: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() {} ///:~
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.

L' uso dei namespace

Alcune delle regole di sopra possono sembrare scoraggianti all'inizio, specialmente se si pensa di doverle usare continuamente. In generale, invece, si puo' fare un uso molto semplice dei namespace, una volta capito come funzionano. Il concetto chiave da ricordare è che introducendo una direttiva using globale (attraverso una "using namespace" esterna a qualsiasi scope) si apre il namespace per il file in cui è inserita. Questo in generale va bene per i file di implementazione (un file "cpp") perchè la direttiva using ha effetto solo fino alla fine della compilazione del file. Cioè non ha effetto su nessun altro file, percio' si puo' effettuare il controllo dei namespace un file di implementazione alla volta. Ad esempio, se si scopre un conflitto di nomi a causa dell'uso di diverse direttive using in un particolare file di implementazione, è semplice modificare quel file in modo che usi qualificazioni esplicite o dichiarazioni using per eliminare il conflitto, senza modificare altri file di implementazione.

  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:
class A {
  static int i;
public:
  //...
};
Bisogna definire spazio di memoria per questo dato membro statico nel file di definizione, così:
int A::i = 1;
Se vogliamo definire una variabile globale ordinaria, dobbiamo scrivere:
int 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,
//: 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();
} ///:~
Qui la qualificazione ConStatic:: estende lo scope di ConStatic all'intera definizione.

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:
//: 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; } ///:~
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: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; } ///:~
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.

Classi nidificate e locali

Si possono facilmente mettere membri dati statici dentro classi nidificate all'interno di altre classi. La definizione di tali membri è un'estensione ovvia ed intuitiva - semplicemente si usa un altro livello di risoluzione di scope. Tuttavia non si possono avere dati membri static dentro classi locali (una classe locale è una classe definita dentro una funzione). Così,
//: 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(); } ///:~
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.

funzioni membro static

Si possono creare anche funzioni membro static che, come i dati membro static, funzionano per la classe nel suo insieme, piuttosto che per un particolare oggetto della classe. Invece di costruire una funzione globale che vive e "inquina" il namespace globale e locale, si pone la funzione dentro la classe. Quando si crea una funzione membro static si esprime un'associazione con una classe particolare. Si puo' chiamare una funzione membro static nel modo ordinario, con il punto o la freccia, in associazione con un oggetto. Tuttavia è più usuale chiamare una funzione membro static da sola, senza nessun oggetto specifico, utilizzando l'operatore di risoluzione di scope, come nell'esempio seguente:
//: C10:FunzioneSempliceMembroStatico.cpp 
class X {
public:
  static void f(){};
};

int main() {
  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: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
} ///:~
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: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;
} ///:~
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:
Uovo u = *Uovo::istanza();
Uovo u2(*Uovo::istanza());
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.

Dipendenza dall' inizializzazione di oggetti statici

All'interno di un'unità di compilazione l'ordine di inizializzazione di oggetti statici è garantito essere uguale all'ordine in cui le definizioni degli oggetti appaiono nel file. L'ordine di distruzione è garantito essere l'inverso di quello di inizializzazione. Tuttavia non c'è nessuna garanzia nell'ordine di inizializzazione di oggetti statici trasversalmente alle unità di compilazione, e il linguaggio non fornisce nessun modo per specificare tale ordine. Questo puo' causare problemi significativi. Come esempio disastroso (che potrebbe causare l'interruzione del sistema operativo e la morte dei processi), se un file contiene
//: C10:Out.cpp {O}
// Primo file
#include <fstream>
std::ofstream out("out.txt"); ///:~
e un altro file usa l'oggetto out in uno dei suoi inizializzatori
//: C10:Oof.cpp
// Secondo file
//{L} Out
#include <fstream>
extern std::ofstream out;
class Oof {
public:
  Oof() { std::out << "ahi"; }
} oof;
int main() {} ///:~
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:

 

extern int y;
int x = y + 1;
e in un secondo file si ha, sempre a livello globale:
extern int x;
int y = x + 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.

Cosa fare

Ci sono tre approcci per gestire questo problema:
  1. Non farlo. Evitare le dipendenze da inizializzazione statica è la soluzione migliore.
  2. Se bisogna farlo, è bene mettere le definizioni critiche di oggetti statici in un solo file, così da controllare la loro inizializzazione in modo portabile, mettendoli nell'ordine giusto.
  3. Se si è convinti che è inevitabile sparpagliare oggetti statici in varie unità di compilazione - come nel caso di una libreria, dove non si puo' controllare l'uso che il programmatore ne fa - ci sono due tecniche di programmazine per risolvere il problema.

Tecnica numero uno

Questa tecnica è stata introdotta da Jerry Schwarz mentre creava la libreria iostream (giacchè le definizioni di cin, cout e cerr sono static e vivono in file separati). Questa è attualmente meno usata della seconda, ma è stata usata per molto tempo e ci si potrebbe imbattere in codice che la usa; percio' è importante capire come funziona. Questa tecnica richiede una classe addizionale nell'header file della libreria. Questa classe è responsabile dell'inizializzazione dinamica degli oggetti statici della libreria. Questo è un semplice esempio:
//: 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 ///:~
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: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;
///:~
(Naturalmente, un'istanza di init viene posta anche in questo file quando l'header viene incluso.) Supponiamo che l'utente della libreria crei altri due file:
//: C10:Inizializzatore.cpp {O}
// Inizializzazione Static
#include "Inizializzatore.h"
///:~
e
//: 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;
} ///:~
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( ).

Tecnica numero due

Dopo tanto tempo che era in uso la prima tecnica, qualcuno (non so chi), ha introdotto la tecnica presentata in questa sezione, che è molto più semplice e chiara della prima. Il fatto che c'è voluto tanto tempo per scoprirla è il tributo da pagare alla complessità del C++. La tecnica si basa sul fatto che gli oggetti statici all'interno delle funzioni vengono inizializzati (soltanto) la prima volta che la funzione viene chiamata. Bisogna ricordare che il problema che stiamo cercando di risolvere non è quando gli oggetti statici vengono inizializzati (cosa che possiamo controllare separatamente), ma piuttosto assicurare che l'inizializzazione avvenga nell'ordine appropriato. Questa tecnica è molto pulita e intelligente. Per ogni oggetto che ha una dipendenza dall'inizializzazione, poniamo un oggetto statico all'interno della funzione che restituisce un riferimento a quell'oggetto. Così, l'unico modo che abbiamo per accedere all'oggetto statico è chiamare la funzione, e se quest'oggetto ha la necessità di accedere ad altri oggetti statici da cui esso dipende, deve chiamare le loro funzioni. E la prima volta che la funzione viene chiamata, forza l'inizializzazione. La correttezza dell'ordine di inizializzazione statica è garantita dal modo in cui viene costruito il codice e non da un ordine arbitrario stabilito dal linker. Per fare un esempio, qui ci sono due classi che dipendono l'una dall'altra. La prima contiene un bool che è inizializzato soltanto dal costruttore, così possiamo verificare se il costruttore è stato chiamato per un'istanza statica della classe (l'area di memorizzazione statica viene inizializzata a zero allo startup del programma, il che da luogo ad un valore false per un bool se il costruttore non è stato chiamato):
//: 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, 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: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 ///:~
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: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();
} ///:~
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:Dipendenza1StatFun.h
#ifndef DIPENDENZA1STATFUN_H
#define DIPENDENZA1STATFUN_H
#include "Dipendenza1.h"
extern Dipendenza1& d1();
#endif // DIPENDENZA1STATFUN_H ///:~
In realtà lo specificatore "extern" è ridondante per la dichiarazione di una funzione. Qui c'è il secondo header file:
//: C10:Dipendenza2StatFun.h
#ifndef DIPENDENZA2STATFUN_H
#define DIPENDENZA2STATFUN_H
#include "Dipendenza2.h"
extern Dipendenza2& d2();
#endif // DIPENDENZA2STATFUN_H ///:~
A questo punto, nei file di implementazione dove prima avremmo messo la definizione degli oggetti statici, mettiamo la definizione delle funzioni contenitrici:
//: C10:Dipendenza1StatFun.cpp {O}
#include "Dipendenza1StatFun.h"
Dipendenza1& d1() {
  static Dipendenza1 dip1;
  return dip1;
} ///:~
Presumibilmente andrebbe messo altro codice in questo file. Segue l'altro file:
//: C10:Dipendenza2StatFun.cpp {O}
#include "Dipendenza1StatFun.h"
#include "Dipendenza2StatFun.h"
Dipendenza2& d2() {
  static Dipendenza2 dip2(d1());
  return dip2;
} ///:~
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:Tecnica2b.cpp
//{L} Dipendenza1StatFun Dipendenza2StatFun
#include "Dipendenza2StatFun.h"
int main() { d2(); } ///:~
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.

Specificazione di linkage alternativi

Cosa succede se stiamo scrivendo un programma in C++ e vogliamo usare una libreria C? Se prendiamo la dichiarazione di funzione C,
float f(int a, char b);
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:
extern "C" 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);
  double d(int a, char b);
}
Oppure, per un header file,
extern "C" {
#include "Mioheader.h"
}
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.

Sommario

La parola chiave static puo' generare confusione, in quanto in alcune situazioni controlla la posizione in memoria, mentre in altre controlla la visibilità e il linkage dei nomi. Con l'introduzione dei namespaces, caratteristica propria del C++, abbiamo un'alternativa migliore e molto più flessibile per controllare la proliferazione dei nomi in progetti grandi. L'uso di static all'interno delle classi è un modo ulteriore di controllare i nomi in un programma. I nomi (all'interno delle classi) non confliggono con quelli globali e la visibilità e l'accesso sono tenuti dentro i confini del programma, fornendo maggior controllo nella manutenzione del codice.

Esercizi

Le soluzioni agli esercizi selezionati possono essere trovate nel documento elettronico The Thinking in C++ Annotated Solution Guide, disponibile in cambio di un piccolo onorario su www.BruceEckel.com.
  1. Creare una funzione con una variabile statica che è un puntatore (con un argomento di default a zero). Quando il chiamante fornisce un valore per questo argomento, questo viene usato per puntare all'inizio di un array di int. Se la funzione viene chiamata senza argomenti (usando l'argomento di default), la funzione restituisce il valore successivo nell'array, fino a quando non incontra il valore "-1" nell'array (che agisce come indicatore di fine array). Utilizzare questa funzione nel main( ).
  2. Creare una funzione che restituisce il valore successivo in una sequenza di Fibonacci, ogni volta che viene chiamata. Aggiungere un argomento che è un bool con valore di default a false cosicchè quando si passa l'argomento con valore true esso "resetta" la funzione al valore iniziale della sequenza di Fibonacci. Utilizzare questa funzione nel main( ).
  3. Creare una classe che memorizza un array di ints. Settare la dimensione dell'array usando static const int all'interno della classe. Aggiungere una variabile const int e inizializzarla dentro la lista di inizializzazione del costruttore; rendere il costruttore inline. Aggiungere una variabile static int e inizializzarla ad un valore specifico. Aggiungere una funzione membro static che stampa il dato membro static. Aggiungere una funzione membro inline chiamata print( ) per stampare tutti I valori nell'array e chiamare tutte le funzioni membro static. Utilizzare questa classe nel main( ).
  4. Creare una classe chiamata Monitor che tiene traccia del numero di volte che la funzione membro incident( ) è stata chiamata. Aggiungere una funzione membro print( ) che mostra il numero di chiamate ad incident(). Ora creare una funzione globale (non membro) che contiene un oggetto di tipo static Monitor. Ogni volta che viene chiamata, questa funzione deve chiamare incident( ), e poi la funzione membro print( ) per stampare il contatore incident. Utilizzare questa funzione nel main( ).
  5. Modificare la classe Monitor dell'Esercizio 4 in modo che si possa decrementare, con decrement( ), il contatore incident. Costruire una classe Monitor2 che accetta come argomento del suo costruttore un puntatore alla classe e che memorizza il puntatore e chiama le funzioni incident( ) e print( ). Nel distruttore di Monitor2 chiamare decrement( ) e print( ). Adesso creare un oggetto static di tipo Monitor2 dentro la funzione. All'interno del main( ), vedere cosa succede con il distruttore di Monitor2 se si chiama oppure non si chiama la funzione.
  6. Creare un oggetto globale di tipo Monitor2 e vedere cosa succede.
  7. Creare una classe con un distruttore che stampa un messaggio e poi chiama exit( ). Creare un oggetto globale di questa classe e vedere cosa succede.
  8. In DistruttoriStatici.cpp, fare delle prove con l'ordine di chiamata del costruttore e del distruttore, con le chiamate alle funzioni f( ) e g( ) dentro il main( ) in ordini diversi. Come si comporta il vostro compilatore, li chiama nell'orrdine corretto (il costruttore e il distruttore, s'intende)?
  9. In DistruttoriStatici.cpp, testare la gestione di default degli errori del vostro ambiente, cambiando la definizione originale di out in una dichiarazione extern e mettendo l'attuale definizione dopo quella di a (il cui costruttore Obj invia informazioni ad out). Assicurarsi che non ci sia nient'altro di importante che gira sulla vostra macchina mentre gira il vostro programma o che la vostra macchina sappia gestire in maniera robusta gli errori.
  10. Provare che le variabili di tipo "file static" (cioè statiche ma visibili solo al livello di file) definite in file di intestazione non collidono l'una con l'altra se incluse in più file cpp.
  11. Creare una semplice classe contenente un int, un construttore che inizializza l'int con il suo argomento, una funzione membro che setta l'int con il suo argomento, e una funzione print( ) che stampa int. Mettere la classe in un header file e includere il file in due file cpp. In un file cpp creare un'istanza della classe, e nell'altro dichiarare questo identificatore come extern e testare il tutto nel main( ). Ricordate che i due file oggetto devono essere linkati o altrimenti il linker non trova gli oggetti.
  12. Porre l'istanza dell'oggetto nell'Esercizio 11 come static e verificare che esso non puo' essere trovato dal linker.
  13. Dichiarare una funzione in un header file. Definire la funzione in un file cpp e chiamarla nel main( ) in un secondo file cpp. Compilare e verificare che funziona. Adesso cambiare la definizione della funzione in modo che sia static e verificare che il linker la puo' trovare.
  14. Modificare Volatile.cpp dal capitolo 8 per trasformare comm::isr( ) in qualcosa che possa funzionare come routine di servizio di un interrupt. Suggerimento: una routine di interrupt non ha argomenti.
  15. Scrivere e compilare un semplice programma che contiene le parole chiave auto e register.
  16. Creare un header file che contiene un namespace. Dentro il namespace creare diverse dichiarazioni di funzioni. Adesso creare un secondo header file che include il primo e continua la definizione del namespace, aggiungendo diverse altre dichiarazioni di funzioni. Adesso creare un file cpp che include il secondo header file. Sostituire il nome del namespace con uno pseudonimo (più corto). All'interno di una definizione di funzione, chiamare una delle funzioni con la risoluzione di scope. All'interno di una definizione di funzione separata, scrivere una direttiva using per inserire il namespace nello scope di questa funzione e mostrare che non è necessario usare la risoluzione di scope per chiamare le funzioni dal namespace.
  17. Creare un header file con un namespace senza nome. Includere l' header in due file cpp separati e mostrare che un namespace senza nome è unico per ogni unità di compilazione.
  18. Usando l'header file dell'Esercizio 17, mostrare che i nomi di un namespace senza nome sono disponibili automaticamente in un'unità di compilazione, senza qualificazione.
  19. Modificare IniezioneFriend.cpp per aggiungere una definizione per una funzione friend e per chiamare la funzione all'interno del main( ).
  20. In Aritmetica.cpp, dimostrare che la direttiva using non si estende al di fuori della funzione in cui la direttiva è stata inserita.
  21. Rimediare al problema in AmbiguitaDaSovrapposizione.cpp, prima con la risoluzione di scope, poi invece usando la dichiarazione using che costringe il compilatore a scegliere una dei nomi di funzione identici.
  22. In due header file, creare due namespace, ciascuno contenente una classe(con tutte definizioni inline) con un nome identico a quella dell'altro namespace. Creare un file cpp che include entrambi gli header file. Creare una funzione e al suo interno usare la direttiva using per introdurre entrmbi i namespace. Provare a creare un oggetto di questa classe e vedere cosa succede. Rendere globale le direttive using (esterne alle funzioni) per vedere se c'è qualche differenza. Risolvere il problema usando la risoluzione di scope e creare oggetti di entrambe le classi.
  23. Rimediare al problema nell'Esercizio 22 con una dichiarazione using che costringe il compilatore a scegliere uno dei nomi di classe identici.
  24. Estrarre le dichiarazioni di namespace in BobsSuperDuperLibrary.cpp e NamespaceSenzaNome.cpp e metterle in header file separati, e dare un nome al namespace attualmente senza nome. In un terzo header file creare un nuovo namespace che combina gli elementi degli altri due namespace con dichiarazioni using. Nel main( ), introdurre il nuovo namespace con una direttiva using e accedere a tutti gli elementi del namespace.
  25. Creare un header file che include <string> e <iostream> ma che non usa nessuna direttiva o dichiarazione using. Aggiungere "include guards" come abbiamo visto negli header file in questo libro. Creare una classe con tutte funzioni inline che contiene un membro di tipo string, con un costruttore che inizializza questa string con il suo argomento e una funzione print( ) che mostra la string. Creare un file cpp e utilizzare la classe nel main( ).
  26. Creare una classe contenente uno static double e un long. Scrivere una funzione membro static che ne stampa i valori.
  27. Creare una classe contenente un int, un costruttore che inizializza l' int con il suo argomento e una funzione print( ) per mostrare l' int. Adesso creare una seconda classe che contiene un oggetto static della prima classe. Aggiungere una funzione membro static che chiama la funzione print( ) dell'oggetto static. Utilizzare la classe nel main( ).
  28. Creare una classe contenente array di int sia const che non-const static. Scrivere metodi static per stampare gli array. Utilizzare la classe nel main( ).
  29. Creare una classe contenente una string, con un costruttore che inizializza la string con il suo argomento e una funzione print( ) per mostrare la string. Creare un'altra classe contenente array di oggetti della prima classe, sia const che non-const static, e metodi static per stampare questi array. Utilizzare questa seconda classe nel main( )
  30. Creare una struct che contiene un int e un costruttore di default che inizializza l' int a zero. Rendere questa struct locale ad una funzione. All'interno della funzione creare un array di oggetti della struct e dimostrare che ciascun int nell' array è stato automaticamente inizializzato a zero.
  31. Creare una classe che rappresenta una connessione ad una stampante e che permette di avere solo una stampante.
  32. In un header file, creare una classe Mirror che contiene due dati membro: un puntatore a un oggetto Mirror e un bool. Fornirgli due costruttori: il costruttore di default inizializza il bool a true e il puntatore Mirror a zero. Il secondo costruttore prende come argomento un puntatore a un oggetto Mirror, che esso assegnerà al puntatore interno all'oggetto; in più setta il bool a false. Aggiungere una funzione membro test( ): se il puntatore all'oggetto è diverso da zero, esso restituisce il valore di test( ) chiamato attraverso il puntatore. Se il puntatore è zero, restituisce il bool. Adesso creare cinque file cpp, ciascuno dei quali include l'header Mirror. Il primo file cpp definisce un oggetto Mirror globale, usando il costruttore di default. Il secondo file dichiara come extern l'oggetto del primo file e definisce un oggetto Mirror globale, usando il secondo costruttore, con un puntatore al primo oggetto. Continuare così fino all'ultimo file, che dovrà contenere anche la definizione di un oggetto globale. In questo file, il main( ) dovrebbe chiamare la funzione test( ) e riportare il risultato. Se il risultato è true, vedere come cambiare l'ordine di link per il vostro linker e cambiare l'ordine fino a quando il risultato è false.
  33. Rimediare al problema nell'Esercizio 32 usando la tecnica numero uno mostrata in questo libro.
  34. Rimediare al problema nell'Esercizio 32 usando la tecnica numero due mostrata in questo libro.
  35. Senza includere header file, dichiarare la funzione puts( ) della Libreria Standard del C. Chiamare questa funzione dal main( ).

[47]Bjarne Stroustrup and Margaret Ellis, The Annotated C++ Reference Manual, Addison-Wesley, 1990, pp. 20-21.
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
Ultima modifica:24/12/2002