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
|