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