next up previous contents index
Next: Funzioni friend Up: Dettagli sulle classi. Templates Previous: Conversioni   Indice   Indice analitico

Membri statici

In C++ è possibile definire variabili di tipo static globali, all'interno di funzioni e all'interno di classi; purtroppo, il significato di static in questi tre casi è profondamente diverso.

Se dichiariamo una variabile globale static vogliamo che essa sia ``interna'' al file, cioè che non possa essere acceduta da un file che non sia quello contenente la dichiarazione. Vediamo i seguenti files di esempio:


// ex10_2_1.cpp

int a;
static int b;

void main() {
  a = 1; b = 2;
}

// ex10_2_2.cpp
#include <iostream.h>

extern int a;
extern int b;

void stampa() {
  cout << a;
  cout << b;
}

Il compilatore non torna nessun messaggio di errore: le variabili a e b sono dichiarate extern in ex10_2_2.cpp, per cui esse verranno risolte in fase di collegamento; tuttavia, quando il linker va a cercare la variabile b nel file ex10_2_1.cpp, trova che essa non può essere utilizzata esternamente a tale file, per cui torna un messaggio di errore del tipo ``undefined reference to `b''' (riferimento a `b' non risolto). Per cui una variabile static va utilizzata quando si ritiene che l'informazione in essa contenuta abbia significato solo ed esclusivamente nel contesto del file nel quale è dichiarata.

Dichiarando una variabile static all'interno di una funzione, l'effetto è ben diverso: la variabile viene creata una ed una sola volta durante la vita del programma; è come se la variabile appartenesse alla funzione vera e propria, piuttosto che alla singola chiamata del processo. Naturalmente l'inizializzazione delle variabili statiche 10.2 avviene una sola volta, nella prima chiamata alla funzione: nelle successive chiamate la definizione della variabile viene ignorata dal compilatore. Vediamo un esempio:


// ex10_2_3.cpp
#include <iostream.h>

void f() {
  // il seguente statement viene valutato una solta volta
  static int contatore = 0;
  contatore++;
  int contatore_sbagliato = 0;
  contatore_sbagliato++;
  cout << "contatore:\t\t" << contatore << "\n";
  cout << "contatore_sbagliato:\t" << contatore_sbagliato << "\n";
}

void main() {
  f();  // prima chiamata
  for (int i = 0; i < 2; i++)
    f();  // chiamate successive
}

output:
 contatore: 1
 contatore_sbagliato: 1
 contatore: 2
 contatore_sbagliato: 1
 contatore: 3
 contatore_sbagliato: 1

La differenza tra una variabile statica ed una locale in una funzione è evidente: la inizializzazione di contatore_sbagliato (int) avviene tutte le volte che la funzione f() viene chiamata, mentre contatore )static int viene inizializzata una ed una sola volta, nella prima chiamata della funzione. In questa maniera possiamo, come nel nostro esempio, avere una variabile che porti il conto delle chiamate di una funzione. Le variabili statiche nelle funzioni servono dunque a condivere un dato tra tutte le chiamate della stessa funzione; naturalmente avremmo potuto ottenere lo stesso risultato con una variabile globale:


// ex10_2_4.cpp
#include <iostream.h>

int contatore;

void f() {
  contatore++;
  cout << "contatore: " << contatore << "\n";
}

void main() {
  contatore = 0;
  f();
  for (int i = 0; i < 2; i++)
    f();
}

output:
contatore: 1
contatore: 2
contatore: 3

L'inconveniente è notevole: se dichiariamo una variabile globale, essa è condivisibile da qualunque funzione del nostro programma, mentre noi vogliamo che la nostra variabile sia propria della funzione, ossia possa perdere coerenza se utilizzata impropriamente.

Il concetto di variabile static in una classe, infine, è del tutto diverso da quello delle variabili statiche globali e locali a funzioni: una variabile statica in una classe è propria della classe stessa, non delle singole istanze create nel programma. Non è un concetto semplice; fino ad ora abbiamo sempre visto una classe come una dichiarazione, la quale viene poi utilizzata per creare degli oggetti, ciascuno dei quali ha i propri campi dati del tutto indipendenti:


// ex10_2_5.cpp
#include <iostream.h>

class A {
  int i;
public:
  A () { i = 0; }
  A& f() { i++; return *this; }  // permette la concatenazione
  void stampa() { cout << i << "\n"; }
};

void main() {
  A x, y;
  x.f().f().f();
  x.stampa();
  y.f();
  y.stampa();
}

output:
3
1

I campi dati privati x.i e y.i corrispondono a due variabili intere del tutto indipendenti tra di loro, separate sia a livello concettuale che logico (cioè esse risiedono in due zone di memoria le quali, teoricamente, non hanno alcun legame tra di esse); tuttavia, entrambe le variabili appartengono ad oggetti della medesima classe. Se vogliamo che un campo dati appartenga esso stesso a nessun oggetto in particolare, bensì alla classe medesima, allora dobbiamo dichiararlo static. Ovviamente l'inizializzazione non dovrà avvenire nel costruttore o in una qualunque delle funzioni membro, ma a livello globale, tramite la sintassi che vediamo nell'esempio che segue.


// ex10_2_6.cpp
#include <iostream.h>

class A {
  static int i;
public:
  A& f() { i++; return *this; }  // permette la concatenazione
  void stampa() { cout << i << "\n"; }
};

