next up previous contents index
Next: Sovrapposizione di funzioni Up: Puntatori e riferimenti Previous: Puntatori a costanti e   Indice   Indice analitico

Riferimenti

Ora che abbiamo a sufficienza assimilato (speriamo ...) il concetto di puntatore, possiamo passare ai riferimenti, di pari importanza rispetto ai puntatori e molto simili ad essi per diversi aspetti. Primo tra tutti la sintassi di definizione, che è la medesima dei puntatori, solo che al posto di * c'è il simbolo &, che abbiamo già incontrato a proposito degli operatori logici (AND = &&), di confronto bit a bit (AND bit a bit = &) e come operatore ``indirizzo di'' (&). Così come abbiamo fatto per i puntatori, anche in tal caso si invita a non fare confusione tra i diversi significati che il simbolo & possiede in C++ ( purtroppo i simboli disponibili su di una tastiera sono limitati, non ci si può far nulla ...). Esempi di riferimento sono pertanto:
 int& a int &a riferimento di una variabile intera
 double& a double &a riferimento di una variabile reale
per i quali valgono i discorsi analoghi condotti sui puntatori, circa la posizione dell'operatore derivatore di tipo &.

Un riferimento, come dice la parola stessa, è una variabile che ``si riferisce'' ad un'altra, ne costituisce pertanto un alias, un sinonimo; ecco la seconda importante somiglianza con i puntatori, i quali anch'essi possono essere alias di oggetti, in particolare di variabili, se preceduti dall'operatore *. Nel caso dei riferimenti il legame tra essi e le variabili riferite è più stretto di quello esistente tra puntatori e variabili puntate: un puntatore contiene l'indirizzo di una variabile e, a meno che esso non sia stato dichiarato costante, è possibile modificare tale indirizzo, inserendoci quello di variabili sempre diverse. Al contrario, ogni riferimento va inizializzato, ossia gli va assegnato un valore iniziale, e il suo valore non può cambiare: la variabile riferita rimarrà sempre la medesima, per tutta la vita del riferimento (la quale si computa con gli stessi criteri delle altre variabili e dei puntatori). Vediamo un esempio:


//ex5_5_1.cpp
#include <iostream.h>
void main() {
  int a = 6, b = 3;
  // int& r; // NO: errore
  // `r' declared as reference but not initialized
  // `r' dichiarato come riferimento ma non inizializzato
  int& r = a;
  cout << "a = " << a <<
    "\tr = " << r << 
    "\tb = " << b << "\n";
  r = b;  // in realta' e' come scrivere a = b
  cout << "a = " << a <<
    "\tr = " << r << 
    "\tb = " << b << "\n";
}

output:
 a = 6 r = 6 b = 3
 a = 3 r = 3 b = 3

Prima osservazione: il compilatore ci torna un messaggio di errore se cerchiamo di dichiarare un riferimento, mentre è possibile dichiarare un puntatore, come vediamo nel seguente esempio, che si invita a confrontare con il precedente:


//ex5_5_2.cpp
#include <iostream.h>
void main() {
  int a = 6, b = 3;
  int* r; // OK
  r = &a;
  cout << "a = " << a <<
    "\tr = " << *r << 
    "\tb = " << b << "\n";
  r = &b;
  cout << "a = " << a <<
    "\tr = " << *r << 
    "\tb = " << b << "\n";
}

output:
 a = 6 r = 6 b = 3
 a = 6 r = 3 b = 3

Come si vede adesso il valore della variabile a non viene modificato, come invece avveniva nell'esempio ex5_5_1.cpp. I riferimenti hanno le caratteristiche dei puntatori costanti: devono essere inizializzati e non possono cambiare la variabile di cui sono alias; essi però a questo scopo posseggono una maggiore chiarezza espressiva, in quanto non necessitano dell'operatore ``indirizzo di'' (&) o dell'operatore *. Si veda il seguente:


