Scrigroup - Documente si articole

     

HomeDocumenteUploadResurseAlte limbi doc
BulgaraCeha slovacaCroataEnglezaEstonaFinlandezaFranceza
GermanaItalianaLetonaLituanianaMaghiaraOlandezaPoloneza
SarbaSlovenaSpaniolaSuedezaTurcaUcraineana

BiografiaBiologiaBotanicaChimicaComputerComunicazioneCostruzioneDiritto
EducazioneElettronicaFisicaGeografiaGiochiGrammaticaIngegneriaLetteratura
LibriMarketingMatematicaMedicinaMusicaNutrizionePsicologiaRicette
SociologiaStoriaVarie

Puntatori e reference

computer



+ Font mai mare | - Font mai mic



DOCUMENTE SIMILARE

Puntatori e reference

Oltre ai tipi primitivi visti precedentemente, esistono altri due tipi fondamentali usati solitamente in combinazione con altri tipi (sia primitivi che non): puntatori e reference.



L'argomento di cui ora parleremo potrÀ risultare particolarmente complesso, soprattutto per coloro che non hanno mai avuto a che fare con i puntatori: alcuni linguaggi non forniscono affatto i puntatori (come il Basic, almeno in alcune vecchie versioni), altri (Pascal) invece forniscono un buon supporto; tuttavia il C++ fa dei puntatori un punto di forza (se non il punto di forza) e fornisce un supporto ad essi persino superiore a quello fornito dal Pascal. È quindi caldamente consigliata una lettura attenta di quanto segue e sarebbe bene fare pratica con i puntatori non appena possibile.

Puntatori

I puntatori possono essere pensati come maniglie da applicare alle porte delle celle di memoria per poter accedere al loro contenuto sia in lettura che in scrittura, nella pratica una variabile di tipo puntatore contiene l'indirizzo di una locazione di memoria.

Vediamo alcuni esempi di dichiarazione di puntatori:

short * Puntatore1;

Persona * Puntatore3;

double * * Puntatore2;

int UnIntero = 5;

int * PuntatoreAInt = &UnIntero;

Il carattere * (asterisco) indica un puntatore, per cui le prime tre righe dichiarano rispettivamente un puntatore a short int, un puntatore a Persona e un puntatore a puntatore a double. La quinta riga dichiara un puntatore a int e ne esegue l'inizializzazione mediante l'operatore & (indirizzo di) che serve ad ottenere l'indirizzo della variabile (o di una costante o ancora di una funzione) il cui nome segue l'operatore.

Si osservi che un puntatore a un certo tipo puÒ puntare solo a oggetti di quel tipo, non È possibile ad esempio assegnare l'indirizzo di una variabile di tipo float a un puntatore a char, o meglio in molti casi È possibile farlo, ma viene eseguita una coercizione (vedi appendice A):

float Reale = 1.1;

char * Puntatore = &Reale; // errore !

È anche possibile assegnare ad un puntatore un valore particolare a indicare che il puntatore non punta a nulla:

Puntatore = 0;

In luogo di 0 i programmatori C usano la costante NULL, tuttavia l'uso di NULL comporta alcuni problemi di conversione di tipo; in C++ il valore 0 viene automaticamente convertito in un puntatore NULL di dimensione appropriata.

Nelle dichiarazioni di puntatori bisogna prestare attenzione a diversi dettagli che possono essere meglio apprezzati tramite esempi:

float * Reale, UnAltroReale;

int Intero = 10;

const int * Puntatore = &Intero;

int * const CostantePuntatore = &Intero;

const int * const CostantePuntatoreACostante = &Intero;

La prima dichiarazione contrariamente a quanto si potrebbe pensare non dichiara due puntatori a float, ma un puntatore a float (Reale) e una variabile di tipo float (UnAltroReale ): * si applica solo al primo nome che lo segue e quindi il modo corretto di eseguire quelle dichiarazioni era

float * Reale, * UnAltroReale;

La terza riga mostra come dichiarare un puntatore a un intero costante, attenzione non un puntatore costante; la dichiarazione di un puntatore costante È mostrata nella penultima riga. Un puntatore a una costante consente l'accesso all'oggetto da esso puntato solo in lettura (ma ciÒ non implica che l'oggetto puntato sia effettivamente costante), mentre un puntatore costante È una costante di tipo puntatore (a ), non È quindi possibile modificare l'indirizzo in essa contenuto e va inizializzato nella dichiarazione.

L'ultima riga mostra invece come combinare puntatori costanti e puntatori a costanti per ottenere costanti di tipo puntatore a costante (intera, nell'esempio).

