next up previous contents index
Next: Sovrapposizione degli operatori Up: Introduzione alle classi Previous: Costruttori e distruttore   Indice   Indice analitico

L'oggetto this e i membri const

Sebbene non sia mai stato detto esplicitamente, il C++ possiede un sistema di protezione degli identificatori il quale, ad esempio, ci permette di utilizzare per due variabili diverse gli stessi nomi. Sappiamo bene, ad esempio, che è il seguente programma è lecito:

// 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;
e successivamente chiamiamo la funzione 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; }
}
in quanto all'interno di 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; }
};
sarà il compilatore a scegliere la funzione da utilizzare a seconda che l'oggetto istanziato sia costante o no.

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.


next up previous contents index
Next: Sovrapposizione degli operatori Up: Introduzione alle classi Previous: Costruttori e distruttore   Indice   Indice analitico
Claudio Cicconetti
2000-09-06