// inizializzazione della variabile static i
int A::i = 0;

void main() {
  /* corpo principale identico a ex10_2_5.cpp */
}

output:
3
4

La variabile statica i viene inizializzata a livello globale con il valore 0; ogni volta che la funzione membro f() viene chiamata, i viene incrementata di una unità: siccome essa appartiene alla classe A, non importa se la chiamata avviene su x o su y, il risultato sarà lo stesso. Proprio in quanto le variabili statiche non appartengono a nessun oggetto in particolare, esse possono essere modificate anche senza fare riferimento ad un particolare oggetto classe allocato.


// ex10_2_7.cpp
#include <iostream.h>

class B {
public:
  static int s;
};

int B::s = 0;

void main() {
  // nessun oggetto di tipo B e` stato creato
  B::s = 5;
  cout << B::s << "\n";
}

output:
5

Si capisce allora come abbia senso definire all'interno di una classe anche funzioni static; esse, come le variabili, appartengono alla classe stessa e non alle singole istanze via via create. Naturalmente le funzioni statiche non possono che accedere membri statici (attenzione: abbiamo già visto come le funzioni non statiche possano accedere membri statici), per esse non è cioè definito l'oggetto *this. Vediamo un semplice esempio:


// ex10_2_8.cpp
#include <iostream.h>

class C {
  static int s;
  int ns;
public:
  C() { ns = 0; }
  void stampans() {
    cout << "s: " << s << "\n";   // ok. membro statico
    cout << "ns: " << ns << "\n"; // ok. membro non statico
  }
  static void stampas();
};

int C::s = 0;
void C::stampas() {
  cout << "s: " << s << "\n";      // ok. membro statico
  // cout << "ns: " << ns << "\n"; // NO: membro non statico
}

void main() {
  C x;
  C::stampas();     // ok. funzione statica
  // C::stampans(); // NO: funzione non statica
  x.stampas();      // ok. funzione statica
  x.stampans();     // ok. funzione non statica
}

La classe C contiene due campi dati interi, uno statico e uno non statico, e due funzioni di stampa, anch'esse una statica e una non statica. Come abbiamo detto, la funzione stampas() non può accedere i campi non statici, in quanto l'oggetto *this non è definito; ricordiamo che all'interno di una funzione membro quando una variabile membro viene acceduta, è automaticamente premessa ad essa l'espressione this->. Tuttavia, mentre dal corpo principale possiamo effettuare la chiamata a stampas(), non possiamo fare lo stesso ovviamente per stampans(), la quale è istanziata singolarmente per ogni oggetto creato. Si noti che le funzioni statiche non possono essere definite all'interno della classe.

Utilizzando membri statici possiamo, ad esempio, costruire un contatore per gli oggetti creati:


// ex10_2_9.cpp
#include <iostream.h>

class D {
  static int contatore;
public:
  D() { contatore++; }
  static int n_oggetti();
  ~D() { contatore--; }
};

int D::contatore = 0;
int D::n_oggetti() {
  return contatore;
}

void main() {
  D* array = new D[5];
  cout << D::n_oggetti() << "\n";
  D* d = new D;
  cout << D::n_oggetti() << "\n";
  delete[] array;
  cout << D::n_oggetti() << "\n";
}

output:
5
6
1

Si tratta di una tecnica molto semplice che può essere utilizzata in fase di debug di un programma, in modo tale da avere sempre sotto controllo il numero di oggetti che si allocano. Un altro esempio potrebbe essere il seguente:


// ex10_2_10.cpp
#include <iostream.h>

class Aspirapolvere {
  static int nds;  // numero di serie
  /* tanti campi dati */
public:
  Aspirapolvere() { /* tante operazioni */ }
  static void nuovo(int n = 0);
  static int totale();
  static void distruggi(int n); 
  ~Aspirapolvere() { /* ... */ }
};

int Aspirapolvere::nds = 0;
void Aspirapolvere::nuovo (int n) {
  if (n >= 0)
    nds += n;
}

int Aspirapolvere::totale () {
  return nds;
}

void Aspirapolvere::distruggi (int n) {
  if (n >= 0)
    nds -= n;
  if (nds < 0)
    nds = 0;
}

void main() {
  Aspirapolvere::nuovo (5);
  cout << Aspirapolvere::totale() << "\n"; // 5
  Aspirapolvere::nuovo (-1); // nessun effetto
  Aspirapolvere::distruggi (2);
  cout << Aspirapolvere::totale() << "\n"; // 3
  Aspirapolvere::distruggi (100);
  cout << Aspirapolvere::totale() << "\n"; // 0
}

Si noti come l'intero statico nds non possa assumere un valore qualunque, ma resti comunque positivo; infatti è ben difficile che si riesca a produrre aspirapolveri negativi.

ex-2
si programmi una classe che abbia una funzione statica, la quale ritorni il numero di volte che la funzione f() (una seconda funzione non statica) viene chiamata;
ex-3
si programmi una classe il cui costruttore stampi una stringa del tipo classeXXX, ove al posto di XXX ci sia il numero di oggetti instanziati di tale classe


next up previous contents index
Next: Funzioni friend Up: Dettagli sulle classi. Templates Previous: Conversioni   Indice   Indice analitico
Claudio Cicconetti
2000-09-06