// ex5_5_3.cpp
void main() {
  int a = 3, b = 7;
  int* const p_a = &a;
  int& r_a = a; // non occorre l'operatore &
  // p_a = &b;  // NO: errore
  // r_a = b;   // ok ma r_a resta alias di a
                // equivale a 'a = b'
  b += *p_a;    // b = 10
  b += r_a;     // b = 13 // non occorre l'operatore *
}

Anche nel caso si utilizzino i riferimenti come argomenti di funzioni, il vantaggio dal punto di vista estetico è notevole; diminuisce di pari passo dunque anche il rischio di dimenticare l'operatore & o *. Come esempio riassuntivo sulle modalità di passaggio degli argomenti alle funzioni, vediamo il seguente:


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

// passaggio per valore
// sbagliata: funziona solo se a == b
void scambia1 (int a, int b) {
  int t = a;
  a = b;
  b = t;
}

// passaggio per indirizzo
void scambia2 (int* a, int* b) {
  int t = *a;
  *a = *b;
  *b = t;
}

// passaggio per riferimento
void scambia3 (int& a, int& b) {
  int t = a;
  a = b;
  b = t;
}

void main() {
  int a = 3, b = 7;
  cout << "a = " << a << "\tb = " << b << "\n";
  scambia1 (a, b); // non scambia
  cout << "a = " << a << "\tb = " << b << "\n";
  scambia2 (&a, &b);
  cout << "a = " << a << "\tb = " << b << "\n";
  scambia3 (a, b);
  cout << "a = " << a << "\tb = " << b << "\n";
}

output:
 a = 3 b = 7
 a = 3 b = 7
 a = 7 b = 3
 a = 3 b = 7

Vediamo una per una le diverse modalità di passaggio degli argomenti. La prima funzione (scambia1) utilizza il passaggio per valore; è importante capire perché non vengono in realtà scambiate le due variabili a e b nella funzione main: quando viene effettuata la chiamata alla funzione scambia1, i valori di a e b della main servono ad inizializzare le variabili a e b della scambia1, le quali sono ben distinte dalle loro omonime. Successivamente si scambiano tra di loro queste due variabili; in realtà a e b della main non sono state mai toccate, se non in fase di lettura al momento della chiamata. Al contrario, nella seconda funzione (scambia2), vengono passati gli indirizzi di a e b della main, per cui lo scambio avviente effettivamente fra di esse. L'unico neo consiste nel fatto che bisogna stare attenti, se si utilizza il passaggio per indirizzo, a due cose: passare alla funzione gli indirizzi delle variabili nelle funzione chiamante, e utilizzare l'operatore * per i puntatori nella funzione chiamata. La soluzione più pulita consiste infine nel passaggio per riferimento (funzione scambia3), che è praticamente identica alla scambia1 però funziona, perché le variabili a e b sono riferimenti, e dunque alias di a e b della funzione main.

