1
2 Variabili statiche e dinamiche Un programma è un processo in esecuzione a cui il sistema operativo assegna una certa zona di memoria. Tale zona può essere suddivisa in quattro grandi blocchi: STACK Stack: zona di memoria che costituisce un’area di appoggio temporaneo CODICE Codice: contiene il codice macchina del programma HEAP Heap: zona di memoria libera di essere utilizzata sul momento DATI Dati: contiene tutte le variabili e le costanti definite nel programma
3 Il blocco stack è utilizzato principalmente dalle procedure e funzioni richiamate dal programma; in particolare lo stack conserva le variabili locali, i valori dei parametri formali passati per valore e gli indirizzi di quelli passati per riferimento, l’indirizzo di ritorno della procedura. Il blocco heap è riservato alle variabili dinamiche. Lo spazio occupato dai dati e dal codice è fisso. STACK CODICE HEAP DATI
4 Allocazione statica. Memoria viene allocata automaticamente in un processo mediante una dichiarazione esplicita di un identificatore che rappresenta una variabile o una costante che appare nella lista dei parametri di una function. Tale memoria viene automaticamente deallocata quando il processo termina.
5 Allocazione dinamica. La memoria è allocata e/o deallocata in un processo mediante delle istruzioni particolari definite in quel processo. Di norma, tale memoria non è disponibile prima che l’opportuna istruzione sia posta in essere e potrebbe continuare o meno ad esistere dopo che il processo è terminato.
6 Una variabile allocata dinamicamente è detta variabile dinamica: per introdurre una tale variabile è necessario servirsi di un puntatore.
7 Puntatori Come è noto in un computer la memoria è organizzata in celle consecutive, ciascuna della grandezza di un byte. Queste celle possono essere gestite singolarmente (ad esempio, per oggetti di tipo char che occupano un 1 byte, o a gruppi (ad esempio, per oggetti di tipo float che occupano 4 byte, …). Ad ogni oggetto viene associato un indirizzo di memoria che rappresenta il primo byte della zona ad esso riservata.
8 Puntatori Definiamo puntatore una variabile che contiene l’indirizzo di un’altra variabile: la variabile p conservata, ad esempio a partire dall’indirizzo 0x0064fd0, contiene l’indirizzo 0x0064fef del primo byte della variabile float pi che è un numero reale. Questa situazione viene descritta scrivendo float pi=3.1415; float* p; p pi=3.14 0x0064fef
9 Puntatori Per identificare una variabile di tipo puntatore si usa un * come suffisso al tipo che la variabile rappresenta, come mostrato di seguito: float pi=3.1415; float* p, float* q; p = π p pi=3.14 0x0064fef L’istruzione float* p associa alla variabile puntatore di nome p un indirizzo a partire dal quale si prevede un’occupazione di memoria necessaria per il tipo float. Per associare alla variabile p ad esempio il valore contenuto in pi si pone p=&pi. Ovviamente pi deve essere dello stesso tipo di p.
10 Puntatori float pi=3.1415; float* p, float* q; p = π Se ora scriviamo q = p; Abbiamo che anche la variabile q punta allo stesso indirizzo purchè sia dello stesso tipo di p: l’assegnazione sta ad indicare che l’indirizzo contenuto in p viene copiato in q (che vale ancora 0x0064fef) e che punta, quindi, allo stesso contenuto. p pi=3.14 0x0064fef q
11 float n 44.5 ………….. 0x0064fd0 0x0064fd1 0x0064fd2 0x0064fd3 0x0064fd4 0x0064fd5 0x0064fd6 0x0064fd7 0x0064fd8 0x0064fd9 0x0064fda 0x0064fdb 0x0064fdc 0x0064fdd 0x0064fde 0x0064fdf 0x0064fe0 0x0064fe1 0x0064fe2 0x0064fe3 0x0064fe4 ……………… ………….. Ad esempio se alla variabile float n=44.5 è stata assegnata la cella 0x0064fd5 (in esadecimale) in memoria si avrà la situazione mostrata a sinistra. 0x0064fd5 44.5
12 float n 44.5 ………….. 0x0064fd0 0x0064fd1 0x0064fd2 0x0064fd3 0x0064fd4 0x0064fd5 0x0064fd6 0x0064fd7 0x0064fd8 0x0064fd9 0x0064fda 0x0064fdb 0x0064fdc 0x0064fdd 0x0064fde 0x0064fdf 0x0064fe0 0x0064fe1 0x0064fe2 0x0064fe3 0x0064fe4 ……………… ………….. Se ora dichiariamo la variabile puntatore float* pn avremo che in memoria verrà allocato un byte per pn. 0x0064fd float n=44.5; float* pn; pn= &n; 0x0064fd5 float* pn 0x0064fd5 0x0064fd3
13 …………………………………. 0x0064fd0 0x0064fef …………………………………. 4 byte float* p p = &pi pi= Ricapitolando definiamo puntatore una variabile che contiene l’indirizzo di un’altra variabile: la variabile p conservata a partire dall’indirizzo 0x0064fd0, contiene l’indirizzo 0x0064fef del primo byte della variabile float pi che è un numero reale. & è l’operatore di indirizzo x0064fef
14 Definite due variabili puntatore p e q ad un float; float* p, float* q; Supponiamo che esse abbiano come indirizzo (ad es. 0x0064fd0 e 0x0064fd9). Inizialmente il valore di p è nullo. Posto p = π alla variabile puntatore p viene associato l’indirizzo della variabile pi ottenendo la situazione vista prima ( l’operatore & è l’operatore di indirizzo ); Posto q = p; Nell’indirizzo della variabile puntatore q, che deve essere dello stesso tipo di p, viene scritto lo stesso indirizzo contenuto in p (che vale ancora 0x0064fef ) e che punta allo stesso contenuto.
15 Il simbolo * posto vicino al tipo della variabile è l’operatore di dichiarazione di puntatore; la scrittura Tipo* p indica che p è una variabile di tipo puntatore che punta a una variabile di tipo Tipo; poiché il puntatore fornisce l’indirizzo soltanto del primo byte, il sistema conosce esattamente la sua lunghezza soltanto attraverso il tipo. È assolutamente vietato far puntare un puntatore ad un oggetto che non sia dello stesso tipo di quello proposto nella dichiarazione; per esempio, tenendo presente le dichiarazioni float* p; int* q il compilatore segnala errore ad una assegnazione del tipo q=p ATTENZIONE alla istruzione float* p, q; p è un puntatore ad un float mentre q è una variabile float.
16 Assegnato un puntatore, come si fa ad ottenere l’oggetto da esso puntato? Se ap punta ad a, possiamo ottenere il valore di a direttamente da ap: l’espressione *ap calcola il valore di a. Tale operatore viene detto operatore di direzione o operatore di derenferenziazione.
17 L’istruzionecout<<*p stamperà il contenuto dell’oggetto puntato da p, cioè , Mentrecout<<p stamperà il valore del puntatore p in esadecimale, cioè l’indirizzo a cui punta p (es. 0x0064fef in esadecimale ). Es. il codice che segue produce l’output di figura. int main() { int a; int *ap; cout<<"a="; cin>>a; ap=&a; cout<<"indirizzo di a = "<<&a<<" valore di a = "<<a<<endl; cout<<"indirizzo di ap = "<<&ap<<" valore di ap = "<<ap<<endl; cout<<" valore a cui punta ap = "<<*ap<<endl; system(“pause”) }
18 0x22ff74 0x22ff72 0x22ff78 0x22ff73 44 …………. int a; int *ap; cin>>a; ap=&a; cout<<"indirizzo di a = "<<&a<<" valore di a = "<<a<<endl; cout<<"indirizzo di ap = "<<&ap<<" valore di ap = "<<ap<<endl; cout<<" valore a cui punta ap = "<<*ap<<endl; *ap 0x22ff74 0x22ff75 0x22ff76 0x22ff77 &a a=44 0x22df70&ap ap Le operazioni che avvengono in memoria sono illustrate in figura.
19 E’ possibile assegnare ad un puntatore il valore 0 (o la costante simbolica NULL) in tal caso il puntatore non punta a nulla. L’indirizzo è di norma composto da tutti zeri e non viene allocata memoria. int* p=0; // p è un puntatore nullo che non punta a nessun oggetto. Il puntatore nullo non può essere dereferenziato: i risultati sono imprevedibili ed il programma potrebbe andare in crash.
20 E’ possibile definire puntatori a vari livelli come mostrato di seguito: { int n=44; cout<<" n= "<<n<<endl; cout<<" &n= "<<&n<<endl; int* pn=&n; // pn contiene l'indirizzo di n cout<<" pn= "<<pn<<endl; cout<<" &pn= "<<&pn<<endl; cout<<" *pn= "<<*pn<<endl; int** ppn=&pn ; //ppn contiene l'indirizzo di pn cout<<" ppn= "<<ppn<<endl; cout<<" &ppn= "<<&ppn<<endl; cout<<" *ppn= "<<*ppn<<endl; cout<<" **ppn= "<<**ppn<<endl; } 44 ppn int** pn int* n int 0x22ff60 0x22ff68 0x22ff64
21 Un puntatore si dice costante se l’indirizzo non può essere modificato. int n,m,n1=10; int * const p1= &n1 ; p1 = &m ; // l’indirizzo di p1 non può essere modificato: ERRATO *p1=m; //ma il suo contenuto può essere modificato: CORRETTO const int* const p2=&n1; // non è possibile modificare né l’indirizzo di p2 né il suo contenuto 21 Allegato puntatori
22 Puntatori ed array Quando il compilatore incontra un’espressione tipo a[i], dove a è un array di n elementi ed i il suo indice, genera il codice per calcolare l’indirizzo di a[i]. Ad esempio, per controllare l’espressione a[i] < a[i+1] il compilatore deve prima calcolare gli indirizzi di a[i] e di a[i+1] e poi eseguire il confronto tra i due valori.
23 Puntatori ed array Vediamo il calcolatore come opera. Innanzitutto l’array deve essere di un Tipo dichiarato (per es. int o double). Poiché l’array è rappresentato in memoria come una sequenza di byte, il nome a dell’array rappresenta il suo indirizzo-base, cioé l’indirizzo del suo primo elemento, a[0]. Inoltre, tutte le componenti dell’array hanno la stessa lunghezza in byte (che possiamo determinare con sizeof(Tipo)), per cui gli indirizzi dei vari elementi dell’array possono essere calcolati come a + i x sizeof(Tipo) per i=0, 1, 2 ………., n
24 Il compilatore sostituisce ogni riferimento ad a[i] con il calcolo precedente eseguendo le operazioni di addizione e moltiplicazione. Una maniera più efficiente di operare è quello di inizializzare una variabile puntatore all’indirizzo-base dell’array e poi aggiungere la quantità sizeof(Tipo) ad ogni passo del ciclo. Ciò può essere ottenuto in maniera semplice scrivendo *(a + i) il compilatore: 1.determina l’indirizzo del primo elemento dell’array a; 2.aggiunge i volte la grandezza del Tipo dell’elemento; 3.restituisce l’indirizzo totale.
25 int main() { const int Size=3; short a[Size]={22,33,44}; cout<<“a “<<a<<endl; cout<<“size of short “<<sizeof(short)<<endl; short* end=a+Size; short sum=0; for (short* p=a;p<end;p++) { sum+=*p; cout<<“\t p= “<<p; cout<<“\t *p= “<<*p; cout<<“\t sum= “<<sum<<endl; } Allegato scorrArrayPuntatori Il codice mostrato a destra e l’output sottostante mostrano gli indirizzi e il contenuto dell’array sommato passo passo.
26 A titolo esemplificativo riscriviamo le function inserite nel file InsertArray.h. Ricordiamo che la chiamata del vettore deve essere fatta con un puntatore. Allegato InsertArray.h
27 void LeggeVettore(int [],const int,char, int=100,bool=true); void StampaVettore (const int [],const int, char); void LeggeVettore (int vet[],const int n, char nome, int nr, bool Acaso) { int i; if (Acaso){ srand(time(0)); for (i=0; i<n; i++) { vet[i]=rand() % nr - nr/2; cout<<nome<<"["<<i<<"]="<<vet[i]<<" "; } cout<<endl;} else { for (i=0; i<n; i++) { cout<<nome<<"["<<i<<"]="; cin>>vet[i]; } void StampaVettore (const int vet[], const int n, char nome) { int i; cout<<nome<<"={"; for (i=0; i<n; i++) if (i<n-1) cout<<vet[i]<<","; else cout<<vet[i]<<"}"<<endl; } È necessario introdurre il simbolo di puntatore in entrambi i casi evidenziati: int * in luogo di int [ ] L’istruzione in grassetto va sostituita con *(vet+i)=rand() % nr - nr/2; che risulterà essere più veloce. In generale, ogni occorrenza di vet[i] deve essere sostituita con *(vet+i)
28 void LeggeVettore(int*,const int, char, int=100, bool=true); void StampaVettore (const int*,const int, char); void LeggeVettore(int *vet,const int n, char nome,int nr,bool Acaso) { int i; if (Acaso){ srand(time(0)); for (i=0; i<n; i++) { *vet[i+]=rand() % nr - nr/2; cout<<nome<<"["<<i<<"]="<<vet[i]<<" "; } cout<<endl;} else { for (i=0; i<n; i++) { cout<<nome<<"["<<i<<"]="; cin>>*vet[i+]; } void StampaVettore(const int *vet, const int n, char nome) { int i; cout<<nome<<"={"; for (i=0; i<n; i++) if (i<n-1) cout<<*vet[i+]<<","; else cout<<*vet[i+]<<"}"<<endl; } Il codice riscritto è illustrato a destra. Allegato InsertArrayPunt.h void LeggeVettore(int [],const int,char, int=100,bool=true); void StampaVettore (const int [],const int, char); void LeggeVettore (int vet[],const int n, char nome, int nr, bool Acaso) { int i; if (Acaso){ srand(time(0)); for (i=0; i<n; i++) { vet[i]=rand() % nr - nr/2; cout<<nome<<"["<<i<<"]="<<vet[i]<<" "; } cout<<endl;} else { for (i=0; i<n; i++) { cout<<nome<<"["<<i<<"]="; cin>>vet[i]; } void StampaVettore (const int vet[], const int n, char nome) { int i; cout<<nome<<"={"; for (i=0; i<n; i++) if (i<n-1) cout<<vet[i]<<","; else cout<<vet[i]<<"}"<<endl; }
29 Allegato InsertArrayPunt.h // Selection sort con puntatori sortPuntatori2 Esercizio Scrivere una procedura ricorsiva che su un array ordinato ricerca Un determinato valore utilizzando la ricerca binaria utilizzando i puntatori. Esercizio Scrivere una procedura ricorsiva che ordini con l’inserction sort un array assegnato da tastiera utilizzando i puntatori.
Se si vuole che la cella puntata non possa essere modificata, allora è utile introdurre la clausola const davanti alla definizione ottenendo il puntatore a costante. ……………. int n=3; const int* p1; // p1 punta ad un intero costante p1=&n; // viene segnalato un errore perché la cella n non può essere modificata Dall’esempio si trae inoltre che un oggetto dichiarato costante può essere puntato soltanto da un puntatore costante.