// ex5_4_1.cpp void main() { const int a = 5; int b = 7; const int* p_a_cost = &a; const int* p_b_cost = &b; // int* p_a = &a; // warning int* p_b = &b; b++; (*p_b)++; // (*p_a_cost)++; // errore 1 // (*p_b_cost)++; // errore 2 }Abbiamo commentato le righe che restituiscono messaggi di warning o di errore da parte del compilatore, le quali sono le più interessanti da analizzare. Per quanto riguarda le altre righe di programma, quelle non commentate, tutto è come possiamo aspettarci: le variabili
p_a_cost
e
p_b_cost
sono due puntatori a variabili intere costanti; il fatto che
b
non sia costante, non è rilevante perché in C++ un qualsiasi
oggetto che non è costante, può anche essere considerato tale (ma,
attenzione, non viceversa). Per schematizzare, è come se ogni
variabile avesse dei permessi di lettura e scritttura: le variabili non
costanti li hanno entrambi abilitati, mentre quelle costanti non hanno il
permesso di scrittura. Nessuna sorpresa dunque per l'errore numero 1, nel cui
statemente *p_a_cost
è alias di a
, la quale è una variabile
costante e, dunque, non può essere incrementata di valore. Perché allora
anche la riga successiva (errore 2) non è corretta: in fondo
*p_b_cost
è alias di b
, che non è costante; questo è vero,
però siccome p_b_cost
è una variabile che è stata definita come
``puntatore a variabile intera costante'', il compilatore non permette di
modificare il valore della variabile cui punta, anche se in realtà essa non
è costante. Infine, quale è la ragione del warning segnalato? Se fosse
possibile creare un puntatore a una variabile non costante e riferirlo ad una
variabile costante, avremmo un po' di problemi: infatti il compilatore
dovrebbe permetterci di modificare, tramite aliasing, la variabile puntata, la
quale è costante.
Non si tratta di un discorso semplice; comunque è fondamentale capire che nel linguaggio C++ non si può ``barare'': se una variabile è costante non è possibile in nessun modo modificare il suo valore. Siccome creando un puntatore a una variabile non costante, assegnandogli l'indirizzo di una variabile costante, e utilizzandolo come alias potremmo modificare il valore di una costante, il compilatore ci stronca l'idea sul nascere, rifiutandosi il più delle volte di portare a termine la compilazione, o al limite tornando dei messaggi di warning. Nasce allora una domanda: abbiamo visto che l'uso dei puntatori è quello di creare degli alias per le variabili, in maniera tale da poterle modificare all'interno delle funzioni; visto che non possiamo modificare il valore delle costanti, perché utilizzare dei puntatori a esse? La risposta è composta da due parti. La prima osservazione è che l'uso dei puntatori che abbiamo visto è uno dei tanti, per cui esistono situazioni (che non abbiamo ancora visto) nelle quali può rendersi utile dichiarare puntatori a oggetti costanti; in secondo luogo, la domanda posta è del tutto legittima, in quanto i suddetti casi particolati sono davvero pochi.
Ben altro discorso invece per (attenzione a non fare confusione con la terminologia) i puntatori costanti. Si tratta in questo caso di puntatori (a variabili costanti e non) che sono essi stessi costanti, ovvero il loro valore non può cambiare; siccome il valore di un puntatore è l'indirizzo di una variabile, allora i puntatori costanti restano alias fedeli della medesima variabile in tutta la loro vita. Ricordiamo che, come per tutte le variabili che abbiamo fin'ora visto, la vita di un puntatore coincide con quella del programma se esso è globale oppure è dichiarato nella funzione main
, mentre se esso è stato creato all'interno di una funzione, essa coincide con la durata della funzione stessa. Vediamo un esempio di puntatore costante:
// ex5_4_2.cpp // ATTENZIONE: NON FUNZIONA void main() { double x = 2.71; double* p_x = &x; double* const p_const_x = &x; const double* p_x_const = &x; const double* const p_const_x_const = &x; double y = 3.14; (*p_x)++; // OK p_x = &y; // OK (*p_const_x)++; // OK p_const_x = &y; // NO // assignment of read-only variable `p_const_x' // assegnamento della variabile in sola lettura 'p_const_x' (*p_x_const)++; // NO // increment of read-only location // incremento di una locazione di memoria in sola lettura p_x_const = &y; // OK (*p_const_x_const)++; // NO // increment of read-only location p_const_x_const = &y; // NO // assignment of read-only variable `p_const_x_const' }
In questo esempio abbiamo elencato tutte le possibili combinazioni riguardo i puntatori, le quali sono nell'ordine: puntatore a una variabile non costante (nell'esempio di tipo double), puntatore costante a una variabile non costante, puntatore a una variabile costante, puntatore costante a una variabile costante. Le righe di codice commentate con NO sono quelle che tornano messaggi di errore da parte del compilatore5.5, i quali sono stati segnalati in ogni riga successiva a quella incriminata.
A cosa servono i puntatori costanti (e anche quelli a costanti)? Per ora si sappia che sono una forma di protezione: se, ad esempio, sappiamo che una funzione non deve cambiare il valore di un certo oggetto che le viene passato, è buona regola passare un puntatore costante, per evitare che erroneamente si possa corrompere il dato. Vediamo un esempio
// ex5_4_3.cpp // calcola l'area di un triangolo // dati due lati e l'angolo fra essi compreso #include <iostream.h> #include <math.h> double area (const double* a, const double* b, const double* theta) { // altezza relativa al lato b double h = (*a) * sin(*theta); return .5 * (*b) * h; } void main() { double a, b, theta; cout << "lato 1? "; cin >> a; cout << "lato 2? "; cin >> b; cout << "angolo compreso? "; cin >> theta; double A = area (&a, &b, &theta); cout << "l'area e': " << A; }
esempio di output:
lato 1? 6
lato 2? 10
angolo compreso? 0.5235987756
l'area e': 15
La funzione area
non ha ragione di modificare i dati che le vengono immessi, in quanto il suo unico compito è quello di calcolare l'area in base a essi; è allora più ``sicuro'' passare un puntatore costante, cosicché non si possa neanche volendo cambiare, per esempio, la lunghezza di un lato del triangolo mentre se ne calcola l'area. Certamente questi problemi non ci sarebbero stati se avessimo passato a
, b
e theta
per valore, piuttosto che per indirizzo; cosa conviene fare allora, passare le variabili per indirizzo o per valore? In linea di massima, per quanto riguarda gli oggetti di tipi primitivi (come le variabili che abbiamo fino ad ora utilizzato), conviene passarli per indirizzo solo quando è strettamente necessario; in tutti gli altri casi passare per valore. Questo perché il passaggio per valore è molto più semplice, benché sia teoricamente più lento: infatti bisogna copiare da qualche parte nella memoria tutta la variabile, piuttosto che solo il suo indirizzo. Comunque la differenza a livello di tempo di esecuzione, per oggetti dei tipi primitivi, non è apprezzabile; si sappia però che esistono altri oggetti (ne vedremo più avanti) che possono occupare una porzione non irrilevante della memoria principale, per cui passarli per valore in una funzione sarebbe davvero molto inefficiente.