Esercizio 2: Algebra dei Puntatori e Puntatori a Puntatori
Transcript
Esercizio 2: Algebra dei Puntatori e Puntatori a Puntatori
Esercizio 2: Algebra dei Puntatori e Puntatori a Puntatori Salvatore Mandrà 7 Ottobre 2008 1 Esercizio L’esercizio prevede l’implementazione di funzioni per il prodotto di una matrice per un vettore, attraverso l’uso dei puntatori. I puntatori sono letteralmente variabili che contengono l’indirizzo di memoria della variabile a cui puntano. Nell’esempio double a = 1 0 ; double ∗ i n d i r i z z o a = &a ; la variabile a avrà il valore 10 mentre la variabile indirizzo a il suo indirizzo di memoria. Se cerchiamo di stampare a video il contenuto di queste due variabili c o u t << a << " -> " << p u n t a t o r e a << " . " << e n d l ; otteniamo 10 −> 0 x b f 8 0 b e f 0 . Se conosciamo l’indirizzo di memoria della variabile possiamo recuperare il valore di quella variabile double a = 1 0 ; double ∗ i n d i r i z z o a = &a ; double v a l a = ∗ i n d i r i z z o a ; c o u t << a << " -> " << p u n t a t o r e a << " = " << v a l a << " . " << e n d l ; che da come risultato 10 −> 0 x b f 8 0 b e f 0 = 1 0 . Conoscendo l’indirizzo di memoria, possiamo modificarne il contenuto 1 double a = 1 0 ; double ∗ i n d i r i z z o a = &a ; c o u t << a << " -> " << p u n t a t o r e a << " . " << e n d l ; ∗ indirizzo a = 20; c o u t << a << " -> " << p u n t a t o r e a << " . " << e n d l ; per ottenere 10 −> 0 x b f 8 0 b e f 0 . 20 −> 0 x b f 8 0 b e f 0 . Attraverso l’uso dei puntatori possiamo implementare gli array. Ad esempio double ∗ a r r a y ; a r r a y = new double [ 1 0 ] ; f o r ( i n t i = 0 ; i < 1 0 ; i ++) a r r a y [9 − i ] = i ∗ i ; f o r ( i n t i = 0 ; i < 1 0 ; i ++) c o u t << a r r a y [ i ] << " -> " << ( a r r a y+i ) << " . " << e n d l ; con il risultato 81 −> 0 x 9 1 8 f 0 0 8 . 64 −> 0 x 9 1 8 f 0 1 0 . 49 −> 0 x 9 1 8 f 0 1 8 . 36 −> 0 x 9 1 8 f 0 2 0 . 25 −> 0 x 9 1 8 f 0 2 8 . 16 −> 0 x 9 1 8 f 0 3 0 . 9 −> 0 x 9 1 8 f 0 3 8 . 4 −> 0 x 9 1 8 f 0 4 0 . 1 −> 0 x 9 1 8 f 0 4 8 . 0 −> 0 x 9 1 8 f 0 5 0 . Sono da notare due cose: la prima è che gli indirizzi sono sequenziali (in base esadecimale) e la differenza in byte tra un indirizzo e l’altro è di otto (in decimale). Questo perché nella mia macchina i double sono di 8 byte anziché gli usuali 4. Per l’utilizzo degli array è fondamentale che l’allocazione della memoria degli elementi sia sequenziale: in questo modo possiamo conoscere gli elementi dell’array partendo dalla “testa” (indicata dall’indirizzo di memoria array) e spostandosi verso destra attraverso l’algebra dei puntatori array + i, che dice di spostarsi a destra rispetto alla testa di i posizioni. Il comando array[i] e l’abbreviazione di *(array + i), ovvero prendi la testa dell’array, spostati a destra di i posizioni e restituiscimi il valore. Quando si l’allocazione dinamica della memoria è fondamentale non modificare le zone di memoria non appositamente allocate per l’array. Frequentemente capita di non impostare i cicli for nel modo corretto 2 double ∗ a r r a y ; a r r a y = new double [ 1 0 ] ; f o r ( i n t i = 0 ; i <= 1 0 ; i ++) a r r a y [9 − i ] = i ∗ i ; f o r ( i n t i = 0 ; i <= 1 0 ; i ++) c o u t << a r r a y [ i ] << " -> " << ( a r r a y+i ) << " . " << e n d l ; che cerca di scrivere nella posizione data dall’indirizzo di memoria (array+10) = &array[10]. Ma C ++ indirizza a partire da &array[0] per cui &array[10] non è stato correttamente allocato. I risultati sono imprevedibili ma spesso portano al Segmentation Fault in fase di esecuzione (ATTENZIONE! Esecuzione non compilazione. Questo perché il codice scritto è assolutamente lecito ed il compilatore non fa controlli sulla dimensione degli array allocati dinamicamente). È possibile riallocare la memoria usando le vecchie librerie di C <stdlib.h> ma lo standard C ++ ne sconsiglia l’uso. Esistono i contenitori standard della libreria STL ma in questo corso non ne faremo uso. Negli esercizi eviteremo quindi di dover riallocare la memoria. È possibile dichiarare funzioni che accettano puntatori come argomenti. All’interno della funzione sarà possibile accedere direttamente alla zone di memoria desiderata e modificarne il contenuto. Il tipico esempio di funzione è i n t f u n c t i o n ( double ∗ a r r a y 1 , i n t ∗ a r r a y 2 ) ; Spesso è necessario dover utilizzare delle tabelle, ovvero oggetti bidimensionali che accettano (a differenza degli array) due interi per indicizzare il valore. Un tipico esempio sono le matrici. Queste tabelle bidimensionali possono essere viste (e sono viste nei linguaggi di programmazione basati su puntatori) come array di array, ovvero un array che punta a degli oggetti che sono a loro volta degli array. Per risolvere questo problema in C/C ++ esistono i puntatori a puntatori che fanno esattamente questo. Nell’esempio seguente vogliamo ricreare una matrice 2 × 3 di double : double ∗∗ m a t r i c e ; m a t r i c e = ∗double [ 2 ] ; f o r ( i n t i = 0 ; i < 2 ; i ++) m a t r i c e [ i ] = new double [ 3 ] ; // Crea l e r i g h e f o r ( i n t i = 0 ; i < 2 ; i ++) f o r ( j = 0 ; j < 3 ; j ++) // Assegna d e i v a l o r i // ad o g n i e l e m e n t o 3 // Crea l e c o l o n n e // p e r OGNI r i g a matrice [ i ] [ j ] = i ∗ j ; // d e l l a m a t r i c e Con il comando matrice = *double[2]; stiamo allocando lo spazio per due righe (ovvero due array). Poiché la variabile matrice è a sua volta una variabile puntatore allora matrice[0] conterrà l’indirizzo della testa del primo array. In modo analogo matrice[1]. Successivamente attraverso un ciclo for allochiamo dinamicamente la memoria per OGNI riga. Se non allocassimo la memoria per ogni riga non avremo lo spazio sufficiente per tutta la matrice. Come nel caso degli array, implementare l’uso di funzioni che richiedono puntatori a puntatori è semplice i n t f u n c t i o n ( double ∗ a r r a y 1 , i n t ∗∗ m a t r i c e d i i n t e r i ) ; Bisogna fare molta attenzione quando si alloca la memoria dinamicamente poiché tale memoria non è autonomamente liberata dal compilatore durante l’esecuzione del programma (ma solo alla sua uscita). Prendiamo il semplice caso #i n c l u d e <i o s t r e a m > using namespace s t d ; double ∗ a l l o c a ( ) { double ∗ v e t t o r e = new double [ 1 0 0 0 0 ] ; return v e t t o r e ; } i n t main ( ) { double ∗ v e t t o r e ; int i = 0 ; while ( 1 ) { vettore = alloca ( ) ; c o u t << ( i ++) << e n d l ; } return 0 ; } In questo caso il programma ha una funzione indipendente che alloca la memoria ma all’interno del programma non ci si preoccupa più di liberarla. Poiché la memoria del sistema è finita prima o poi non ci sarà più spazio per allocare nuove variabili. Di fatto, l’esempio precedente restituisce ... 40163 40164 40165 4 40166 40167 40168 40169 t e r m i n a t e c a l l e d a f t e r t h r o w i n g an i n s t a n c e o f what ( ) : std : : bad alloc Abortito std : : bad alloc ovvero, il sistema ci informa che la memoria si è esaurita. Il codice precedente deve essere ricorretto aggiungendo la chiamata delete che libera la memoria dinamicamente allocata e non più necessaria #i n c l u d e <i o s t r e a m > using namespace s t d ; double ∗ a l l o c a ( ) { double ∗ v e t t o r e = new double [ 1 0 0 0 0 ] ; return v e t t o r e ; } i n t main ( ) { double ∗ v e t t o r e ; int i = 0 ; while ( 1 ) { vettore = alloca ( ) ; c o u t << ( i ++) << e n d l ; /∗ Codice che u t i l i z z a i l v e t t o r e allocato dalla funzione alloca ∗/ delete ( v e t t o r e ) ; } return 0 ; } Questo ci garantisce che la memoria non sarà inutilmente saturata. Nel caso della matrice dell’esempio sopra riportato la liberazione della memoria deve essere fatta nel seguente modo f o r ( i n t i = 0 ; i < 2 ; i ++) delete ( m a t r i x [ i ] ) ; delete ( m a t r i x ) ; ovvero, bisogna innanzitutto liberare la memoria allocata per ogni singola riga e solo successivamente il vettore dei puntatori (cioè in maniera opposta con cui si è allocata la memoria). Prima di concludere voglio riportare un esempio tipico di codice assolutamente lecito e che non conclude in modo anomalo l’esecuzione del programma (ovvero il programma non crasha restituendo “Segmentation Fault”). Consideriamo il seguente codice 5 #i n c l u d e <i o s t r e a m > using namespace s t d ; i n t main ( ) { int i ; int v [ 5 ] ; // V a r i a b i l e i n t // Array d i 5 e l e m e n t i i n t che va da v [ 0 ] . . v [ 4 ] c o u t << &i << e n d l ; c o u t << &v [ 5 ] << e n d l ; // Stampiamo l i n d i r i z z o d e l l a v a r i a b i l e i // Stampiamo l i n d i r i z z o d e l l a r r a y i n // p o s i z i o n e 5 , o v v e r o f u o r i d a l r a n g e a l l o c a t o return 0 ; } il codice eseguito restituirà 0 xbfc3f320 0 xbfc3f320 ovvero possiedono lo stesso indirizzo!!! Questo perché il compilatore C ++ tende ad allocare in modo sequenziale la memoria delle variabili staticamente allocate. Il seguente codice opportunamente creato in maniera errata #i n c l u d e <i o s t r e a m > using namespace s t d ; i n t main ( ) { int i ; int v [ 5 ] ; // V a r i a b i l e i n t // Array d i 5 e l e m e n t i i n t che va da v [ 0 ] . . v [ 4 ] f o r ( i = 0 ; i <= 5 ; i ++) { v [ i ] = 0; c o u t << i ; } return 0 ; } una volta eseguito stamperà a video 123451234512345123451234512345123451234512345123451234512345... e cosı̀ all’infinito! Questo perché l’ultima chiamata nella funzione for, ovvero v[5] = 0, scrive direttamente nell’indirizzo di memoria della variabile i riportandola a zero. L’uso errato dei puntatori può comportare quindi errori veramente sottili e molto difficili da ritrovare in codici lunghi. Progettare: 1. In un file prod.hpp dichiarare la funzione prod per moltiplicare un vettore con una matrice 6 2. Sempre nel file prod.hpp dichiarare le funzioni prod per stampare una matrice o un vettore 3. In un file prod.cpp implementare tali funzioni 4. In un file esercizio2.cpp chiedere all’utente di inserire i valori della matrice e del vettore e stampare a video il vettore risultante. 7