Membri static

Normalmente istanze diverse della stessa classe non condividono direttamente risorse di memoria, l'unica possibilita` sarebbe quella di avere puntatori che puntano allo stesso indirizzo, per il resto ogni nuova istanza riceve nuova memoria per ogni attributo. Tuttavia in alcuni casi e` desiderabile che alcuni attributi fossero comuni a tutte le istanze; per utilizzare un termine tecnico, si vuole realizzare una comunicazione ad ambiente condiviso. Si pensi ad esempio ad un contatore che indichi il numero di istanze di una certa classe in un certo istante...
Per rendere un attributo comune a tutte le istanze occorre dichiararlo static:


  class MyClass {
    public:
      MyClass();
      /* ... */

    private:
      static int Counter;
      char * String;
      /* ... */
  };


Gli attributi static possono in pratica essere visti come elementi propri della classe, non dell'istanza. In questo senso non e` possibile inizializzare un attributo static tramite la lista di inizializzazione del costruttore, tutti i metodi (costruttore compreso) possono accedere sia in scrittura che in lettura all'attributo, ma non si puo` inizializzarlo tramite un costruttore:


  MyClass::MyClass() : Counter(0) {    // Errore!
    /* ... */
  }


Il motivo e` abbastanza ovvio, qualunque operazione sul membro static nel corpo del costruttore verrebbe eseguita ogni volta che si istanzia la classe, una inizializzazione eseguita tramite costruttore verrebbe quindi ripetuta piu` volte rendendo inutili i membri statici.
L'inizializzazione di un attributo static va eseguita successivamente alla sua dichiarazione ed al di fuori della dichiarazione di classe:


  class MyClass {
    public:
      MyClass();
      /* ... */

    private:
      static int Counter;
      char * String;
      /* ... */
  };

  int MyClass::Counter = 0;


Successivamente l'accesso a un attributo static avviene come se fosse un normale attributo, in particolare l'idea guida dell'esempio era quella di contare le istanze di classe MyClass esistenti in un certo momento; i costruttori e il distruttore sarebbero stati quindi piu` o meno cosi`:


  MyClass::MyClass() : /* inizializzazione membri */
                       /* non static */
  {
    ++Counter;         // Ok, non e` una inizializzazione
    /* ... */
  }

  MyClass::~MyClass() {
    --Counter;         // Ok!
    /* ... */
  }


Oltre ad attributi static e` possibile avere anche metodi static; la keyword static in questo caso vincola il metodo ad accedere solo agli attributi statici della classe, un accesso ad un attributo non static costituisce un errore:


  class MyClass {
    public:
      static int GetCounterValue();
      /* ... */

    private:
      static int Counter = 0;
      /* ... */
  };

  int MyClass::GetCounterValue() {
    return Counter;
  }


Si noti che nella definizione della funzione membro statica la keyword static non e` stata ripetuta, essa e` necessaria solo nella dichiarazione (anche in caso di definizione inline). Ci si puo` chiedere quale motivo ci possa essere per dichiarare un metodo static, ci sono essenzialmente tre giustificazioni:

  • maggiore controllo su possibili fonti di errore: dichiarando un metodo static, chiediamo al compilatore di accertarsi che il metodo non acceda ad altre categorie di attributi;
  • minor overhead di chiamata: i metodi non static per sapere a quale oggetto devono riferire, ricevono dal compilatore un puntatore all'istanza di classe per la quale il metodo e` stato chiamato; i metodi static per loro natura non hanno bisogno di tale parametro e quindi non richiedono tale overhead;
  • i metodi static oltre a poter essere chiamati come un normale metodo, associandoli ad un oggetto (con la notazione del punto), possono essere chiamati come una normale funzione senza necessita` di associarli ad una particolrare istanza, ricorrendo al risolutore di scope come nel seguente esempio:

    
      MyClass Obj;
      int Var1 = Obj.GetCounterValue();       // Ok!  
      int Var2 = MyClass::GetCounterValue();  // Ok!  
Non e` possibile dichiarare static un costruttore o un distruttore.



Membri const e mutable

Oltre ad attributi di tipo static, e` possibile avere attributi const; in questo caso pero` l'attributo const non e` trattato come una normale costante: esso viene allocato per ogni istanza come un normale attributo, tuttavia il valore che esso assume per ogni istanza viene stabilito una volta per tutte all'atto della creazione dell'istanza stessa e non potra` mai cambiare durante la vita dell'oggetto. Il valore di un attributo const, infine, va settato tramite la lista di inizializzazione del costruttore:


  class MyClass {
    public:
      MyClass(int a, float b);
      /* ... */

    private:
      const int ConstMember;
      float AFloat;
  };

  MyClass::MyClass(int a, float b)
         : ConstMember(a), AFloat(b) { };


Il motivo per cui bisogna ricorrere alla lista di inizializzazione e` semplice: l'assegnamento e` una operazione proibita sulle costanti, l'operazione che si compie tramite la lista di inizializzazione e` invece concettualmente diversa (anche se per i tipi primitivi e` equivalente ad un assegnamento), la cosa diverra` piu` evidente quando vedremo che il generico membro di una classe puo` essere a sua volta una istanza di una generica classe.