Attenzione: anche const, se utilizzato per dichiarare una costante puntatore, si applica ad un solo nome (come *).

In alcuni casi È necessario avere puntatori generici, in questi casi il puntatore va dichiarato void:

void * Puntatore generico;

I puntatori void possono essere inizializzati come un qualsiasi altro puntatore tipizzato, e a differenza di questi ultimi possono puntare a qualsiasi oggetto senza riguardo al tipo o al fatto che siano costanti, variabili o funzioni; tuttavia non È possibile eseguire sui puntatori void alcune operazioni definite sui puntatori tipizzati.

Operazioni sui puntatori

Dal punto di vista dell'assegnamento, una variabile di tipo puntatore si comporta esattamente come una variabile di un qualsiasi altro tipo primitivo, basta tenere presente che il loro contenuto È un indirizzo di memoria:

int Pippo = 5, Topolino = 10;

char Pluto = 'P';

int * Minnie = &Pippo;

int * Basettoni;

void * Manetta;

// Esempi di assegnamento a puntatori:

Minnie = &Topolino;

Manetta = &Minnie; // 'Manetta' punta a 'Minnie'

Basettoni = Minnie; // 'Basettoni' e 'Minnie' puntano ora

// allo stesso oggetto

I primi due assegnamenti mostrano come assegnare esplicitamente l'indirizzo di un oggetto ad un puntatore: nel primo caso la variabile Minnie viene fatta puntare alla variabile Topolino, nel secondo caso al puntatore void Manetta si assegna l'indirizzo della variabile Minnie (e non quello della variabile Topolino); per assegnare il contenuto di un puntatore ad un altro puntatore non bisogna utilizzare l'operatore &, basta considerare la variabile puntatore come una variabile di un qualsiasi altro tipo, come mostrato nell'ultimo assegnamento.

L'operazione piÙ importante che viene eseguita sui puntatori e quella di dereferenziazione o indirezione che permette l'accesso all'oggetto puntato; l'operazione viene eseguita tramite l'operatore di dereferenziazione * posto prefisso al puntatore, come mostra il seguente esempio:

short * P;

short int Val = 5;

P = &Val; // P punta a Val (cioÈ Val e *P sono

// lo stesso oggetto);

cout << 'Ora P punta a Val:' << endl;

cout << '*P = ' << *P << endl;

cout << 'Val = ' << Val << endl << endl;

*P = -10; // Modifica l'oggetto puntato da P

cout << 'Val È stata modificata tramite P:' << endl;

cout << '*P = ' << *P << endl;

cout << 'Val = ' << Val << endl << endl;

Val = 30;

cout << 'La modifica su Val si riflette su *P:' << endl;

cout << '*P = ' << *P << endl;

cout << 'Val = ' << Val << endl << endl;

Il codice appena mostrato fa sÌ che il puntatore P riferisca alla variabile Val, ed esegue una serie di assegnamenti sia alla variabile che all'oggetto puntato da P mostrandone gli effetti. L'operatore * prefisso ad un puntatore seleziona l'oggetto puntato dal puntatore cosÌ che *P utilizzato come operando in una espressione produce l'oggetto puntato da P.

Ecco quale sarebbe l'output del precedente frammento di codice se eseguito:

Ora P punta a Val:

*P = 5

Val = 5

Val È stata modificata tramite P:

*P = -10

Val = -10

La modifica su Val si riflette su *P:

*P = 30

Val = 30

L'operazione di dereferenziazione puÒ essere eseguita su un qualsiasi puntatore a condizione che questo non sia stato dichiarato void. In generale infatti non È possibile stabilire il tipo dell'oggetto puntato da un puntatore void e il compilatore non sarebbe in grado di trattare tale oggetto.

Ovviamente dereferenziare un puntatore a cui non È stato assegnato un indirizzo È un errore a run time; quando possibile comunque il compilatore segnala eventuali tentativi di dereferenziare puntatori che potrebbero non essere stati inizializzati tramite una warning.

Per i puntatori a strutture (o unioni) È possibile utilizzare un altro operatore di dereferenziazione che consente in un colpo solo di dereferenziare il puntatore e selezionare il campo desiderato:

Persona Pippo;

Persona * Puntatore = &Pippo;

Puntatore->Eta = 40;

cout << 'Pippo.Eta = ' << Puntatore->Eta << endl;

La terza riga dell'esempio dereferenzia Puntatore e contemporaneamente seleziona il campo Eta (il tutto tramite l'operatore ->) per eseguire un assegnamento a quest'ultimo. Nell'ultima riga viene mostrato come utilizzare -> per ottenere il valore di un campo dell'oggetto puntato.

