// ex9_4_1.cpp #include <iostream.h> int n; // variabile GLOBALE void main() { n = 7; // variabile GLOBALE cout << n << "\n"; // stampa 7 { int n = 3; // variabile LOCALE al blocco { } cout << n << "\n"; // stampa 3 } cout << n << "\n"; // stampa 7 }
Esiste un operatore, detto risolutore di visibilità globale e
indicato con i doppi due punti (::
), che abbiamo già incontrato a
proposito della definizione delle funzioni membro di una classe; esso, per
così dire, risale alla definizione globale di una variabile, una funzione,
una classe o un qualunque altro costrutto del C++. Per esempio diventa
possibile accedere una variabile globale dall'interno di un blocco:
// ex9_4_1.cpp #include <iostream.h> int n; // variabile GLOBALE void main() { n = 7; // variabile GLOBALE { int n = 3; // variabile LOCALE al blocco { } cout << n << "\n"; // stampa 3 cout << ::n << "\n"; // stampa 7 } }
output:
3
7
Se l'operatore ::
è preceduto dal nome di una struttura o di una
classe esso ci permette di accedere ai membri di essa, se si ha il permesso di
farlo (cioè se il membro è stato dichiarato public); vedremo
in seguito l'importanza di tale operatore per i membri statici delle classi.
La protezione dei nomi (i quali vengono incapsulati in namespace) avviene in maniera del tutto automatica; ad esempio se consideriamo la seguente classe:
class MiaClasse { int n; public: MiaClasse (int N) { n = N; } void raddoppia() { n *= 2; } } |
il compilatore si occupa di fare in modo che, se creiamo un oggetto con:
MiaClasse m; |
m.raddoppia()
, all'interno
della funzione venga sostituito n
con m.n
, che è il nome
``vero'' di tale variabile. Cioè esiste un particolare oggetto, il quale è
puntato tramite la parola chiave this, che all'interno della
definizione di una classe rappresenta l'oggetto istanziato; la classe
MiaClasse
può essere definita equivalentemente nel seguente modo:
class MiaClasse { int n; public: MiaClasse (int N) { this->n = N; } void raddoppia() { this->n *= 2; } } |
MiaClasse
la variabile n
è un campo
degli oggetti che saranno creati nel programma, e this punta proprio
a tali oggetti; il tipo di this nel precedente caso è
MiaClasse*
, in generale esso un puntatore alla classe. Per evitare
confusione e perdita di tempo, si è ben pensato di evitare che il
programmatore dovesse scrivere this->
ogni volta che chiama un membro
di una classe dall'interno di essa, facendo in modo che fosse il compilatore
a gestire i nomi; dunque abbiamo la possibilità di scrivere n
ogni
volta che leggiamo o scriviamo sul campo n
della classe
MiaClasse
, piuttosto che this->n
, come sarebbe più esatto.
Come è ovvio, nessuno utilizza l'oggetto this per identificare un membro in una classe; esso è invece utilizzato ogni volta che si vuole che una funzione membro torni l'oggetto stesso cui essa appartiene. Ad esempio, è probabile che una funzione che modifica i campi di un oggetto classe voglia tornare l'oggetto stesso, al fine di operare su di esso in lettura o in ulteriore modifica. Vediamo il seguente esempio:
// numeropiccolo.h // classe che rappresenta numeri interi // minori di un numero fissato in fase // di creazione dell'oggetto classe #include <iostream.h> const int MAX_DEFAULT = 12; class NumeroPiccolo { int numero; int max; public: NumeroPiccolo (int Max = MAX_DEFAULT) { if (Max > 0) max = Max; // max deve essere positivo else max = MAX_DEFAULT; // altrimenti viene utilizzato numero = 0; } // un valore predefinito NumeroPiccolo& modifica (int N) { if ((N >= 0) && (N <= max)) numero = N; return *this; } NumeroPiccolo& dimezza () { numero /= 2; return *this; } void stampa () { cout << numero << "\n"; } // ~NumeroPiccolo(); // tutti i campi sono non dinamici };
Il seguente file sia di prova per la classe NumeroPiccolo
:
// ex9_4_3.cpp #include "numeropiccolo.h" void main() { NumeroPiccolo np (100); np.modifica(39); np.stampa(); np.modifica(200); np.stampa(); np.modifica(8).dimezza().dimezza().dimezza(); np.stampa(); // np.stampa().dimezza(); // no: stampa ritorna void }
output:
39
39
1
Siccome sia la funzione modifica
che dimezza
tornano degli
oggetti che sono del tipo riferimento a NumeroPiccolo
, possiamo fare in
modo che la funzione successiva abbia come argomento proprio tali oggetti; si
dice in tale caso che abbiamo reso possibile la concatenazione delle
funzioni modifica
e dimezza
. La concatenazione, ovviamente, non
è possibile perché stampa
torna void e non un oggetto di
tipo NumeroPiccolo
. La concatenazione delle funzioni può avvenire
anche facendo in modo che esse tornino dei puntatori alla classe, ovvero
l'oggetto this semplicemente piuttosto che *this
; vediamo il
seguente esempio:
// numerogrande.h // classe che permette la rappresentazione di // numeri non piu' piccoli di uno fissato #include <iostream.h> const int MIN_DEFAULT = 100; class NumeroGrande { int numero; int min; public: NumeroGrande (int Min = MIN_DEFAULT) { if (Min > 0) min = Min; else min = MIN_DEFAULT; numero = min; } NumeroGrande* modifica (int N) { if (N >= min) numero = N; return this; } NumeroGrande* raddoppia () { numero *= 2; return this; } void stampa () { cout << numero << "\n"; } };
una cui funzione principale di prova è:
// ex9_4_4.cpp #include "numerogrande.h" void main() { NumeroGrande ngs; // non dinamico NumeroGrande* ngd = new NumeroGrande; // non dinamico ngs.modifica(250)->raddoppia()->raddoppia(); ngd->modifica(120)->raddoppia()->raddoppia(); ngs.stampa(); ngd->stampa(); }
output:
1000
480
La NumeroGrande
è completamente speculare alla NumeroPiccolo
,
solo che le funzioni membro di essa tornano puntatori a this
piuttosto che riferimenti ad esso; solitamente è una idea migliore adottare
la tecnica utilizzata in NumeroPiccolo
perché si riduce il rischio di
errori nell'utilizzo della classe.
Abbiamo dunque visto che this all'interno di una classe rappresenta
l'oggetto istanziato; ci sono funzioni che modificano i campi di tale oggetto
e altre che lo lasciano invariato: nella NumeroPiccolo
, ad esempio,
modifica
e dimezza
modificano il campo dato n
mentre
stampa
lascia tutti i campi dati inalterati. Ovviamente, le funzioni
costruttore e distruttore modificano sempre i campi dati di un oggetto classe.
Le funzioni che lasciano i dati dell'oggetto inalterato, ovvero quelle che non
modificano this, possono essere dichiarate const; il
vantaggio è il seguente: esse sono più potenti in quanto possono essere
utilizzate con oggetti costanti e non costanti. Vediamo il seguente esempio,
nel quale si mettono a confronto due classi identiche, A
e B
,
delle quali però la prima ha alcune funzioni dichiarate const.
// ex9_4_5.cpp #include <iostream.h> // contiene una funzione const class A { int a; public: A () { a = 0; } int valore () const { return a; } A& modifica (int A) { a = A; } }; class B { int b; public: B () { b = 0; } int valore () { return b; } B& modifica (int B) { b = B; } }; void main() { A a; const A a_costante; B b; const B b_costante; a.modifica(7); // ok // a_costante.modifica(7); // no! cout << a.valore() << "\n" << b.valore() << "\n"; cout << a_costante.valore() << "\n"; // cout << b_costante.valore(); // no! valore() e` non const }
Siccome l'oggetto b_costante
è stato dichiarato costante, le sole
funzioni che possono essere utilizzate su di esso sono quelle const;
si noti che la sovrapposizione delle funzioni può avvenire anche solo per
l'attributo const: è possibile, ad esempio, dichiarare la classe
B
come segue:
class B { int b; public: B () { b = 0; } int valore () { return b; } int valore () const { return b; } B& modifica (int B) { b = B; } }; |
In questa sezione abbiamo fatto la conoscenza con l'oggetto this, il
quale ha, come vedremo, un ruolo fondamentale nella sovrapposizione degli
operatori del C++; inoltre abbiamo visto che, in generale, conviene dichiarare
costanti le funzioni che non modificano l'oggetto this, perché è
possibile sfruttare tali funzioni anche con oggetti dichiarati costanti. Il
caso più ovvio di funzione costante è la funzione di accesso (in lettura):
siccome le classi in C++ forniscono la possibilità di incapsulare la
struttura dati in maniera che un utilizzatore della classe non possa
accederla, è molto comune scrivere delle funzioni che permettono di disporre
in lettura dei dati sottostanti l'oggetto classe, come la funzione
valore
nelle classi A
e B
di ex9_4_5.cpp
.