E` anche possibile avere funzioni membro const analogamente a quanto avviene per le funzioni membro statiche. Dichiarando un metodo const si stabilisce un contratto con il compilatore: la funzione membro si impegna a non accedere in scrittura ad un qualsiasi attributo della classe e il compilatore si impegna a segnalare con un errore ogni tentativo in tal senso. Oltre a cio` esiste un altro vantaggio a favore dei metodi const: sono gli unici a poter essere eseguiti su istanze costanti (che per loro natura non possono essere modificate). Per dichiarare una funzione membro const e` necessario far seguire la lista dei parametri dalla keyword const, come mostrato nel seguente esempio:


  class MyClass {
    public:
      MyClass(int a, float b) : ConstMember(a),
                                AFloat(b) {};
      
      int GetConstMember() const {
        return ConstMember;
      }
     
      void ChangeFloat(float b) {
        AFloat = b;
      }

    private:
      const int ConstMember;
      float AFloat;
  };

  int main(int, char* []) {
    MyClass A(1, 5.3);
    const MyClass B(2, 3.2);

    A.GetConstMember();     // Ok!
    B.GetConstMember();     // Ok!
    A.ChangeFloat(1.2);     // Ok!
    B.ChangeFloat(1.7);     // Errore!
    return 0;
  }


Si osservi che se la funzione membro GetConstMember() fosse stata definita fuori dalla dichiarazione di classe, avremmo dovuto nuovamente esplicitare le nostre intenzioni:


  class MyClass {
    public:
      MyClass(int a, float b) : ConstMember(a),
                                AFloat(b) {};
      
      int GetConstMember() const;

      /* ... */
  };

  int MyClass::GetConstMember() const {
    return ConstMember;
  }


Avremmo dovuto cioe` esplicitare nuovamente il const (cosa che non avviene con le funzioni membro static).
Come per i metodi static, non e` possibile avere costruttori e distruttori const (sebbene essi vengano utilizzati per costruire e distruggere anche le istanze costanti).

Talvolta puo` essere necessario che una funzione membro costante possa accedere in scrittura ad uno o piu` attributi della classe, situazioni di questo genere sono rare ma possibili (si pensi ad un oggetto che mappi un dispositivo che debba trasmettere dati residenti in ROM attraverso una porta hardware, solo metodi const possono accedere alla ROM...). Una soluzione potrebbe essere quella di eseguire un cast per rimuovere la restrizione del const, ma una soluzione di questo tipo sarebbe nascosta a chi usa la classe.
Per rendere esplicita una situazione di questo tipo e` stata introdotta la keyword mutable, un attributo dichiarato mutable puo` essere modificato anche da funzioni membro costanti:


  class AccessCounter {
    public:
      AccessCounter();
      const double GetPIValue() const;
      const int GetAccessCount() const;

    private:
      const double PI;
      mutable int Counter;
  };

  AccessCounter::AccessCounter() : PI(3.14159265),
                                   Counter(0) {}

  const double AccessCounter::GetPIValue() const {
    ++Counter;       // Ok!
    return PI;
  }

  const int AccessCounter::GetAccessCount() const {
    return Counter;
  }


L'esempio (giocattolo) mostra il caso di una classe che debba tenere traccia del numero di accessi in lettura ai suoi dati, senza mutable e senza ricorrere ad un cast esplicito la soluzione ad un problema simile sarebbe stata piu` artificiosa e complicata.



Costanti vere dentro le classi

Poiche` gli attributi const altro non sono che attributi a sola lettura, ma che vanno inizializzati tramite lista di inizializzazione, e` chiaro che non e` possibile scrivere codice simile:


  class BadArray {
    public:
      /* ... */

    private:
      const int Size;
      char String[Size];      // Errore!
  };


perche` non si puo` stabilire a tempo di compilazione il valore di Size. Le possibili soluzioni al problema sono due.
La soluzione tradizionale viene dalla keyword enum; se ricordate bene, e` possibile stabilire quali valori interi associare alle costanti che appaiono tra parentesi graffe al fine di rappresentarle.
Nel nostro caso dunque la soluzione e`:


  class Array {
    public:
      /* ... */

    private:
      enum { Size = 20 };
      char String[Size];      // Ok!
  };


Si osservi che la keyword enum non e` seguita da un identificatore, ma direttamente dalla parentesi graffa; il motivo e` semplice: non ci interessava definire un tipo enumerato, ma disporre di una costante, e quindi abbiamo creato una enumerazione anonima il cui unico effetto in questo caso e` quello di creare una associazione nome-valore all'interno della tabella dei simboli del compilatore.
Questa soluzione, pur risolvendo il nostro problema, soffre di una grave limitazione: possiamo avere solo costanti intere. Una soluzione definitiva al nostro problema la si trova utilizzando contemporaneamente le keyword static e const:


  class Array {
    public:
      /* ... */

    private:
      static const int Size = 20;
      char String[Size];            // Ok!
  };


