next up previous contents index
Next: Puntatori a costanti e Up: Puntatori e riferimenti Previous: Puntatori (parte seconda)   Indice   Indice analitico

Puntatori e funzioni

Un primo utilizzo dei puntatori consiste nel loro utilizzo come argomenti di funzioni; in tal caso si dice che gli argomenti sono passati alla funzione per indirizzo piuttosto che, come abbiamo già visto, per valore. La differenza tra queste due diverse modalità di scambio dei dati è fondamentale: se una funzione riceve solo il valore di certe variabili contenute nella funzione chiamante (negli esempi fino ad ora mostrati la funzione chiamante è sempre stata 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:

\begin{displaymath}
\left\{ \begin{array}{l}
x = R \cdot \cos t \\
y = R \cdot \sin t
\end{array}\right.
\end{displaymath}

ove $R$ è il raggio della circonferenza; affinché essa venga descritta completamente è necessario che $t$ vari in un intervallo di (almeno) $2\pi$.


// 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.

ex-2
si scriva un programma che stampi su schermo la traiettoria di una retta; si ricorda che l'equazione parametrica di una retta è la seguente:

\begin{displaymath}
\left\{ \begin{array}{l}
x = t \\
y = a \cdot t + b
\end{array} \right.
\end{displaymath}

Siano immessi dall'utente l'intervallo e incremento della variabile indipendente $t$ e i coefficienti $a$ e $b$;
ex-3
si scriva un programma che trovi il minore tra tre numeri dati, utilizzando una funzione min che torni un puntatore a esso;
ex-4
si scriva un programma che converta un angolo espresso in radianti in gradi;
ex-5
si scriva una funzione void che accetti tre puntatori a numeri reali e modifichi i rispettivi alias, in maniera tale che il primo diventi la somma degli altri, il secondo il prodotto, il terzo sia il loro rapporto;
ex-6
si scriva una funzione che sia in grado di tornare la media aritmetica e la media geometrica di tre numeri.


next up previous contents index
Next: Puntatori a costanti e Up: Puntatori e riferimenti Previous: Puntatori (parte seconda)   Indice   Indice analitico
Claudio Cicconetti
2000-09-06