void main()
), le variabili di essa non vengono mai modificate; al contrario scambiando gli indirizzi, tramite l'operatore *
abbiamo a disposizione nella funzione chiamata degli alias di variabili della funzione chiamante. Il discorso non è semplice. Vediamo un esempio:
// ex5_3_1.cpp #include <iostream.h> void scambia (int* a, int* b) { int c = *a; // alias della variabile della // funzione chiamante, // che in questo caso e' void main() *a = *b; *b = c; } void main() { int a = 5, b = 7; cout << "a = " << a << "\tb = " << b << "\n"; scambia (&a, &b); cout << "a = " << a << "\tb = " << b << "\n"; }
output:
a = 5 | b = 7 | |
a = 7 | b = 5 |
È importante notare che non potremmo realizzare una funzione analoga alla scambia
senza utilizzare i puntatori, il che è tutto dire riguardo la loro importanza.
Per mostrare un'altra possibile applicazione dei puntatori, prendiamo un problema: le funzioni in C++ hanno un solo valore di ritorno, come potremmo fare se avessimo bisogno di più ritorni? Semplice: utilizziamo i puntatori come argomenti della funzione. Vediamo il seguente
// ex5_3_2.cpp // converte due angoli gradi -> radianti #include <iostream.h> const double PI = 3.14159265358979323846264338327; // converte un angolo dato in gradi, minuti e secondi // (tutti interi) in un angolo in gradi decimali double gradi (int gg, int mm, int ss) { double x = gg + (mm / 60) + (ss / 3600); return x; } // converte due angoli da gradi a radianti void rad2gradi (double* a1, double* a2) { // 1 radiante = PI / 180 gradi *a1 *= PI / 180; *a2 *= PI / 180; } void main() { int gg1, mm1, ss1; // primo angolo int gg2, mm2, ss2; // secondo angolo cout << "ANGOLO 1\n"; cout << "gradi? "; cin >> gg1; cout << "minuti? "; cin >> mm1; cout << "secondi? "; cin >> ss1; cout << "ANGOLO 2\n"; cout << "gradi? "; cin >> gg2; cout << "minuti? "; cin >> mm2; cout << "secondi? "; cin >> ss2; double a1 = gradi (gg1, mm1, ss1); double a2 = gradi (gg2, mm2, ss2); rad2gradi (&a1, &a2); // due `ritorni' con una sola funzione cout << "\nangolo1: " << a1; cout << "\nangolo2: " << a2; }
esempio di output:
ANGOLO 1
gradi? 90
minuti? 0
secondi? 0
ANGOLO 2
gradi? 43
minuti? 60
secondi? 3600
angolo1: 1.5708
angolo2: 0.785398
La prima obiezione che è possibile sollevare è la seguente: perché convertire due angoli con una sola funzione? È giusto, avremmo anche potuto scrivere, in questo caso, una funzione sola e chiamarla due volte; tuttavia si trattava solo di un esempio, mentre capita a volte che si debba davvero tornare due valori. Uno di questi casi può presentarsi, nel caso volessimo stampare la traiettoria di una equazione parametrica, ad esempio una circonferenza. Ricordiamo a tale proposito che le equazioni parametriche di una curva, non sono altro che le coordinate cartesiane in funzione di una variabile indipendente; per una circonferenza si ha appunto:
// ex5_3_3.cpp // stampa la traiettoria di una circonferenza #include <iostream.h> #include <math.h> const double PI = 3.14159265358979323846264338327; void circ (double* x, double* y, double t, double R) { *x = R * cos(t); *y = R * sin(t); } void main() { double x, y, step, R; // step e' l'incremento della variabile indipendente cout << "incremento var. indip? "; cin >> step; cout << "raggio circonferenza ? "; cin >> R; for (double t = 0; t < 2*PI; t+=step) { circ (&x, &y, t, R); cout << "x: " << x << "\ty: " << y << "\n"; } }
esempio di output:
incremento var. indip? .7
raggio circonferenza ? 2
x: 2 y: 0
x: 1.52968 y: 1.28844
x: 0.339934 y: 1.9709
x: -1.00969 y: 1.72642
x: -1.88444 y: 0.669976
x: -1.87291 y: -0.701566
x: -0.980522 y: -1.74315
x: 0.373025 y: -1.96491
x: 1.55113 y: -1.26253
approfondimento: se volessimo vedere la traiettoria della circonferenza disegnata su di un diagramma, piuttosto che le sue coordinate stampate a schermo, possiamo modificare il nostro programma in tale maniera:
// ex5_3_3.cpp #include <fstream.h> #include <math.h> const double PI = 3.14159265358979323846264338327; void circ (double* x, double* y, double t, double R) { *x = R * cos(t); *y = R * sin(t); } void main() { // apre un file in modalita' scrittura // il nome del file e' `circonferenza' // e si trovera' nella stessa cartella // del file ex5_3_3.cpp // NOTA: se esiste gia' un file chiamato // `circonferenza' esso verra' sovrascritto ofstream out ("circonferenza"); double x, y, step, R; cout << "incremento var. indip? "; cin >> step; cout << "raggio circonferenza ? "; cin >> R; for (double t = 0; t < 2*PI; t+=step) { circ (&x, &y, t, R); // stampa a schermo cout << "x: " << x << "\ty: " << y << "\n"; // stampa su file out << x << "\t" << y << "\n"; } }Una volta stampate le coordinate della circonferenza sul file `circonferenza', basta utilizzare un qualunque programma che disegna grafici, dando essi come input il file `circonferenza'; ad esempio possiamo utilizzare graph o gnuplot, entrambi gratuiti e disponibili presso il sito http://www.gnu.org per diverse piattaforme.
Vediamo ora un esempio nel quale possiamo utilizzare due diverse modalità di ritorno:
// ex5_3_3.cpp // stampa la traiettoria di una circonferenza #include <iostream.h> #include <math.h> double area (double* hyp, double cat1, double cat2) { *hyp = sqrt ( pow(cat1, 2) + pow(cat2, 2) ); // in realta' esiste una funzione in math.h // appositamente per calcolare le ipotenuse dei // triangoli rettangoli; si tratta di: // hyp = hypot (cat1, cat2); return cat1 * cat2 * .5; // area = 1/2 * base * altezza } void main() { // rispettivamente cateti e ipotenusa double a, b, c; cout << "cateto n.1? "; cin >> a; cout << "cateto n.2? "; cin >> b; double A = area (&c, a, b); cout << "il triangolo, di ipotenusa " << c << ", ha area = " << A; }
esempio di output:
ateto n.1? 6
cateto n.2? 8
il triangolo, di ipotenusa 10, ha area = 24
In tal caso il valore di ritorno è l'area del triangolo, mentre l'ipotenusa viene modificata sfruttanto il passaggio alla funzione per indirizzo. Nel procedere del nostro studio del linguaggio C++, incontreremo numerorsi esempi che modificano il valore di variabili passate per indirizzo, tramite l'uso dei puntatori.
I puntatori possono costituire, come tutte le variabili, anche il ritorno di
una funzione. Attenzione però, tornare un puntatore significa tornare
l'alias di una variabile, e c'è dunque un piccolo problema, che finora non
abbiamo sollevato: abbiamo detto che le variabili contenute nella funzione
void main()
vengono distrutte5.3 alla fine del programma; si può dire
lo stesso delle variabili contenute nella altre funzioni? La risposta è NO,
ed il motivo è ovvio: mentre la funzione void main()
viene chiamata
una ed una sola volta, al momento di esecuzione del programma, le altre
possono essere chiamate un numero arbitrario di volte; se, ad esempio, abbiamo
una funzione che calcola il maggiore tra due numeri, potremmo avere bisogno di
essa moltissime volte in un uno stesso programma. Sarebbe allora troppo
pesante tenere in memoria tutte le variabili che, ogni volta che una funzione
viene chiamata, il programma crea; perciò esso distrugge le variabili locali5.4 alla funzione chiamata, non appena si esce da essa. Questo fatto induce un problema riguardo ai puntatori come ritorno di funzioni. Vediamo un esempio (sbagliato):
// ex5_3_5.coo // ATTENZIONE: tale programma NON FUNZIONA #include <iostream.h> int* doppio (int x) { int doppio_x = 2 * x; return &doppio_x; } void main() { int a; cout << "a? "; cin >> a; int* p = doppio (a); cout << "doppio di a = " << *p; }
messaggio di warning:
address of local variable `doppio_x' returned
(ritornato l'indirizzo della variabile locale 'doppio_x')
Il warning tornato ci ha avvertito che c'è qualcosa che, probabilmente, non va; la cosa tragica è che non ci viene segnalato un errore ma un warning, per cui il programma viene tranquillamente compilato. Se proviamo ad eseguirlo poi, ci troviamo (su certe macchine e con certi compilatori) di fronte ad una sorpresa: il programma funziona in realtà. Purtroppo si tratta solo di un caso, dovuto al fatto che il programma non cancella immediatamente tutte le variabili delle funzioni chiamate, il che sarebbe poco efficiente, ma lo fa di tanto in tanto. Per cui se avessimo effettuato molto altre operazioni prima di cout <<...<<*p
avremmo ottenuto un errore in fase di esecuzione. Questo esempio ci porta allora a due importanti riflessioni: non ritornare mai gli indirizzi di variabili locali a funzioni che non siamo void main()
, fare molta attenzione ai messaggi di warning del compilatore, anche e soprattutto se il programma viene provato e funziona. Vediamo invece un esempio corretto.
// ex5_3_6.cpp #include <iostream.h> // ritorna l'indirizzo della variabile il cui // valore e' il medio dei tre int* medio (int* a, int* b, int* c) { // procediamo alla ricerca del valore medio // per tentativi (e' un poco lungo ma e' // comunque la strada piu' conveniente per // tre sole variabili) if ( ( (*a >= *b) && (*a <= *c) ) || ( (*a <= *b) && (*a >= *c) ) ) return a; else if ( ( (*b >= *a) && (*b <= *c) ) || ( (*b <= *a) && (*b >= *c) ) ) return b; // else non e' necessario. Perche' ? return c; } void main() { int a, b, c; cout << "a? "; cin >> a; cout << "b? "; cin >> b; cout << "c? "; cin >> c; int* m = medio (&a, &b, &c); cout << "medio: " << *m; }
esempio di output:
a? 3
b? -2
c? 19
medio: 3
In questo esempio tutto funziona alla perfezione perché in realtà nella funzione medio
non vengono create variabili locali: le uniche in tutto il programma sono a
, b
e c
, le quali appartengono alla funzione main
e quindi non corrono il rischio di essere distrutte prima della fine del programma.
min
che torni un puntatore a esso;