6 Puntatori e gestione della memoria in C++
Transcript
6 Puntatori e gestione della memoria in C++
Cosa sono i puntatori? I puntatori sono fondamentalmente delle variabili che non contengono un valore numerico o alfanumerico, ma un puntatore (ossia un indirizzo) alla locazione di memoria dove è memorizzato un certo valore (intero, reale o carattere) La definizione di una variabile puntatore avviene nello stesso modo con cui si definisce una variabile, eccetto che dobbiamo aggiungere un’asterisco tra il tipo e l’identificatore del puntatore (ossia il nome della variabile puntatore) nel nome conviene indicare una ‘p’ iniziale per ricordarci che è un puntatore Laboratorio di Informatica 6. Puntatori e gestione della memoria in C++ Esempi: int* pOne; short* pnum; Persona* pdip; double** ptot; Corso di Laurea in Ingegneria Elettronica e Telecomunicazioni A.A. 2013-2014 2° Semestre Prof. Giovanni Pascoschi Laboratorio di Informatica – A.A. 2013-2014 Uso dei puntatori a cura di Pascoschi Giovanni 2 Esempio Esempio #include <iostream> using namespace std; int* pOne; int* pTwo; int num1, num2; int nNumber; int* pPointer; int main() { nNumber = 15; pPointer = &nNumber; // stampiamo il valore di nNumber cout<< "nNumber is equal to :”<<nNumber<<endl; // ora, alteriamo nNumber per mezzo di pPointer *pPointer = 25; // stampiamo il nuovo valore di nNumber cout<<"nNumber is equal to:“<<nNumber<<endl; return 0; } pOne = &num1; pTwo = &num2; L’operatore & deve essere letto come “l’indirizzo di” (Address-of) e restituisce l’indirizzo di memoria della variabile e non la variabile. L’operatore * deve essere letto come ‘il contenuto della locazione di memoria puntata da’ a meno che non si trovi in una definizione di variabili puntatore del tipo 'int* pOne’. L’operatore * prende il nome di operatore di dereference (deferenziamento). Laboratorio di Informatica – A.A. 2013-2014 // puntatore ad un int // puntatore ad uno short int // puntatore ad un tipo Persona (p.e. struct) // puntatore ad un puntatore a double a cura di Pascoschi Giovanni 3 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 4 Puntatori : cosa succede nella memoria Cosa succede nella memoria Le definizioni int n=7 e int* pnum, determinano la presenza di due locazioni di memoria allocate nell’area dati #include <iostream> using namespace std; 8A00 int n=7; int* pnum; n int main( ) { pnum = &n; *pnum = 13; return 0; } pnum 1 byte 7 8A01 1 byte 8A02 1 byte 8A03 1 byte Si suppone che il tipo int richieda 2 bytes di memoria ed assumendo che un puntatore occupi anch’esso 2 bytes. la figura sono anche indicati gli indirizzi (in esadecimale) di ciascun byte. L’indirizzo di n è per definizione l’indirizzo del primo byte, cioè 8A00. L’indirizzo di pnum è 8A02. Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 5 Cosa succede nella memoria Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 6 Esempio #include <iostream> using namespace std; Le istruzioni: p=&n; *p=13; int x = - 5, y = 10; int* p; hanno il seguente effetto in memoria: 8A00 n pnum 1 byte 13 8A01 8A02 8A03 Laboratorio di Informatica – A.A. 2013-2014 int main( ) { p = &y; x = *p; return 0; } 1 byte 8A00 // equivale all’istruzione x = y 1 byte 1 byte a cura di Pascoschi Giovanni 7 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 8 Esempio Esempio 8A00 x y p Le istruzioni: p = &y; x = *p; hanno il seguente effetto in memoria 1 byte -5 8A01 8A02 1 byte 10 8A00 1 byte 8A03 1 byte 8A04 1 byte 8A05 1 byte 8A01 9 Puntatori : casi particolari 10 1 byte 8A03 1 byte 8A04 1 byte 8A05 a cura di Pascoschi Giovanni 1 byte 8A02 y p Laboratorio di Informatica – A.A. 2013-2014 1 byte 10 x 8A02 Laboratorio di Informatica – A.A. 2013-2014 1 byte a cura di Pascoschi Giovanni 10 Uso dei puntatori : compatibilità di tipo tra puntatori float* uno, due; // dichiarazione due puntatori a float? NO float* uno, float* due; // dichiarazione due puntatori a float? SI Un puntatore a un tipo T può contenere solo indirizzi di variabili di tipo T. Quindi, tipi di puntatori diversi sono incompatibili fra loro NO CASTING Esempi int x = 10; float * py; py = &x; int Intero = 10; void* PuntatoreGenerico /*dichiarazione puntatore generico puo’ puntare a un qualsiasi tipo */ // non si puo’ fare // int x = 10,int * py; float* pz; py = &x; pz = py; // non si puo’ fare // MOTIVO:l’informazione sul tipo del puntatore serve a dedurre il tipo dell’oggetto puntato, che è indispensabile per effettuare correttamente il dereferenziamento Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 11 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 12 Esempio : operazioni sui puntatori Uso dei puntatori : operazioni con i puntatori Se P è un puntatore, ed n è un int (eventualmente anche negativo) cosa significa un’espressione come : #include <iostream> using namespace std; P+n ??? int main( ) { int a=4, b=15; int* pnum; pnum=&a; (*pnum)++; pnum = &b; (*pnum)- -; cout<<a<<endl; cout<<b<<endl; return 0; } Denota un altro puntatore, che punta “n locazioni dopo” la locazione puntata da P (“locazioni” non “byte”!) Per “locazione” si intende l’insieme di byte necessari a memorizzare una variabile di un certo tipo (provare a utilizzare p.e. sizeof(float) ) // incrementa a (parentesi necessarie) // // decrementa b // stampa 5 // stampa 14 Laboratorio di Informatica – A.A. 2013-2014 – Se int richiede 2 (o 4) byte 1 locazione = 2 bytes – Se float richiede 4 byte 1 locazione = 4 bytes – Se double richiede 8 byte 1 locazione = 8 bytes a cura di Pascoschi Giovanni 13 Uso dei puntatori : operazioni con i puntatori Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 14 Uso dei puntatori : inizializzazione dei puntatori Se n è negativo, la locazione denotata da P+n precede in realtà quella puntata da P E’ buona regola non lasciare indefiniti i puntatori, per cui è bene inizializzarli subito dopo il momento in cui vengono dichiarati Esempio Analogamente, se P e Q sono due puntatori allo stesso tipo T, l’espressione P-Q denota il numero (intero) di celle che separano Q da P (eventualmente negativo se Q precede P) int *p; p=0; oppure p=NULL; • NULL è una costante simbolica dichiarata anche nella libreria <iostream> NOTA: operazioni tra variabili puntatori a tipi diversi (P puntatore a tipo int, Q puntatore a tipo float), come P+Q, sono illegali Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 15 • 0 (zero) è l’unico valore int che si può assegnare ad un puntatore Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 16 Vettori e puntatori Vettori e puntatori Un vettore (array) A è un insieme finito di N variabili dello stesso tipo, ognuna identificata da un indice Un vettore è organizzato come un puntatore costante, inizializzato a puntare un’area di memoria opportunamente allocata al momento della dichiarazione Il nome del vettore A rappresenta in realtà l’indirizzo iniziale dell’area di memoria associata al vettore stesso Le seguenti notazioni sono equivalenti: int vett[10]; int* vett; • L’operatore [ ], applicato ad un nome di vettore, accede alla i-esima cella del vettore • L’operatore *, applicato ad un puntatore, accede alla variabile da esso puntata • Poiché vett è il puntatore al primo elemento del vettore, vale la relazione: *vett = vett[0]; *(vett+1) = vett[1]; ...... *(vett+n-1) = vett[n-1]; Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 17 Vettori e puntatori (aritmetica dei puntatori) Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 18 Vettori e puntatori : esempio Le relazioni viste per i vettori: *A = A[0] int Array[ ] = { 1, 2, 3, 4, 5 }; *(A+i) = A[i] sono un’applicazione dell’aritmetica dei puntatori. A denota il puntatore costante alla cella di memoria che contiene la prima variabile del vettore int* pA = Array; // equivale a pA = &Array[0]; cout << pA[3] << endl; // pA[3] equivale a *(pA+3); pA[4] = 7; // equivalente a *(pA+4) = 7; (A+i) denota un altro puntatore che punta “i locazioni dopo” la locazione puntata da A *(A+i) denota il contenuto della locazione di memoria che si trova “i locazioni dopo” la locazione puntata da A Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 19 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 20 Puntatori e stringhe Array di stringhe Una stringa si puo’ anche definire in maniera analoga al C: Un array di stringhe si puo’ dichiarare in diversi modi: char Array[ ] = "Una stringa"; char* pa = Array; cout << Array << " == " << pa << endl; // non è necessario usare *pa cout << Array[5] << " == " << pa[5] << endl; // stampa solo un elemento Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 21 Puntatori a puntatore string Array[10]; // array di stringhe (stile C++) char Array [10][20]; // matrice di caratteri char* pArr[10]; // array di puntatori a carattere Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 22 Puntatori a strutture Un puntatore puo’ puntare qualunque tipo anche un altro puntatore Un puntatore puo’ puntare qualunque tipo l’operatore -> int A = 10; anche una struttura si usa Esempio: int* pA = &A; struct data { int** ppA = &pA; int giorno; A = 50; // assegna ad A 50 *pA = 50; // assegna ad A 50 **ppA = 50; // assegna ad A 50 string mese; int anno; }; data* pdata; data d; pdata = &d; (*pdata).giorno = 31;//per assegnare al campo giorno della struttura d il valore 31 opp. pdata-> giorno = 31; //per assegnare al campo giorno della struttura d il valore 31 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 23 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 24 Confronto reference(riferimenti) e puntatori Confronto reference(riferimenti) e puntatori cout << “b = " << b << endl; //stampa b = 5 cout << “pb = " << pb << endl; // stampa indirizzo di b int b = 5; cout << "Assegnamento a pb..." << endl; float f = 0.1; *pb = 8; int* pb = &b; // si crea un puntatore a intero che punta a b cout << “b = " << b << endl; // stampa b = 8 int& bref = b; // si crea una variabile alias di b cout << “bref = " << bref << endl; // stampa bref = 8 float& ff = f; // si crea una variabile alias di f cout << "Assegnamento a b..." << endl; Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 25 Confronto reference(riferimenti) e puntatori b = 15; // oppure bref = 15; cout << “b = " << b << endl; // stampa b = 15 cout << “bref = " << bref << endl; // stampa bref = 15 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 26 Funzioni con parametri reference(riferimenti) e puntatori il passaggio di parametri per indirizzo puo’ avvenire con due metodi : reference e puntatori dopo l'inizializzazione, un riferimento non puo’ piu’ essere associato ad un nuovo oggetto: ogni assegnamento al riferimento si traduce in un assegnamento all'oggetto riferito (sono alias). // metodo per reference (riferimento) int main( ) { .... Un riferimento puo` essere inizializzato anche tramite un puntatore: swap (num1, num2); int a = 5; .... int* pInt = &a; int& aRef = *pInt; } // aRef è un alias di a tramite pInt void swap (int& a, int& b) { int comodo; comodo = a; a = b; b = comodo; } Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 27 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 28 Funzioni con parametri reference(riferimenti) e puntatori Confronto reference e puntatori // metodo per puntatori Vantaggio reference : nella chiamata di una funzione non c'e` differenza tra passaggio per valore o per riferimento e` possibile cambiare meccanismo senza dover modificare ne` il codice che chiama la funzione ne` il corpo della funzione stessa; il codice è piu’ leggibile int main( ) { .... swap (&num1, &num2); Svantaggio reference : il meccanismo dei reference nasconde all'utente il fatto che si passa un indirizzo e non una copia, e cio` puo` creare grossi problemi in fase di debugging del programma .... } Vantaggio puntatori: i puntatori consentono un maggior controllo sugli accessi (tramite la keyword const per impedire la modifica) e rendono esplicito il modo in cui il parametro viene passato (nel seguito vedremo che in alcuni casi è piu’ conveniente usare i reference per rendere piu’ leggibile il codice) void swap (int* a, int* b) { int comodo; comodo = *a; *a = *b; *b = comodo; } Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 29 Principale utilizzo dei puntatori Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 30 Gestione della memoria I puntatori sono utilizzati sostanzialmente per quattro scopi: 1. Realizzazione di strutture dati dinamiche (es. liste linkate); dopo 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. A ciascun programma utente viene assegnata una porzione di memoria, che è automaticamente divisa in quattro porzioni: area del codice contiene il codice del programma area statica che contiene le variabili globali e statiche Il primo caso e` tipico di applicazioni per le quali non e` noto a priori la quantita` di dati che si andranno a manipolare. Dichiarando un array si pone un limite massimo al numero di oggetti di un certo tipo immediatamente disponibili. area heap disponibile per allocazioni dinamiche area stack contiene i record di attivazione delle funzioni (variabili locali e parametri). Utilizzando i puntatori invece e` possibile realizzare ad esempio una lista il cui numero massimo di elementi non e` definito a priori. (p.e. anagrafe comune bari) Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 31 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 32 Mappa della memoria Gestione della memoria Area Programma Le dimensioni delle aree Programma e Statica sono fisse e sono decise in fase di compilazione. Tali dimensioni corrispondono al numero di righe in linguaggio macchina del programma e al numero di variabili globali/statiche definite in esso. Area Statica Le altre due aree (Heap e Stack) non hanno dimensione fissa. Tale dimensione varia nel tempo a seconda dell’utilizzo delle due aree. In particolare per quanto riguarda l’area Heap, essa crescerà quando verranno allocate variabili dinamiche. La sua dimensione si ridurrà, invece, quando una o più variabili dinamiche verranno deallocate. Heap Area Dati Stack Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 33 Area di stack a cura di Pascoschi Giovanni 34 Area Heap L'area di memoria Stack (pila) é caratterizzata dalla proprietà che l'ultimo dato che entra in memoria nella fase di scrittura è il primo ad uscire nella fase di lettura (LIFO = Last In First Out). Appena l’esecuzione passa dal programma chiamante a una funzione viene allocato nello Stack un pacchetto di dati (record di attivazione). L’area Heap è soggetta a regole di visibilità e tempo di vita completamente diverse da quelle che governano l'area Stack e precisamente: l'area heap non é allocata automaticamente, ma può essere allocata o rimossa solo su esplicita richiesta del programma (allocazione dinamica della memoria); l'area allocata non é identificata da un nome, ma é accessibile esclusivamente tramite la definizione di un puntatore; la sua visibilità é legata a quella della variabile puntatore che contiene il suo indirizzo; il suo tempo di vita coincide con l'intera durata del programma, a meno che non venga esplicitamente deallocata; se il puntatore va in una particolare condizione, denominata out of scope, l'area non é più accessibile, ma continua a occupare memoria inutilmente. Il record di attivazione contiene l'indirizzo di rientro nel programma chiamante e la lista degli argomenti passati alla funzione. Esso viene accatastato sopra il pacchetto precedente (quello del programma chiamante) e poi automaticamente rimosso dalla memoria appena l'esecuzione della funzione é terminata. Nella memoria stack vengono memorizzate anche altre informazioni, e precisamente i dati relativi a tutte le variabili automatiche (cioè locali e non statiche) create dalla funzione. Il loro tempo di vita é legato all'esecuzione della funzione proprio perché, quando la funzione termina, l'intera area stack allocata viene rimossa. Laboratorio di Informatica – A.A. 2013-2014 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 35 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 36 Gestione dinamica della memoria (operatore new) Gestione dinamica della memoria (operatore new) tipo é il tipo dell'oggetto (o degli oggetti) da creare; Operatore new Per allocare in modo dinamico la memoria il C++ mette a disposizione l'operatore new. Questo operatore costruisce uno o più oggetti nell'area heap e ne restituisce l'indirizzo. In caso di errore (memoria non disponibile) restituisce NULL occorre controllare l’allocazione dello spazio di memoria. dimensione é il numero degli oggetti, che vengono sistemati nella memoria heap consecutivamente (come gli elementi di un array); se questo operando é omesso, viene costruito un solo oggetto; se é presente, l'indirizzo restituito da new punta al primo oggetto dell’array; La sintassi dell'operatore new è: valore_iniziale é il valore con cui l'area allocata viene inizializzata (deve essere di un tipo coerente); se é omesso l'area non é inizializzata. new tipo[dimensione] (valore_iniziale) N.B.: dei tre operandi dell'istruzione solo il primo è obbligatorio Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 37 Operatore new (esempi) Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 38 Operatore new (esempi) Esempi : Esempi : int* pInt = new int; si alloca un oggetto int nell'area heap e si usa il suo indirizzo per inizializzare il puntatore pInt struct persona { int a; string b; }; persona* p_pers; p_pers = new persona[100] + 20; int* pArr = new int [10]; // allocazione di un array si allocano 10 oggetti int nell'area heap e si usa l'indirizzo del primo oggetto per inizializzare il puntatore pArr In tal caso si definisce la struttura persona e si dichiara il puntatore p_pers a questa struttura al puntatore p_pers si assegna l'indirizzo del ventesimo dei cento oggetti di tipo persona, allocati nell'area heap int* pMat = new int[20][20]; // allocazione di una matrice si alloca una matrice 20x20 di interi nell’area heap e si usa l’indirizzo del primo oggetto [0][0] per inizializzare il puntatore pMat Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni // allocazione di una tabella 39 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 40 Gestione dinamica della memoria (operatore delete) Gestione della memoria Per deallocare la memoria dell'area heap in C++ mette a disposizione Questo operatore non restituisce alcun valore l'operatore delete Se l'operando punta a un'area in cui sono stati allocati più oggetti, delete va specificato con una coppia di parentesi quadre (senza la dimensione, che il C++ é in grado di riconoscere automaticamente). La sintassi dell'operatore delete è: float* pArr = new float [100] ; delete [ ] pArr; // alloca 100 oggetti float // libera tutta la memoria allocata delete nome_punt l'operatore delete non cancella la variabile nome_punt, né altera il suo contenuto: l'unico effetto é di liberare la memoria puntata rendendola disponibile per ulteriori allocazioni Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 41 Allocazione dinamica della memoria int* punt = new int; int a; punt = &a; // alloca un int nell'area heap /*assegna a punt un indirizzo dell'area stack (out of scope), per cui l'oggetto int dell'area heap non é più raggiungibile e deallocabile*/ Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 42 Gestione dell’overflow di memoria La memoria heap non è infinita se ci si dimentica di usare l’operatore delete e si ripete più volte l’uso dell’operatore new, si può avere out-ofmemory E’ necessario non lasciare che ciò avvenga deallocando le aree di memoria prima che si perdano i puntatori e diventino inaccessibili (operazione di garbage collection) Per gestire / trappare situazioni di errore di allocazione dinamica (p.e. mancanza di spazio nella memoria heap) set_new_handler( ) funzione che serve a gestire gli errori dell’operatore new, la quale ha come parametro un puntatore a funzione. Pertanto quando si allocano degli oggetti con l’operatore new, è necessario verificare che il puntatore restituito non è un puntatore nullo (NULL). Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 43 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 44 Esempio : Gestione dell’overflow di memoria Riepilogo della lezione #include <iostream> #include <new> using namespace std; void mem_fault( ); int main( ) { set_new_handler (mem_fault); void mem_fault( ) { cout<< “ Fault memoria” << endl; exit(1); } Puntatori e gestione della memoria Puntatori Corretto uso dei puntatori (aritmetica dei puntatori) Confronto tra reference e puntatori Gestione della memoria Allocazione dinamica della memoria in C++ Gestione overflow memoria in C++ long dim; int* pmem; int blocco; cout << “Quanta memoria?”; cin>> dim; for (blocco = 1; ; blocco++) { pmem = new int(dim); cout<<“Alloc. blocco: “<<blocco<<endl; } } Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 45 a cura di Pascoschi Giovanni 47 Fine della lezione Domande? Laboratorio di Informatica – A.A. 2013-2014 Laboratorio di Informatica – A.A. 2013-2014 a cura di Pascoschi Giovanni 46