Cerca nel sito:
ricerca
avanzata

Frasi Celebri...

Il delitto non paga, ma i mandanti si.

Guido Clericetti 

Sondaggio:

Come va la vita con l'Euro?

Ottimo!
Come prima
Fatico ma imparer?
Porc... che macello!

visualizza risultati


 

Uso dei puntatori

I puntatori sono utilizzati sostanzialmente per quattro scopi:

    1. Realizzazione di strutture dati dinamiche (es. liste linkate);
    2. Realizzazione di funzioni con effetti laterali sui parametri attuali;
    3. Ottimizzare il passaggio di parametri di grosse dimensioni;
    4. Rendere possibile il passaggio di parametri di tipo funzione.

Il primo caso e` tipico di applicazioni per le quali non e` noto a priori la quantita` di dati che si andranno a manipolare. Senza i puntatori non sarebbe possibile manipolare contemporaneamente un numero non predefinito di dati, anche utilizzando un array porremmo un limite massimo al numero di oggetti di un certo tipo immediatamente disponibili.
Utilizzando i puntatori invece e` possibile realizzare ad esempio una lista il cui numero massimo di elementi non e` definito a priori:

 

#include < iostream >
using namespace std;

// Una lista e` composta da tante celle linkate
// tra di loro; ogni cella contiene un valore
// e un puntatore alla cella successiva.

struct TCell {
  float AFloat;  // per memorizzare un valore
  TCell* Next;   // puntatore alla cella successiva
};

// La lista viene realizzata tramite questa
// struttura contenente il numero corrente di celle
// della lista e il puntatore alla prima cella

struct TList {
  unsigned Size; // Dimensione lista
  TCell* First;  // Puntatore al primo elemento
};

int main(int, char* []) {
  TList List;     // Dichiara una lista
  List.Size = 0;  // inizialmente vuota
  int FloatToRead;
  cout << "Quanti valori vuoi immettere? " ;
  cin >> FloatToRead;
  cout << endl;

  // questo ciclo richiede valori reali
  // e li memorizza nella lista

  for(int i=0; i < FloatToRead; ++i) {
    TCell* Temp = List.First;
    cout << "Creazione di una nuova cella..." << endl;
    List.First = new TCell; // new vuole il tipo di
                            // variabile da creare
    cout << "Immettere un valore reale " ;

    // cin legge l'input da tastiera e l'operatore di
    // estrazione >> lo memorizza nella variabile.

    cin >> List.First -> AFloat;
    cout << endl;
    List.First -> Next = Temp; // aggiunge la cella in
                               // testa alla lista
    ++List.Size;       // incrementa la
                       // dimensione della lista
  }

  // il seguente ciclo calcola la somma
  // dei valori contenuti nella lista;
  // via via che recupera i valori,
  // distrugge le relative celle

  float Total = 0.0;
  for(int j=0; j < List.Size; ++j) {
    Total += List.First -> AFloat;

    // estrae la cella in testa alla lista...
    TCell* Temp = List.First;
    List.First = List.First -> Next;

    // e quindi la distrugge
    cout << "Distruzione della cella estratta..." << endl;
    delete Temp;
  }
  cout << "Totale = " << Total << endl;
  return 0;
}

 

 

Il programma sopra riportato programma memorizza in una lista un certo numero di valori reali, aggiungendo per ogni valore una nuova cella; in seguito li estrae uno ad uno e li somma restituendo il totale; via via che un valore viene estratto dalla lista, la cella corrispondente viene distrutta. Il codice e` ampiamente commentato e non dovrebbe essere difficile capire come funziona. La creazione di un nuovo oggetto avviene allocando un nuovo blocco di memoria (sufficientemente grande) dalla heap-memory (una porzione di memoria riservata all'avvio di un programma per operazioni di questo tipo), mentre la distruzione avviene deallocando tale blocco (che ritorna a far parte della heap-memory); l'allocazione viene eseguita tramite l'operatore new cui va specificato il tipo di oggetto da creare (per sapere quanta ram allocare), la deallocazione avviene invece tramite l'operatore delete, che richiede come argomento un puntatore all'aggetto da deallocare (la quantita` di ram da deallocare viene calcolata automaticamente).
In alcuni casi e` necessario allocare e deallocare interi array, in questi casi si ricorre agli operatori new[] e delete[]:

 

// alloca un array di 10 interi
int* ArrayOfInt = new int[10];

// ora eseguiamo la deallocazione
delete[] ArrayOfInt;

 

La dimensione massima di strutture dinamiche e` unicamente determinata dalla dimensione della heap memory che a sua volta e` generalmente limitata dalla quantita` di memoria del sistema.
Un altro importante aspetto degli oggetti allocati dinamicamente e` che essi non ubbidiscono alle normali regole di scoping statico, solo i puntatori in quanto tali sono soggetti a tali regole, un oggetto allocato dinamicamente puo` quindi essere creato in un certo scope ed essere acceduto in un altro semplicemente trasmettendone l'indirizzo (il valore del puntatore).
Consideriamo ora il secondo uso che si fa dei puntatori.
Esso corrisponde a quello che in Pascal si chiama "passaggio di parametri per variabile" e consente la realizzazione di funzioni con effetti laterali sui parametri attuali:

 