I puntatori in C++ sono un bagaglio acquisito dalla compatibilità con il C, nel quale essi erano davvero indispensabili: il programmatore era costantemente posto di fronte ad operazioni a basso livello sulla memoria, con enormi ed evidenti difficoltà nello scrivere dei programmi stabili e affidabili. Essendo invece il C++ un linguaggio di programmazione ad oggetti molto astratto, i puntatori si sarebbero tranquillamente potuti trascurare, utilizzando esclusivamente i riferimenti che, come si vede già da primi semplici esempi, sono molto più comodi da gestire; un esempio di linguaggio ad alto livello che non possiede i puntatori è l'ormai celebre Java. Comunque, in C++ i puntatori ci sono, e dunque vanno usati; si cerchi comunque di non confonderli con i riferimenti, o viceversa, ricordando che questi ultimi costituiscono una maggiore astrazione rispetto ai puntatori. Si noti infatti che il riferimento ad un oggetto, è in realtà l'oggetto stesso; è quindi naturale che i riferimenti siano da preferire ai puntatori come argomenti di funzioni: perché portare dentro una funzione un misterioso indirizzo (che va ``catturato'' tramite l'operatore &, che spesso si dimentica ...) quando si può avere l'oggetto bello e pronto. Nel caso non si intenda modificare tale oggetto nella funzione chiamata, lo si dichiara poi costante ed il gioco è fatto. Vediamo il seguente:


// ex5_5_5.cpp
// calcola il perimetro di un quadrato
#include <iostream.h>

double perimetro (const double& lato) {
  return lato * 4;
}

void rendiPositivo (double& lato) {
  if (lato < 0)
    lato = -lato;
}

void main() {
  double lato;
  cout << "lato quadrato? "; cin >> lato;
  rendiPositivo (lato);
  cout << "perimetro = " << perimetro (lato);
}

esempio di output:
lato quadrato? -5.5
perimetro = 22

La funzione rendiPositivo modifica la variabile lato, nel caso essa sia negativa, per cui il riferimento passatole non può essere costante; invece la funzione perimetro sicuramente non modifica tale variabile, la quale può quindi tranquillamente essere passata per riferimento costante; in questa maniera la funzione è più potente rispetto alla stessa funzione con il riferimento non costante. Perché? Si veda il seguente esempio, che mette a confronto due funzioni identiche, che calcolano l'area di un quadrato:


// ex5_5_6.cpp
double area_const (const double& lato) {
  return lato * lato;
}

double area (double& lato) {
  return lato * lato;
}

void main() {
  double lato = 10;
  double A = area_const (lato); // A = 100
  double B = area_const (10);   // B = 100  OK
  double C = area (lato);       // C = 100
  // double D = area (10);      // NO
}

Il compilatore può considerare la costante letterale 10 come variabile costante riferita da lato nella area_const, ma non può fare lo stesso in area perché una costante (solo lettura) non può essere alias di un riferimento non costante (lettura e scrittura), per cui il compilatore torna un errore. Come abbiamo già fatto notare per i puntatori, questo problema non sussiste nel passaggio per valore degli argomenti delle funzioni, che è dunque da preferire ove possibile (ad esempio nella funzione rendiPositivo dell'esempio ex5_5_5.cpp non è possibile). Ultima osservazione: anche i riferimenti, come i puntatori, possono essere utilizzati come tipo di ritorno di una funzione; quando ci addentreremo maggiormente nella programmazione a oggetti, vedremo esempi significativi; per ora ci si accontenti del seguente:


// ex5_5_7.cpp
#include <iostream.h>
#include <stdlib.h>
#include <time.h>

// tale funzione sceglie "a caso" una variabile,
// tra le due date, da ritornare
int& scegliVariabile (int& a, int& b) {
  if (rand() % 2 == 0)
    return a;
  else        // l'else non e' necessario: perche'?
    return b;
}

void main() {
  srand ( time(0) );
  int a, b;
  for (int i = 0; i < 5; i++) {
    a = 0; b = 0;
    scegliVariabile(a,b) = 1;
    cout << "a = " << a << "\tb = " << b << "\n";
  }
}

esempio di output:
 a = 1 b = 0
 a = 0 b = 1
 a = 0 b = 1
 a = 1 b = 0
 a = 0 b = 1

ex-2
si scriva una funzione che accetti due riferimenti a double e restituisca la loro somma; gli argomenti devono essere costanti? Perché?
ex-3
si scriva una funzione void che accetti un riferimento a int e, nel caso esso abbia valore nullo, lo incrementa; in tutti gli altri casi l'argomento viene lasciato inalterato; l'argomento deve essere costante? Perché?
ex-4
si scrivano due funzioni che scambino tre numeri (se i numeri sono $a$, $b$ e $c$, allora $b$ prende il valore di $a$, $c$ di $b$ e $a$ di $c$, in senso orario); una passi gli argomenti per indirizzo e l'altra per riferimento; gli argomenti in quest'ultimo caso devono essere costanti? Perché? Quale delle due funzioni scritte scegliereste da utilizzare in un vostro programma? Perché? È possibile passare gli argomenti per valore? Perché?


next up previous contents index
Next: Sovrapposizione di funzioni Up: Puntatori e riferimenti Previous: Puntatori a costanti e   Indice   Indice analitico
Claudio Cicconetti
2000-09-06