Premessa per chi conosce i puntatori

NOTA BENE: in questo capitolo mancano di proposito il concetto di puntatore e di variabile globale: questa premessa è solo per chi già li conoscesse. Chi non conosce i puntatori deve saltare questo paragrafo e passare a quello sottostante

Sia nel linguaggio C che in C++ si può effettuare il passaggio di parametri esclusivamente per valore, cioè copiando il contenuto delle variabili indicate tra parentesi. Vedere esempio passaggio_per_valore. Questo impedisce di effettuare delle modifiche sui parametri effettivi della funzione.

int triplicare(int x)
{
 x=x*3; // modifica solo la var. locale x
        // il risultato deve essere restituito
 return x;
}

Tuttavia, se al posto dei tradizionali tipi di dato (come int) si usano i puntatori (come int*), la funzione riesce ad accedere e a modificare anche i parametri effettivi.

int triplicare(int* p)
{
 *p=*p*3;  // modifica anche la variabile puntata da p
 return 0; // non e' necessario restituire il risultato
}

Come si può notare dal secondo esempio, questo appesantisce il codice costringendo ad aggiungere l'operatore di deferimento * (l'asterisco) davanti al puntatore p.

I references, invece, hanno praticamente la stessa “forza” dei puntatori, ma una sintassi molto più semplice.

References

Quando una funzione ha come parametro formale un reference, al suo interno NON viene creata una nuova variabile locale, ma si lavora proprio sul parametro effettivo usando il nome del parametro formale come il suo soprannome temporaneo.

void calcolaTriplo(int& x); 

La dichiarazione si legge in questo modo: la funzione accetta come argomento un reference a… intero. La chiamata della funzione, invece, è come quella vista nel passaggio per valore…

calcolaTriplo(mioNumero); 

Nella seguente figura le funzioni main() e calcolaTriplo() sono rappresentate da due contenitori. la variabile x non ha una propria area di memoria, ma è solo il soprannome (alias) della variabile mioNumero. mioNumero è una variabile locale di main() e non si può usare il suo nome in calcolaTriplo() ma è possibile usare il termine x per fare la stessa cosa.

Purtroppo il simbolo &, usato in un altro luogo, possiede anche altri significati, completamente diversi. Dentro un'espressione logica può significare operatore “AND binario”, oppure, scritto davanti al nome di una variabile, diventa l'operatore “indirizzo di…”. Il reference al momento della sua dichiarazione deve essere sempre inizializzato, infatti non si può usare un soprannome senza dire a chi corrisponde quell'alias… Al contrario dei puntatori, i reference, una volta inizializzati come alias ad un oggetto, non possono diventare l'alias di niente altro.

22.cpp
// Questo programma è stato scritto da Fabio.
// Serve a calcolare il triplo di un numero.
 
#include <iostream>
 
// questo modo di passare il parametro 
// non crea una copia locale
// questa funzione e' di tipo void, cioe' non restituisce nulla (manca return)
 
void calcolaTriplo(int& x) 
{
  x = 3*x;
}
 
int main ()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  calcolaTriplo(mioNumero); // chiamata della funzione che modifica il mioNumero
 
  std::cout << "il triplo vale " << mioNumero << ".\n";
  return 0;
}

Premettendo const davanti ad un reference si impedisce di modificare il parametro effettivo. L'effetto è simile al classico passaggio per valore, ma con la differenza che invece di creare una copia del parametro effettivo, in pratica si copia solo il suo indirizzo.

Questo è utilizzato soprattutto quando il parametro effettivo è un “oggetto” (che potrebbe essere difficile da copiare o di notevoli dimensioni).

23.cpp
// Questo programma è stato scritto da Fabio.
 
#include <iostream>
 
// questo modo di passare il parametro permette di agire sul parametro effettivo
// NON passa una copia
// il termine const evita modifiche indesiderate al parametro passato.
// NON crea una nuova copia della variabile, si risparmia memoria!
// x è solo il soprannome temporaneo che viene dato a mioNumero
 
int calcolaTriplo(const int& x)  
{
 return 3*x;
}
 
int main ()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  std::cout << "il triplo di " << mioNumero
            << " vale " << calcolaTriplo(mioNumero) ".\n";
  return 0;
}

Per comprendere il risparmio di memoria ottenuto con i references costanti, confrontare la seguente tabella di traccia con quella ottenuta con il passaggio per valore. Quante colonne ci sono nelle diverse tabelle?

mioNumero (alias x) output
6 -
6 -
6 18

I const reference sono anche l'unico modo con cui si possono utilizzare gli oggetti temporanei inaccessibili (come le espressioni costanti o gli oggetti temporanei restituiti da una funzione)

vedere Thinking in C++, capitolo 11: References & the Copy-Constructor pag.477

Le funzioni costanti sono importanti perché sono un buono strumento per evitare errori. Basta aggiungere il modificatore const in fondo della dichiarazione della funzione (prima del punto e virgola). Questo impedisce modifiche accidentali ai dati. Il compilatore controllerà che questa funzione non esegua alcuna istruzione di modifica su NESSUNO dei dati passati. Se il programmatore non rispetta questa regola, verrà avvisato dal compilatore. Per questo motivo const dovrebbe essere usato su tutte le funzioni di tipo ispettrici (vedere classificazione_delle_funzioni)

Le funzioni costanti (quindi, ispettrici) sono anche le uniche che possono avere come argomenti interi oggetti costanti come:

  • fun(const int& x) const
  • fun(const Classe unOggetto) const
  • fun(const Classe& unOggetto) const
  • fun(const Classe* unOggetto) const

Infatti, provando a passare un oggetto costante ad una funzione non costante si ottiene errore.

  • fun(const Classe unOggetto) // ERRORE

Questo perché una tale funzione (non ispettrice, ma modificatrice) dovrebbe consentire di modificare l'oggetto.

Errore da evitare

Restituire un reference (return) è un'istruzione legale ma a volte non è corretta perché può generare un problema di allocazione della memoria, in quanto le variabili locali di una funzione vengono distrutte al suo termine.

Per approfondire si può studiare il seguente esempio, dove l'uso dei reference costanti è corretto.

24.cpp
// Questo programma è stato scritto da Fabio.
 
// Serve a calcolare il triplo di un numero.
// I reference sono utili per risparmiare memoria
// anche sul tipo restituito dalla funzione
// basta aggiungere UN SECONDO reference (sempre preceduto da un const)
// ma e' molto facile commettere errori...
 
#include <iostream>
 
const int& triplicare(const int& x)  
{
 return 3*x;
}
 
int main ()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  std::cout << "il triplo di " << mioNumero
            << " vale " << triplicare(mioNumero) ".\n";
  return 0;
}

Il nuovo alias davanti alla funzione evita di creare una nuova variabile temporanea che sarebbe necessaria anche per restituire il valore con return. Il nuovo const davanti alla funzione impedisce modifiche indesiderate a tale oggetto. In questo caso restituire un reference è corretto perché l'oggetto non è locale, altrimenti questo sarebbe stato distrutto…

I reference sono un nuovo tipo del C++ quindi, tutte le funzioni che usano il tipo reference sono utilizzate sempre con la dot notation… (Fare prima la programmazione orientata agli oggetti e DOPO i reference?)

  1. int compare ( const string& str ) const;
  2. int find ( char c, size_t pos = 0 ) const;
  • appunti3s/references.txt
  • Last modified: 2018/04/25 07:55
  • (external edit)