Sui puntatori È definita una speciale aritmetica composta da somma e sottrazione. Se P È un puntatore di tipo T, sommare 1 a P significa puntare all'elemento successivo di un ipotetico array di tipo T cui P È immaginato puntare; analogamente sottrarre 1 significa puntare all'elemento precedente.

È possibile anche sottrarre da un puntatore un altro puntatore (dello stesso tipo), in questo caso il risultato È il numero di elementi che separano i due puntatori:

int Array[10] = ;

int * P1 = &Array[5];

int * P2 = &Array[9];

cout << P1 - P2 << endl; // visualizza 4

cout << *P1 << endl; // visualizza 5

P1+=3; // equivale a P1 = P1 + 3;

cout << *P1 << endl; // visualizza 8

cout << *P2 << endl; // visualizza 9

P2-=5; // equivale a P2 = P2 - 5;

cout << *P2 << endl; // visualizza 4

Sui puntatori sono anche definiti gli usuali operatori relazionali:

< minore di

> maggiore di

<= minore o uguale

>= maggiore o uguale

== uguale a

!= diverso da

Puntatori vs Array

Esiste una stretta somiglianza tra puntatori e array dovuta alla possibilitÀ di dereferenziare un puntatore nello stesso modo in cui si seleziona l'elemento di un array e al fatto che lo stesso nome di un array È di fatto un puntatore al primo elemento dell'array:

int Array[ ] = ;

int * Ptr = Array; // equivalente a Ptr = &Array[0];

cout << Ptr[3] << endl; // Ptr[3] equivale a *(Ptr + 3);

Ptr[4] = 7; // equivalente a *(Ptr + 4) = 7;

La somiglianza diviene maggiore quando si confrontano array e puntatori di caratteri:

char Array[ ] = 'Una stringa'

char * Ptr = 'Una stringa'

// la seguente riga stampa tutte e due le stringhe

// si osservi che non È necessario dereferenziare un char *

// (a differenza degli altri tipi di puntatori)

cout << Array << ' == ' << Ptr << endl;

// in questo modo, invece, si stampa solo un carattere:

// la dereferenziazione di un char * o l'indicizzazione

// di un array causano la visualizzazione di un solo carattere

// perché in effetti si passa all'oggetto cout non un puntatore a

// char, ma un oggetto di tipo char (che cout tratta giustamente

// in modi diversi)

cout << Array[5] << ' == ' << Ptr[5] << endl;

cout << *Ptr << endl;

In C++ le dichiarazioni char Array[ ] = 'Una stringa' e char * Ptr = 'Una stringa' hanno lo stesso effetto, entrambe creano una stringa (terminata dal carattere nullo) il cui indirizzo È posto rispettivamente in Array e in Ptr, e come mostra l'esempio un char * puÒ essere utilizzato esattamente come un array di caratteri.

Esistono tuttavia profonde differenze tra puntatori e array: un puntatore È una variabile a cui si possono applicare le operazioni viste sopra e che puÒ essere usato come un array, ma non È vero il viceversa: il nome di un array non È un puntatore a cui È possibile assegnare un nuovo valore (non È cioÈ modificabile). Ecco un esempio:

char Array[ ] = 'Una stringa';

char * Ptr = 'Una stringa';

Array[3] = 'a'; // Ok!

Ptr[7] = 'b'; // Ok!

Ptr = Array; // Ok!

Ptr++; // Ok!

Array++; // errore, tentativo di assegnamento!

In definitiva un puntatore È piÙ flessibile di quanto non lo sia un array, anche se a costo di un maggiore overhead.

Uso dei puntatori

I puntatori sono utilizzati sostanzialmente per tre scopi:

Realizzazione di strutture dati dinamiche (es. liste linkate);

Realizzazione di funzioni con effetti laterali sui parametri attuali;

Ottimizzare il passaggio di parametri di grosse dimensioni.

Il primo caso È tipico di applicazioni che necessitano di strutture dati che si espandano e si comprimano dinamicamente durante l'esecuzione, ad esempio un editor di testo.

Ecco un esempio:

#include <iostream.h>

// Una lista È composta da tante celle linkate

// tra di loro; ogni cella contiene un valore

// e un puntatore alla cella successiva.

struct TCell ;

// La lista viene realizzata tramite questa

// struttura contenente il numero di celle

// della lista e il puntatore alla prima cella

struct TList ;

void main() // 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(unsigned j=0; j<List.Size; ++j) // estratta