Essendo static, Size viene inizializzata prima della creazione di una qualunque istanza della classe ed essendo const il suo valore non puo` essere modificato e risulta quindi prefissato gia` a compile time. Le costanti dichiarate in questo modo possono avere tipo qualsiasi e in questo caso il compilatore puo` non allocare alcuna memoria per esse, si ricordi solo che non tutti i compilatori potrebbero accettare l'inizializzazione della costante nella dichiarazione di classe, in questo caso e` sempre possibile utilizzare il metodo visto per gli attributi static:


  class Array {
    public:
      /* ... */

    private:
      static const int Size;
      char String[Size];          // Ok!
  };

  const int Array::Size = 20;


Anche in questo caso non bisogna riutilizzare static in fase di inizializzazione.



Membri volatile

Il C++ e` un linguaggio adatto a qualsiasi tipo di applicazione, in particolare a quelle che per loro natura si devono interfacciare direttamente all'hardware. Una prova in tal proposito e` fornita dalla keyword volatile che posta davanti ad un identificatore di variabile comunica al compilatore che quella variabile puo` cambiare valore in modo asincrono rispetto al sistema:


  volatile int Var;

  /* ... */
  int B = Var;
  int C = Var;
  /* ... */


In tal modo il compilatore non ottimizza gli accessi a tale risorsa e ogni tentativo di lettura di quella variabile e` tradotto in una effettiva lettura della locazione di memoria corrispondente.
Gli oggetti volatile sono normalmente utilizzati per mappare registri di unita` di I/O all'interno del programma e per essi valgono le stesse regole viste per gli oggetti const; in particolare solo funzioni membro volatile possono essere utilizzate su oggetti volatile e non si possono dichiarare volatile costruttori e distruttori (che sono comunque utilizzabili sui tali oggetti):


  class VolatileClass {
    public:
      VolatileClass(int ID);
      long int ReadFromPort() volatile;
      /* ... */

    private:
      volatile long int ComPort;
      const int PortID;
      /* ... */
  };

  VolatileClass::VolatileClass(int ID) : PortID(ID) {}

  long int VolatileClass::ReadFromPort() volatile {
    return ComPort;
  }


Si noti che volatile non e` l'opposto di const: quest'ultima indica al compilatore che un oggetto non puo` essere modificato indipendentemente che sia trattato come una vera costante o una variabile a sola lettura, volatile invece dice che l'oggetto puo` cambiare valore al di fuori del controllo del sistema; quindi e` possibile avere oggetti const volatile. Ad esempio unita` di input, come la tastiera, possono essere mappati tramite oggetti dichiarati const volatile:


  class Keyboard {
    public:
      Keyboard();
      const char ReadInput() const volatile;
      /* ... */

    private:
      const volatile char* Buffer;
  };





Dichiarazioni friend

In taluni casi e` desiderabile che una funzione non membro possa accedere direttamente ai membri (attributi e/o metodi) privati di una classe. Tipicamente questo accade quando si realizzano due o piu` classi, distinte tra loro, che devono cooperare per l'espletamento di un compito complessivo e si vogliono ottimizzare al massimo le prestazioni, oppure semplicemente quando ad esempio si desidera eseguire l'overloading degli operatori ostream& operator<<(ostream& o, T& Obj) e istream& operator>>(istream& o, T& Obj) per estendere le operazioni di I/O alla classe T che si vuole realizzare. In situazioni di questo genere, una classe puo` dichiarare una certa funzione friend (amica) abilitandola ad accedere ai propri membri privati.
Il seguente esempio mostra come eseguire l'overloading dell'operatore di inserzione in modo da poter visualizzare il contenuto di una nuova classe:


  #include < iostream >
  using namespace std;

  class MyClass {
    public:
      /* ... */

    private:
      float F1, F2;
      char C;
      void Func();
      /* ... */

    friend ostream& operator<<(ostream& o, MyClass& Obj);
  };

  void MyClass::Func() {
    /* ... */
  }

  // essendo stato dichiarato friend dentro MyClass, il
  // seguente operatore puo` accedere ai membri privati
  // della classe come una qualunque funzione membro.
  ostream& operator<<(ostream& o, MyClass& Obj) {
    o << Obj.F1 << ' ' << Obj.F2 << ' ' << Obj.C;
    return o;
  }


in tal modo diviene possibile scrivere:


  MyClass Object;
  /* ... */
  cout << Object;


L'esempio comunque risultera` meglio comprensibile quando parleremo di overloading degli operatori, per adesso e` sufficiente considerare ostream& operator<<(ostream& o, MyClass& Obj) alla stessa stregua di una qualsiasi funzione.
La keyword friend puo` essere applicata anche a un identificatore di classe, abilitando cosi` una classe intera


  class MyClass {
    /* ... */
    friend class AnotherClass;
  };


in tal modo qualsiasi membro di AnotherClass puo` accedere ai dati privati di MyClass.
Si noti infine che deve essere la classe proprietaria dei membri privati a dichiarare una funzione (o una classe) friend e che non ha importanza la sezione (pubblica, protetta o privata) in cui tale dichiarazione e` fatta.



Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - seconda edizione
© Copyright 1996-1999, Paolo Marotta