void Change(int* IntPtr) {
  *IntPtr = 5;
}

 

La funzione Change riceve come unico parametro un puntatore a int, ovvero un indirizzo di una cella di memoria; anche se l'indirizzo viene copiato in una locazione di memoria visibile solo alla funzione, la dereferenzazione di tale copia consente comunque la modifica dell'oggetto puntato:

 

int A = 10;

cout << " A = " << A << endl;
cout << " Chiamata a Change(int*)... " << endl;
Change(&A);
cout << " Ora A = " << A << endl;

 

 

l'output che il precedente codice produce e`:

 

A = 10
Chiamata a Change(int*)...
Ora A = 5

 

Quello che nell'esempio accade e` che la funzione Change riceve l'indirizzo della variabile A e tramite esso e` in grado di agire sulla variabile stessa.
L'uso dei puntatori come parametri di funzione non e` comunque utilizzato solo per consentire effetti laterali, spesso un funzione riceve parametri di dimensioni notevoli e l'operazione di copia del parametro attuale in un'area privata della funzione ha effetti deleterei sui tempi di esecuzione della funzione stessa; in questi casi e` molto piu` conveniente passare un puntatore che generalmente occupa pochi byte:

 

void Func(BigParam parametro);

// funziona, ma e` meglio quest'altra dichiarazione

void Func(const BigParam* parametro);

 

 

Il secondo prototipo e` piu` efficiente perche` evita l'overhead imposto dal passaggio per valore, inoltre l'uso di const previene ogni tentativo di modificare l'oggetto puntato e allo stesso tempo comunica al programmatore che usa la funzione che non esiste tale rischio.
Infine quando l'argomento di una funzione e` un array, il compilatore passa sempre un puntatore, mai una copia dell'argomento; in questo caso inoltre l'unico modo che la funzione ha per conoscere la dimensione dell'array e` quello di ricorrere ad un parametro aggiuntivo, esattamente come accade con la funzione main() (vedi capitolo precedente).
Ovviamente una funzione puo` restituire un tipo puntatore, in questo caso bisogna pero` prestare attenzione a cio` che si restituisce, non e` raro infatti che un principiante scriva qualcosa del tipo:

 

int* Sum(int a, int b) {
  int Result = a + b;
  return &Result;
}

 

Apparentemente e` tutto corretto e un compilatore potrebbe anche non segnalare niente, tuttavia esiste un grave errore: si ritorna l'indirizzo di una variabile locale. L'errore e` dovuto al fatto che la variabile locale viene distrutta quando la funzione termina e riferire ad essa diviene quindi illecito. Una soluzione corretta sarebbe stata quella di allocare Result nello heap e restituire l'indirizzo di tale oggetto (in questo caso e` cura di chi usa la funzione occuparsi della eventuale deallocazione dell'oggetto).
Infine un uso importante dei puntatori e` per passare come parametro un'altra funzione. Si tratta di un meccanismo che sta alla base dei linguaggi funzionali e che permette di realizzare algoritmi generici (anche se in C++ molte di queste cose sono spesso piu` semplici da ottenere con i template, in alcuni casi pero` il vecchio approccio risulta migliore):

 

#include < iostream >
using namespace std;

// Definiamo un tipo funzione:
typedef bool Eval(int, int);

bool Max(int a, int b) {
  return (a>=b)? true: false;
}

bool Min(int a, int b) {
  return (a<=b)? true: false;
}

// Notare il tipo del primo parametro
void Check(Eval* Func, char* FuncName, int Param1, int Param2) {
  cout << "E` vero che " << Param1 << " = " << FuncName << '(' << Param1 << ',' << Param2 << ") ? ";

  // Utilizzo del puntatore per eseguire la chiamata
  // alla funzione puntata (nella condizione dell'if)

  if (Func(Param1, Param2)) cout << "Si" << endl;
  else cout << "No" << endl;
}

int main(int, char* []) {
  for(int i=0; i<10; ++i) {
    cout << "Immetti un intero: ";
    int A;
    cin >> A;
    cout << endl << "Immetti un altro intero: ";
    int B;
    cin >> B;
    cout << endl;

    // Si osservi il modo in cui viene
    // ricavato l'indirizzo di una funzione
    // (primo parametro della Check)

    Check(Max, "Max", A, B);
    Check(Min, "Min", A, B);
    cout << endl << endl;
  }
  return 0;
}

 

La typedef dice che Eval e` un tipo "funzione che prende due interi e restituisce un bool", quindi conformemente al tipo Eval definiamo due funzioni Max e Min dall'evidente significato. Si definisce quindi una funzione Check che riceve quattro parametri: un puntatore a Eval, una stringa e due interi. La funzione Check usa Func per eseguire la chiamata alla funzione puntata e ricavarne il valore restituito. Si noti che la chiamata alla funzione puntata viene eseguita come se Func fosse esso stesso la funzione (ovvero utilizzando l'operatore () e passando normalmente i parametri).
Si noti infine che la funzione main ricava l'indirizzo di Max e Min senza ricorrere all'operatore &, analogamente a quanto si fa con gli array.

 

successivo
–«  INDICE  »–

 

 

 

 
Powered by paper&pencil (carta&matita ) - Copyright © 2001-2022 Cataldo Sasso