cout << 'Totale = ' << Total << endl;

}

L'esempio mostra come creare e distruggere oggetti dinamicamente.

Il 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, distruggendo le relative celle, e li somma restituendo il totale. Il codice È 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, 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 quantitÀ di RAM da deallocare viene calcolata automaticamente).

In alcuni casi È 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;

Si noti inoltre che gli oggetti allocati nella heap-memory non ubbidiscono alle regole di scoping statico valide per le variabili ordinarie (tuttavia i puntatori a tali oggetti sono sempre soggetti a tali regole), la loro creazione e distruzione È compito del programmatore.

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:

void Change(int * IntPtr)

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 dereferenziazione di tale copia consente comunque la modifica dell'oggetto puntato:

int A = 10;

cout << ' A = ' << A << endl;

cout << ' Chiamata della funzione Change(&A) ' << endl;

Change(&A);

cout << ' Ora A = ' << A << endl;

l'output che il precedente codice produce È:

A = 10

Chiamata della funzione Change(&A)

Ora A = 5

Quello che nell'esempio accade È che la funzione Change riceve l'indirizzo della variabile A e tramite esso È in grado di agire sulla variabile stessa.

L'uso dei puntatori come parametri di funzione non È comunque utilizzato solo per consentire effetti laterali, spesso una funzione riceve parametri di dimensioni notevoli e l'operazione di copia del parametro attuale in un'area privata della funzione ha effetti deleteri sui tempi di esecuzione della funzione; in questi casi È molto piÙ conveniente passare un puntatore:

void Func(BigParam parametro); // funziona, ma È meglio

// la seguente dichiarazione

void Func(const BigParam * parametro);

Il secondo prototipo È piÙ efficiente perché 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 È un array, il compilatore passa sempre un puntatore, mai una copia dell'argomento; in questo caso inoltre l'unico modo che ha la funzione per conoscere la dimensione dell'array È quello di ricorrere ad un parametro aggiuntivo, esattamente come accade con la funzione main (vedi capitolo precedente).

Ovviamente una funzione puÒ restituire un tipo puntatore, in questo caso bisogna perÒ prestare attenzione a ciÒ che si restituisce, non È raro infatti che un principiante scriva qualcosa del tipo:

int * Sum(int a, int b)

Apparentemente È 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 È 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 È cura di chi usa la funzione occuparsi della eventuale deallocazione dell'oggetto).

Reference

I reference (riferimenti) sono un costrutto a metÀ tra puntatori e variabili: come i puntatori essi sono contenitori di indirizzi, ma non È necessario dereferenziarli per accedere all'oggetto puntato (si usano come se fossero variabili). In pratica possiamo vedere i reference come un meccanismo per creare alias di variabili, anche se in effetti questa È una definizione non del tutto esatta.

CosÌ come un puntatore viene indicato nelle dichiarazioni dal simbolo *, cosÌ un reference viene indicato dal simbolo &:

int Var = 5;

float f = 0.5;

int * IntPtr = &Var;

int & IntRef = Var; // nei reference non È necessario

float & FloatRef = f; // usare & a destra di =

Le ultime due righe dichiarano rispettivamente un riferimento di tipo int e uno di tipo float che vengono subito inizializzati usando le due variabili dichiarate prima; un riferimento va inizializzato immediatamente, e dopo l'inizializzazione non puÒ essere piÙ cambiato, si noti che non È necessario utilizzare l'operatore & (indirizzo di) per eseguire l'inizializzazione. Dopo l'inizializzazione il riferimento potrÀ essere utilizzato in luogo della variabile cui È legato, utilizzare l'uno o l'altro sarÀ indifferente:

cout << 'Var = ' << Var << endl;

cout << 'IntRef = ' << IntRef << endl;

cout << 'Assegnamento a IntRef' << endl;

IntRef = 8;

cout << 'Var = ' << Var << endl;

cout << 'IntRef = ' << IntRef << endl;

cout << 'Assegnamento a Var' << endl;

Var = 15;

cout << 'Var = ' << Var << endl;

cout << 'IntRef = ' << IntRef << endl;

Ecco l'output del precedente codice:

Var = 5

IntRef = 5

Assegnamento a IntRef

Var = 8

IntRef = 8;

Assegnamento a Var

Var = 15

IntRef = 15

Dall'esempio si capisce perché, dopo l'inizializzazione, un riferimento non possa essere piÙ associato ad un nuovo oggetto: ogni assegnamento al riferimento si traduce in un assegnamento all'oggetto riferito.

Un riferimento puÃ’ essere inizializzato anche tramite un puntatore:

