Il compilatore interpreta che la dichiarazione di una variabile sia la richiesta di assegnazione di una locazione di memoria che il programmatore intende identificare con un nome e che sia sufficientemente grande da contenere un certo insieme di valori.
Quando il compilatore incontra quella variabile in una espressione:
cerca l'indirizzo di memoria assegnato a quella variabile
legge o scrive, a seconda se la variabile si trova a primo o a secondo membro di una espressione, il valore contenuto a quell'indirizzo
Il compilatore C++ sostituisce all'espressione &x l'indirizzo di memoria della variabile x. Invece, se, in una espressione, l'indirizzo della variabile è preceduto dall'operatore * (asterisco), come ad esempio *(&x), il compilatore sostituisce il valore contenuto a quell'indirizzo, ovvero *(&x) equivale a x.
I puntatori consentono una grande flessibilità di programmazione, ad esempio:
passaggio dei parametri per riferimento
gestione di strutture dati complesse, perchè l'indirizzo di un dato può essere calcolato in fase di esecuzione
realizzare il polimorfismo, cioè scrivere funzioni che elaborano dati di cui se ne conosce il tipo solo al momento dell'esecuzione
Un puntatore è una variabile che contiene l'indirizzo di una locazione di memoria. Graficamente un puntatore viene rappresentato con una freccia che parte da una locazione e termina in un'altra.
In figura la freccia che parte dalla locazione di indirizzo 102, associata alla variabile ptr, punta alla locazione associata alla variabile x, il cui indirizzo 108 è contenuto nella variabile ptr.
La sintassi per dichiarare un puntatore, denominato ad esempio ptr, ad un intero x è:
int *ptr = &x;
Il cui significato è: "ptr è un puntatore ad un intero, inizializzato con l'indirizzo
della variabile x".
Un puntatore occupa 32 bit, pari alla dimensione del bus indirizzi della CPU. Nella dichiarazione il tipo
che precede il nome assegnato al puntatore si riferisce alla locazione puntata.
Dopo aver dichiarato il puntatore, per accedere al valore puntato si antepone il carattere asterisco al nome
del puntatore. Ad esempio:
cout << *ptr;
stampa il valore puntato da ptr, che nell'esempio precedente è il valore contenuto nella
variabile x. Se non si fosse usato l'asterisco, con l'istruzione cout << ptr; si
sarebbe ottenuta la stampa dell'indirizzo di x.
Per assegnare un valore a x si può usare indifferentemente una delle seguenti espressioni:
*ptr = 5;
x = 5;
I puntatori possono essere usati per passare i parametri ad una funzione. Il seguente esempio è una funzione che calcola il quadrato di un numero:
void quadrato (int *numPtr) {
*numPtr = *numPtr * *numPtr;
}
int main () {
int x = 5;
quadrato(&x);
cout << x; // Stampa 25
}
Nella funzione si osserva il doppio significato attribuito all'operatore asterisco. Quando precede il nome di una variabile ha il significato di "valore puntato da", mentre quando si trova tra due variabili rappresenta l'operatore di moltiplicazione
Il C++, come il C, associa una costante ad un identificatore. Il compilatore provvede a sostituire l'espressione costante dovunque incontra l'identificatore a cui è associata. Gli identificatori di costanti hanno lo stesso effetto degli identificatori usati nelle direttive define: Specificano un'espressione che il compilatore, durante la compilazione del programma, sostituirà al posto dell'identificatore. Le costanti però hanno un vantaggio: rientrano nella tabella dei simboli del compilatore e quindi sono accessibili a un debugger.
In una dichiarazione, il termine const può trovarsi davanti al tipo della variabile puntata:
const int *ptr;
per specificare che il puntatore può essere modificato, ma il valore puntato non deve essere modificato,
nell'altro caso, invece, il termine const può precedere il nome del puntatore, come nel seguente esempio:
int * const ptr;
per specificare che il puntatore non deve essere modificato, ma il valore puntato può essere cambiato.
Anche nelle dichiarazioni di puntatori si può avere la presenza di puntatori costanti. In tali dichiarazioni è significativo il posto occupato dal termine const. Per esempio:
// il puntatore è costante e contiene l'indirizzo di buf
char *const ptr = stringa;
// Cambia il carattere a cui punta ptr; (consentito)
*ptr ='a';
// cambia il puntatore; (errore)
ptr = altraStringa;
La prima istruzione dichiara che ptr è un puntatore costante a una stringa inizializzato con l'indirizzo di una stringa. La seconda istruzione modifica il valore puntato da ptr, ma lascia inalterato il valore di ptr, nella terza istruzione non è ammesso modificare ptr, affinchè punti ad un'altra stringa.
La dichiarazione seguente ha un significato differente:
const char *ptr = stringa; // Puntatore a costante
ptr = altraStringa; // modifica il puntatore ; (corretto)
*ptr ='a'; // Cambia il char a cui ptr Punta; (errore)
In questo secondo esempio ptr è un puntatore ad una stringa costante. Si può modificare ptr stesso affinchè punti a un'altra stringa, ma non si può modificare la stringa usando ptr. In effetti, ptr è diventato un puntatore di sola lettura.
Si può usare const quando si vuole dichiarare che una funzione non deve modificare uno dei suoi parametri. Si consideri il seguente prototipo di funzione:
// Nodo è un record molto grande
int readonly( const struct Nodo *nodoptr );
Il prototipo dichiara che la funzione readonly non può modificare la struttura Nodo a
cui punta il parametro che riceve.
Persino se un puntatore viene dichiarato dentro la funzione, il parametro resta ancora protetto perchè non si
può assegnare un puntatore di sola lettura a un puntatore ordinario. Ad esempio:
int readonly( const struct Nodo *nodoptr ) {
struct Nodo *writeptr; // Puntatore ordinario
writeptr = nodeptr; // Errore - assegnazione vietata
}
Se una simile assegnazione fosse consentita, la struttura Nodo potrebbe essere modificata attraverso writeptr.
I riferimenti sono usati per passare parametri a funzioni e per restituire valori da funzioni.
Un riferimento può essere pensato come sinonimo di una variabile, cioè un nome alternativo per quella variabile. Quando si assegna il valore iniziale ad un riferimento, in effetti lo si associa ad una variabile. Il riferimento è associato in modo permanente a quella variabile. In un secondo momento non si potrà cambiare il riferimento in modo che diventi il sinonimo di un'altra variabile.
L'operatore & (indirizzo di una variabile) identifica un riferimento, come mostrato nel seguente esempio:
int numero;
int &altroNumero = numero; // dichiarazione di un riferimento
Queste istruzioni dichiarano un intero denominato numero e informano il compilatore che la variabile numero ha anche un altro nome: altroNumero. Tutte le operazioni fatte usando uno dei due nomi producono lo stesso effetto.
Il seguente esempio mostra che si può usare la variabile o il riferimento alla variabile indifferentemente.
void main() {
int numero = 123;
int &altroNumero = numero;
cout << endl << numero;
cout << endl << altroNumero;
altroNumero++;
cout << endl << numero;
cout << endl << altroNumero;
numero++;
cout << endl << numero;
cout << endl << altroNumero;
}
L'esempio mostra che le operazioni sulla variabile altroNumero agiscono anche sulla variabile numero. Infatti, si osservi il risultato prodotto dal programma, che dimostra che i due nomi di variabile identificano la stessa locazione di memoria:
123 123 124 124 125 125
Un riferimento non è una copia della variabile a cui si riferisce, ma è la stessa variabile con un nome diverso. Il seguente esempio mostra l'indirizzo di una variabile e il riferimento alla variabile.
void main() {
int numero = 123;
int &altroNumero = numero;
cout << &numero << ' ' << &altroNumero;
}
Quando viene eseguito, il programma stampa lo stesso indirizzo per entrambi gli identificatori.
Notare che, nell'esempio precedente, l'operatore & è usato in due modi diversi:
Nella dichiarazione di altroNumero la & è parte del tipo della variabile. La variabile altroNumero è di tipo int &, cioè riferimento a intero.
Nella successiva istruzione cout la & ha il significato di "indirizzo della variabile a cui è applicato" (come anche in C)
Inizializzare un riferimento
Un riferimento non esiste se non c'è la variabile a cui deve far riferimento. Quindi il riferimento viene inizializzato, quando viene dichiarato, dandogli qualcosa a cui riferirsi.
Le eccezioni a questa regola si evrificano nei seguenti casi:
La variabile è dichiarata con extern, quindi è inizializzata in un altro file,
La variabile è un membro di una classe, quindi viene inizializzata dal costruttore della classe,
La variabile è un parametro di una funzione, quindi il suo valore viene assegnato in fase di chiamata,
La variabile è il tipo restituito da una funzione, quindi il valore è assegnato dalla funzione.
Confronto tra Riferimenti e Puntatori
I riferimenti sono simili ai puntatori. Si consideri la seguente dichiarazione:
int x, *px;
x è una variabile intera e px è il puntatore ad una variabile intera. La seguente istruzione assegna l'indirizzo della variabile x al puntatore px:
px = &x;
Per assegnare un valore alla variabile x si può usare sia la variabile che il puntatore:
x = 5;
*px = 5;
La sintassi *px, se usata in una dichiarazione si legge "px è un puntatore a un intero", mentre se usata in un'espressione, si legge "la locazione puntata da px".
Nella dichiarazione del riferimento altroNumero si sarebbe potuto usare anche un puntatore costante:
int numero = 123;
int *const ptr = №
// puntatore costante: punta a numero.
Una simile dichiarazione rende *ptr un modo alternativo per riferirsi alla variabile numero. Ogni assegnazione a *ptr agisce su numero, e viceversa. Anche un riferimento si comporta in questo modo, ma non richiede l'uso dell'asterisco. Poichè *ptr è un puntatore costante, non lo si può far puntare ad un altro intero, dopo che è stato inizializzato a numero. Lo stesso vale per un riferimento.
I riferimenti, però non possono essere manipolati come i puntatori. Con un puntatore si può distinguere tra il puntatore stesso e la variabile a cui punta usando l'asterisco. Per esempio, ptr è il puntatore, mentre *ptr è il valore puntato. Con un riferimento, non è richiesto l'uso dell'asterisco, quindi si può usare solo la variabile che viene riferita, non il riferimento stesso.
Il definitiva, con i riferimenti è impossibile:
Puntarli,
prelevare l'indirizzo,
confrontarli,
Assegnare valori,
compiere operazioni aritmetiche,
modificarli.
Ognuna delle operazioni elencate non agisce sul riferimento ma sulla variabile riferita.
Ad esempio, se si applica l'operazione di incremento a un riferimento, si incrementa il valore a cui fa riferimento. Prelevando l'indirizzo di un riferimento si ottiene l'indirizzo della variabile riferita.
Così come è possibile dichiarare puntatori costanti e puntatori a costanti, è possibile dichiarare un riferimento a costante. Per esempio:
int numero = 123;
const int &altroNumero = numero; // Riferimento a constante intera
Questa dichiarazione rende altroNumero un sinonimo di sola lettura della variabile numero.
Non si possono apportare modifiche ad altroNumero, solo a numero.
Non è possibile
dichiarare un riferimento costante:
int &const altroNumero = numero; // Errore
Questa dichiarazione non ha significato, perchè i riferimenti sono costanti per definizione.
I riferimenti non servono semplicemente a dare un altro nome a una variabile, il loro uso più comune avviene nei parametri di funzione.
Riferimenti come parametri di funzione
In linguaggio C ci sono due modi per passare una variabile come parametro a una funzione:
// passare Riferimenti per ridurre il tempo per copiare i
// parametri sullo stack ed eliminare la notazione del puntatore.
struct record { // una struttura molto grande
int serie;
char descrizione[1000]; // una grossa quantità di caratteri
} bo = {123, "questo record e' molto grande"};
// Tre funzioni che ricevono la struttura come parametro.
void fval(record vl); // chiamata per valore
void fptr(const record *pl); // chiamata per puntatore
void frif(const record &rl); chiamata per riferimento
int main () {
fval(bo); // si passa la variabile
fptr(&bo); // si passa l'indirizzo della variabile
frif(bo); // si passa un riferimento alla variabile
system("PAUSE");
return 0;
}
// passaggio per valore
void fval(record vl) {
cout << endl; << vl.serie;
cout << endl; << vl.descrizione;
}
// passaggio per puntatore
void fptr(record *pl) {
cout << endl; << pl->serie; // accesso tramite puntatore
cout << endl; << pl->descrizione;
}
// passaggio per riferimento
void frif(record &rl) {
cout << endl; << rl.serie; // accesso tramite riferimento
cout << endl; << rl.descrizione;
}
Il parametro r1 è un riferimento inizializzato con la variabile bo quando frif() è chiamata. All'interno della funzione frif() il nome r1 è un sinonimo di bo. Questo riferimento si trova in una regione di visibilità diversa da quella della variabile a cui si riferisce.
Quando si passa un riferimento come parametro, il compilatore, in effetti, passa l'indirizzo della variabile che
vede il chiamante. Quindi il passaggio per riferimento è conveniente, perchè il passaggio del
parametro avviene rapidamente.
Inoltre la sintassi per passare un riferimento è identica a quella del passaggio per valore, però non
si richiede l'uso della & o dell'operatore di accesso indiretto: -> (tramite puntatore).
In altri termini, il passaggio per riferimento combina i vantaggi del passaggio per puntatore e la chiarezza
sintattica del passaggio per valore.
Quando si passa un riferimento come parametro ogni modifica al parametro influenza anche la variabile del chiamante. Questo è importante, perchè, dalla sintassi, non si deduce questo comportamento. Per esempio:
fval(bo); // La funzione non modifica bo
fptr(&bo); // & implica che la funzione può modificare bo
frif(bo); // Stessa sintassi di fval;
// implica che la funzione può modificare bo
La sintassi per chiamare frif potrebbe far ritenere che la funzione non può modificare la
variabile passata.
Nel caso di frif questa assunzione è corretta. Siccome il parametro di frif è
un riferimento a una costante, il suo parametro è un sinonimo di sola lettura della variabile del chiamante.
La funzione frif non può modificare la variabile bo.
Si può usare un riferimento ordinario come parametro, invece di un riferimento a costante. Questo consente alla funzione di modificare la variabile del chiamante, persino se la sintassi farebbe pensare il contrario. Per esempio:
// Tecnica sbagliata: si modifica una variabile attraverso il riferimento ricevuto come parametro:
void print( int ¶m ) {
cout << param;
param = 0;
}
int main() {
int a = 5;
print( a); // il parametro viene modificato (inaspettato effetto collaterale).
}
L'uso del riferimento potrebbe confondere chi legge il programma. Si intende sottolineare la difficoltà che incontra un programmatore, leggendo la chiamata di funzione, a stabilire se la funzione modifica o non modifica il parametro ricevuto. Senza poter vedere la dichiarazione di funzione è impossibile dire se la funzione riceve un valore o un riferimento.
Le seguenti regole, se rispettate, hanno lo scopo di prevenire confusioni nell'interpretazione della tecnica usata per passare i parametri.
Se la funzione modifica il parametro usare il puntatore,
Se la funzione non modifica il parametro usare un riferimento a una costante.
Notare che i riferimenti sono convenienti quando si devono passare parametri molto grandi, di un tipo definito dal programmatore. I parametri dei tipi predefiniti, char, int, long, ecc.. possono esere passati per valore.
I riferimenti possono anche essere usati per restituire un valore da una funzione. Per esempio:
int num = 0; // variabile Globale
int &numero() {
return num;
}
int main() {
int i;
i = numero();
numero() = 5; // si assegna 5 a num
}
In questo esempio il valore restituito dalla funzione numero() è un riferimento inizializzato con la variabile globale num. Di conseguenza, l'espressione num() rappresenta un sinonimo di numero. Questo comporta che una chiamata di funzione può trovarsi anche al primo membro di un'espressione di assegnazione.
Il nome di un array (senza le parentesi quadre) è l'indirizzo del primo elemento dell'array. Se si scrive vett[3] si sta chiedendo al compilatore di fornire il valore dell'elemento che occupa il terzo posto nell'array vett.
Si consideri il seguente frammento di programma:
1: long vett [] = {68, 10, 97, 46};
2: long * ptr = vett;
3: ptr++;
4: long *ptr2 = vett + 3;
La dichiarazione in linea 1 si legge: vett è un array di long, inizializzato con i valori 68, 10, 97, 46. La dichiarazione in linea 2 si legge: ptr è un puntatore a long inizializzato con l'indirizzo del primo elemento dell'array vett. L'istruzione di incremento nella linea 3, ptr++, incrementa il puntatore. L'incremento però non deve portare il puntatore al byte successivo, ma all'elemento successivo dell'array a cui punta ptr. Il compilatore sa che ptr punta a un long, quindi l'incremento avviene di un numero pari al numero di byte richiesti per memorizzare un long.
Anche un'operazione aritmetica con i puntatori avviene considerando la dimensione dell'elemento puntato. As esempio ptr2-ptr fornisce il numero di elementi compresi tra ptr2 e ptr
Quindi l'elemento a cui si fa riferimento con la notazione vett[3] è lo stesso a cui si fa riferimento con la notazione vett+3.
Si scrivano tre semplici funzioni che ricevono lo stesso numero e tipo di parametri e restituiscono lo stesso tipo di parametro.
Ad esempio una funzione che restituisce la somma dei due parametri ricevuti,
un'altra restituisce il prodotto tra i due parametri
e un'altra restituisce la differenza tra i due parametri:
L'uso di un puntatore al posto di un nome ha il vantaggio di poter calcolare al momento dell'esecuzione del programma la funzione da richiamare.
Nelle righe 1-3 si dichiarano le variabili e si assegnano i valori iniziali a quelle di input.
Nella riga 4 si dichiara che pfun è un puntatore a una funzione che riceve due interi.
Nella riga 5 (e similmente nelle righe 8 e 11) si deve notare che il nome di funzione viene usato senza le parentesi tonde. In questo caso, il compilatore usa l'indirizzo della funzione. Quindi l'espressione pfun = Somma significa che a pfun si assegna il puntatore alla funzione Somma.
Nelle righe 6, 9 e 12 viene richiamata una funzione tramite il puntatore.
La modifica seguente consente di dichiarare un array di puntatori a funzione e, quindi, richiamare le funzioni in un ciclo:
I tre puntatori a funzione possono anche essere usati in espressioni. Ad esempio per ottenere il prodotto tra la somma e la differenza dei due valori:
Risultato = pfun[1](pfun[0](a, b), pfun[2](a, b));
Come ultima variante si dichiara una funzione che riceve il puntatore ad una funzione:
e viene usata in un'espressione nel modo seguente (sostituire le righe da 4 alla fine):