int * IntPtr = new int (5);

// il valore tra parentesi specifica il valore cui

// inizializzare l'oggetto allocato. Per adesso il

// metodo funziona solo con i tipi primitivi.

int & IntRef = *IntPtr;

Si noti che il puntatore va dereferenziato, altrimenti si legherebbe il riferimento al puntatore (in questo caso l'uso del riferimento comporta implicitamente un conversione da int * a int).

Ovviamente il metodo puÃ’ essere utilizzato anche con l'operatore new:

double & DoubleRef = *new Double;

// Ora si puÃ’ accedere all'oggetto allocato

// tramite il riferimento.

// Di nuovo, È compito del programmatore

// distruggere l'oggetto creato con new

delete &DoubleRef;

// Si noti che va usato l'operatore &, per

// indicare l'intenzione di deallocare

// l'oggetto riferito, non il riferimento!

L'uso dei riferimenti per accedere a oggetti dinamici È sicuramente molto comodo perché È possibile uniformare tali oggetti alle comuni variabili, tuttavia È una pratica che bisognerebbe evitare perché puÃ’ generare confusione e di conseguenza errori assai insidiosi.

Uso dei reference

Nel paragrafo precedente sono stati mostrati alcuni possibili usi dei riferimenti. In veritÀ comunque i riferimenti sono stati introdotti nel C++ come ulteriore meccanismo di passaggio di parametri (per riferimento).

Una funzione che debba modificare i parametri attuali puÃ’ ora essere dichiarata in due modi diversi:

void Esempio(Tipo * Parametro);

oppure in modo del tutto equivalente

void Esempio(Tipo & Parametro);

Naturalmente cambierebbe il modo in cui chiamare la funzione:

long double Var = 0.0;

long double * Ptr = &Var;

// nel primo caso avremmo

Esempio(&Var); // oppure

Esempio(Ptr);

// nel caso di passaggio per riferimento

Esempio(Var);

In modo del tutto analogo a quanto visto con i puntatori È anche possibile ritornare un riferimento:

double & Esempio(float Param1, float Param2)

Puntatori e reference possono essere liberamente scambiati, non esiste differenza eccetto che non È necessario dereferenziare un riferimento.

Probabilmente vi starete chiedendo che motivo c'era dunque di introdurre questa caratteristica, dato che i puntatori erano giÀ sufficienti? Il problema in effetti non nasce con le funzioni, ma con gli operatori; il C++ consente anche l'overloading degli operatori e sarebbe spiacevole dover scrivere qualcosa del tipo:

&A + &B

non si riuscirebbe a capire se si desidera sommare due indirizzi oppure i due oggetti (che potrebbero essere troppo grossi per passarli per valore). I riferimenti invece risolvono il problema eliminando ogni possibile ambiguitÀ e consentendo una sintassi piÙ chiara.

Puntatori vs Reference

Visto che per le funzioni È possibile scegliere tra puntatori e riferimenti, come decidere quale metodo scegliere? I riferimenti hanno un vantaggio sui puntatori, dato che nella chiamata di una funzione non c'È differenza tra passaggio per valore o per riferimento, È possibile cambiare meccanismo senza dover modificare né il codice che chiama la funzione né il corpo della funzione stessa.

Tuttavia il meccanismo dei reference nasconde all'utente il fatto che si passa un indirizzo e non una copia, e ciÃ’ puÃ’ creare

grossi problemi in fase di debugging.

Quando È necessario passare un indirizzo È quindi meglio usare i puntatori, che consentono un maggior controllo sugli accessi (tramite la keyword const) e rendono esplicito il modo in cui il parametro viene passato.

Esiste comunque una eccezione nel caso dei tipi definiti dall'utente tramite il meccanismo delle classi. In questo caso vedremo che l'incapsulamento garantisce che l'oggetto passato possa essere modificato solo da particolari funzioni (funzioni membro e funzioni amiche), e quindi usare i riferimenti È piÙ conveniente perché non È necessario dereferenziarli, migliorando cosÃŒ la chiarezza del codice; le funzioni membro e le funzioni amiche, in quanto tali, sono invece autorizzate a modificare l'oggetto e quindi quando vengono usate l'utente sa giÀ che potrebbero esserci effetti laterali.



Politica de confidentialitate | Termeni si conditii de utilizare



DISTRIBUIE DOCUMENTUL

Comentarii


Vizualizari: 711
Importanta: rank

Comenteaza documentul:

Te rugam sa te autentifici sau sa iti faci cont pentru a putea comenta

Creaza cont nou

Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved