Uploaded by Daimon

Puntatori in C

advertisement
I PUNTATORI in C (C++)
SOMMARIO
1. Valore e Indirizzo di una variabile ............................................................................................ 2
2. Dichiarazione di puntatori ......................................................................................................... 3
3. Puntatori a puntatori .................................................................................................................. 5
4. Uso dei puntatori ....................................................................................................................... 6
5. Aritmetica dei puntatori ............................................................................................................ 6
6. Conversioni forzate di tipo ........................................................................................................ 8
7. Legami tra vettori e puntatori.................................................................................................... 9
8. Vettori di puntatori .................................................................................................................. 13
9. Passaggio di vettori a funzioni ................................................................................................ 15
10. Puntatori e matrici ............................................................................................................... 15
11. Passaggio di matrici a funzioni ........................................................................................... 16
12. Esercizi ................................................................................................................................ 16
12.1 Esercizi sulla dichiarazione di puntatori ............................................................................ 16
12.2 Esercizi sulla dichiarazione di puntatori a puntatori .......................................................... 17
12.3 Esercizi sull’uso dei puntatori ............................................................................................ 18
12.4 Esercizi sull’aritmetica dei puntatori ................................................................................. 19
12.5 Esercizi sulle conversioni forzate ...................................................................................... 20
12.6 Esercizi sui legami tra vettori e puntatori .......................................................................... 20
12.7 Esercizi sui vettori di puntatori .......................................................................................... 20
1
1. Valore e Indirizzo di una variabile
La memoria centrale (la cosiddetta “RAM”), è
una lunga sequenza di byte, come fosse un
gigantesco vettore di byte. Se il calcolatore ha
4GB di memoria RAM, significa che possiede
4294967296 byte di memoria (4x10243); essi
andranno dalla posizione 0 alla posizione
4294967295 (vedi figura 1 a lato).
Il contenuto del byte è il suo valore.
La posizione del byte è il suo indirizzo.
Quando il programmatore crea un programma e lo manda
in esecuzione, il Sistema Operativo del Computer assegna
a tale programma un blocco della memoria RAM. Tale
blocco è generalmente suddiviso in tre sezioni:
0
1
2
3
4
…
…
2000
2001
2002
2003
…
3999
4000
…
…
…
Contenuto byte 0
Contenuto byte 1
Contenuto byte 2
Contenuto byte 3
Contenuto byte 4
…
…
Cont. byte 2000
Cont. byte 2001
Cont. byte 2002
Cont. byte 2003
Cont. byte 3999
Cont. byte 4000
…
…
 segmento Dati (Data Segment)
…
 segmento di Codice (Code Segment)
4294967295
…
 segmento di Stack (Stack segment)
Figura 1
Nel primo segmento (Dati) viene ricavato lo spazio per le variabili globali del programma, mentre
nel terzo (Stack) viene ricavato lo spazio per le variabili locali, cioè quelle dichiarate all’interno
delle funzioni, compreso il main.
…
…
Usando una terminologia più tecnica, possiamo dire che le
2000
variabili globali vengono allocate nel segmento Dati mentre
2001
quelle locali vengono allocate nello Stack.
2002
2003
Supponiamo, ad esempio, che nel programma siano state
2004
dichiarate le seguenti variabili globali:
2005
int A=15, B=-10; char CH1=’B’; float X=1.41, Y=3.14;
2006
2007
e che il Sistema Operativo abbia assegnato il segmento Dati che
2008
‘B’
va dal byte in posizione 2000
2009
a quello in posizione 3999. Possiamo immaginare che le
2010
variabili siano state allocate una di seguito all’altra (anche se ciò
2011
è vero solo per i vettori), a partire dal byte di indirizzo 2000.
2012
Tenendo conto del fatto che le variabili int occupano 4 byte
2013
ciascuna, come le variabili float, mentre le char occupano 1
2014
byte, avremmo che la variabile A occupa i byte dal nr. 2000 al
2015
nr. 2003, la variabile B occupa i byte dal nr. 2004 al nr. 2007, la
2016
variabile CH1 occupa il byte nr. 2008 e così via. La situazione è
…
…
illustrata in figura 2.
Figura 2
15
-10
1.41
3.14
Si definisce indirizzo di una variabile la posizione del primo byte (ossia quello meno significativo)
occupato dalla variabile stessa.
Con riferimento alla figura 2 possiamo dunque dire che:
2





l’indirizzo della variabile A è 2000; il suo valore è 15;
l’indirizzo della variabile B è 2004; il suo valore è -10;
l’indirizzo della variabile CH1 è 2008; il suo valore è 66, cioè il codice ASCII di ‘B’
l’indirizzo della variabile X è 2009; il suo valore è 1.41;
l’indirizzo della variabile A è 2013; il suo valore è 3.14;
Si ricorda che i numeri sono salvati nelle variabili int in formato complemento a 2.
Sapendo che 15 in complemento a 2 su 32 bit si scrive: 00000000000000000000000000001111
mentre -10 è 11111111111111111111111111110110, ne consegue che il contenuto specifico dei 4
byte (32 bit) occupati da A è quindi il seguente:
00000000
2003
00000000
2002
00000000
2001
00001111
2000
11111111
2006
11111111
2005
11110110
2004
Analogamente, per la variabile B:
11111111
2007
Naturalmente un discorso analogo potrebbe essere fatto per X e Y, ricordando il formato virgola
mobile.
2. Dichiarazione di puntatori
I puntatori sono variabili che contengono l’indirizzo di un’altra variabile; quest’ultima variabile si
dice “variabile puntata” dal puntatore.
Esiste un tipo distinto di puntatore per ciascun tipo di variabile puntata.
Per dichiarare una variabile puntatore si usa l’operatore asterisco: “ * “. Esempi:
int * p1; int * p2;
float * pf1; float * pf2;
char * pc;
double * pd;
Quindi, la sintassi generale per dichiarare un puntatore è la seguente:
tipoPuntato * nome Puntatore;
dove tipoPuntato è il tipo di una qualsiasi variabile.
L’asterisco diventa parte integrante del tipo del puntatore. Ad esempio, p1 sarà di tipo int*, pf1 e
pf2 di tipo float* e così via. I puntatori sono quindi dei nuovi tipi variabili.
NOTA: Se vogliamo dichiarare più puntatori dello stesso tipo, le dichiarazioni si possono
raggruppare, ma l’asterisco va ripetuto davanti a ciascun puntatore.
Esempio:
float *pf1, *pf2, *pf3;
Infatti, se scrivessimo:
float *pf1, pf2, pf3;
3
il compilatore interpreterebbe solo pf1 come puntatore mentre pf2 e pf3 sarebbero interpretate come
semplici variabili float.
Ad una variabile puntatore si può assegnare solo l’indirizzo di una variabile dello stesso tipo
di quelle puntate dal puntatore, oppure il valore di una altro puntatore dello stesso tipo.
Ad esempio, ad un puntatore di tipo int* si può assegnare solo l’indirizzo di una variabile int,
oppure il valore di un altro puntatore di tipo int*; ad un puntatore char * si può assegnare solo
l’indirizzo di una variabile char, oppure il valore di un altro puntatore di tipo char*, e così via.
Questa regola generale può essere violata solo applicando delle conversioni forzate di tipo (vedere
paragrafo più avanti).
Per catturare l’indirizzo di una variabile, anziché il suo valore, si usa l’operatore “&” anteposto
alla variabile. Per comprendere meglio, si traduca mentalmente l’operatore “&” come: “indirizzo
di…”. Esempio: con riferimento alle stesse variabili dichiarate nel paragrafo 1:
int A=15, B=-10; char CH1=’B’; float X=1.41, Y=3.14;
Possiamo scrivere:
p1 = &A; // carico in p1 l’indirizzo di A ; A è di tipo int, p1 è di tipo int* quindi
// l’istruzione è corretta; dopo tale istruzione si può dire che: “p1 punta a N”
pf1 = &X; // carico in pf1 l’indirizzo di X ; X è di tipi float, pf1 è di tipo float * quindi
// l’istruzione è corretta; dopo tale istruzione si può dire che: “pf1 punta a X”
p2 = &B; // carico in p2 l’indirizzo di B; B è di tipo int, p2 è di tipo int*
// dopo tale istruzione si può dire che: “p2 punta a B”
pf2 = pf1; // carico in pf2 il valore di pf1; pf1 è di tipi float*, cioè dello stesso tipo di pf1,
// quindi l’istruzione è corretta; dopo tale istruzione anche pf2 punta a X
Quindi il valore di p1 è 2000, quello di p2 è 2004, quello di pf1 è 2009, come quello di pf2.
Le seguenti istruzioni sono invece scorrette:
p1 = &X; // l’istruzione è scorretta perché X non è di tipo int
p2 = 150; // l’istruzione è scorretta perché 150 è una costante intera (= int), non un indirizzo
p1 = &CH1; // l’istruzione è scorretta perché CH1 non è di tipo int
pf1 = p1; // l’istruzione è scorretta perché p1 non è di tipo float*
Si può rappresentare graficamente l’effetto delle istruzioni “ p1 = &A; ” , “ pf1 = &X; ” , “ p2 =
p1; ” e “ pf2 = pf1; ” nel modo seguente:
p1
15
A
P2
-10
B
pf1
1.41
X
pf2
4
Naturalmente anche i puntatori, come tutte le variabili, si possono già inizializzare all’atto della
dichiarazione. Ad esempio, si poteva scrivere:
int A=15, B=-10, *p1=&A, *p2=&B;
char CH1=’B’, *pc;
float X=1.41, Y=3.14, *pf1=&X, *pf2=pf1;
Nulla impedisce, poi, di cambiare il valore di un puntatore durante il programma.
Esercizi sulla dichiarazione di puntatori
3. Puntatori a puntatori
Come evidenziato sopra, anche i puntatori sono delle variabili, quindi sono anch’essi caratterizzati
da un valore e da un indirizzo. Ha senso, allora, dichiarare puntatori a puntatori, cioè variabili che
contengono l’indirizzo di un altro puntatore.
Ad esempio, con riferimento a quanto detto sopra, sapendo che p1 è una variabile (puntatore) di tipo
int*, e che la sintassi per dichiarare un puntatore è:
tipoPuntato * nome Puntatore;
è possibile dichiarare un puntatore di nome punt_p1 che punti a p1 nel seguente modo:
int * * punt_p1;
In base alle stesse considerazioni fatte sopra, possiamo dire che punt_p1 è una variabile di tipo
int**.
A punt_p1 può quindi essere assegnato l’indirizzo di p1, ma anche quello di p2, oppure il valore di
un altro puntatore di tipo int**:
punt_p1 = &p1; // carico in punt_p1 l’indirizzo di p1; punt_p1 punta ora a p1
punt_p1 = &p2; // carico in punt_p1 l’indirizzo di p2; punt_p1 punta ora a p2
La cosa si può ovviamente generalizzare. Ad esempio, potremmo dichiarare e inizializzare un
puntatore, di nome punt_punt_p1, che punti a punt_p1, nel seguente modo:
int * * * punt_punt_p1=&punt_p1;
punt_punt_p1 è una variabile di tipo int*** e ad essa può essere assegnato l’indirizzo di punt_p1, ,
oppure il valore di un altro puntatore di tipo int***:
Naturalmente le seguenti istruzioni sono scorrette:
punt_punt_p1=&A; // scorretta, perché A è di tipo int e NON di tipo int**
punt_punt_p1=&p1; // scorretta, perché p1 è di tipo int* e NON di tipo int**
Esercizi sulla dichiarazione di puntatori a puntatori
5
4. Uso dei puntatori
I puntatori rappresentano un modo alternativo e molto potente per manipolare le variabili puntate.
Per modificare il contenuto della variabile puntata si usa l’operatore “*” anteposto al puntatore.
Per comprendere meglio, si traduca mentalmente l’operatore “*” come: “quello puntato da…”.
Esempi.
p1 = &A;
*p1 = 99;
// p1 viene fatto puntare a A, cioè il valore di p1 diventa 2000
// carica il valore 99 in “quello puntato da p1” cioè in A;
// tale istruzione equivale dunque a: A=99; p1 contiene ancora 2000
punt_p1 = &p1;
// punt_p1 viene fatto puntare a p1;
// cioè il valore di punt_p1 diventa 2017, che è l’indirizzo di p1
**punt_p1 = 555;
// carica il valore 555 in A, sovrascrivendo 99;
// tale istruzione equivale dunque a: A=555;
*punt_p1 = &B;
// carica il valore 2004 (cioè l’indirizzo di B) in p1,
// sovrascrivendo 2000; tale istruzione equivale dunque a: p1=&B;
// dopo tale istruzione p1 punta a B
Dal momento che un puntatore può essere fatto puntare a “qualsiasi” area di memoria, attraverso i
puntatori possiamo, almeno in teoria, alterare il contenuto di “qualsiasi” area della RAM (coi limiti
imposti dal Sistema Operativo).
Naturalmente i puntatori possono essere usati per riferirsi indirettamente alle variabili puntate in
espressioni aritmetiche complesse. Ad esempio, se consideriamo le solite dichiarazioni:
int A=15, B=-10, *p1=&A, *p2=&B;
char CH1=’B’, *pc;
float X=1.41, Y=3.14, *pf1=&X, *pf2=pf1;
potremo scrivere istruzioni come le seguenti:
B*=*p1+*p2;
*p1=(A-12)**p1;
// dopo tale istruzione, B vale -50; infatti, (*p1) è A, (*p2) è B;
// quindi è come se avessimo scritto B*=A+B;
// dopo tale istruzione, A vale 45; infatti, (*p1) è A;
// quindi è come se avessimo scritto A=(A-12)*A;
Esercizi sull'uso dei puntatori
5. Aritmetica dei puntatori
Il valore di un puntatore può essere incrementato o decrementato di quantità intere (1, 2, 3, …).
Cioè posso aumentare / diminuire l’indirizzo contenuto nella variabile puntatore. Ad esempio.
p1 = p1 + 1 ;
pf1 = pf1 - 1 ;
// incremento p1 di 1
// decremento pf1 di 1
Le istruzioni precedenti si possono anche scrivere nei seguenti modi:
6
p1 ++ ;
pf1 – – ;
oppure
oppure
p1+=1;
pf1–=1;
Se incremento un puntatore di 1, in realtà l’indirizzo interno aumenta non di una unità ma di tante
unità quanti sono i byte occupati dal tipo di variabile puntata. Ad esempio, con l’istruzione
“p1++;” l’indirizzo contenuto in p1 aumenta di 4 unità poiché il tipo int occupa 4 byte (o 2 unità in
quelle macchine ove il tipo int prevede 2 byte), mentre con l’istruzione “pf1 – – ; ” l’indirizzo
contenuto in pf1 diminuisce di 4 poiché il tipo float occupa 4 byte; con l’istruzione “pf1 = pf1 - 3 ;
” l’indirizzo contenuto in pf1 diminuirebbe di 12 unità.
Questo comportamento apparentemente strano in realtà fa sì che, incrementando (decrementando)
un puntatore di 1, questo punti alla variabile successiva (precedente) nella RAM.
In generale, considerando un generico puntatore P, è sempre lecito scrivere istruzioni del tipo:
P = P + N; oppure P = P – N ; con N variabile di tipo int.
E’ anche possibile scrivere: P1 = P2 + N; oppure
purché P1 e P2 siano dello stesso tipo.
P1 = P2 – N ;
Altra operazione ammessa è la sottrazione tra puntatori, purché siano dello stesso tipo. Il
risultato, tuttavia NON è un puntatore bensì un tipo int. Sono quindi sbagliate le seguenti istruzioni:
p1= p2-p1; // pur essendo corretta la sottrazione p2-p1, è sbagliato l’assegnamento a p1
B= p1-pf1; // sbagliata perché p1 è di tipo int* mentre pf1 è float *
Risulta invece corretto scrivere:
B = p2-p1;
Dopo questa istruzione, se p1 vale 2000 e p2 vale 2004 (vedi paragrafi precedenti), il valore
caricato in B risulta 1 e NON 4. In altre parole, la differenza tra due puntatori NON è la semplice
differenza tra i valori numerici degli indirizzi, bensì il numero di variabili di distanza.
Ciò non deve sorprendere, perché è coerente con l’incremento dei puntatori, che è l’operazione
opposta.
L’istruzione:
B = p1-p2;
darebbe in B il risultato -1.
Non sono ammesse altre operazioni aritmetiche coi puntatori; quindi non ha senso fare la somma
tra puntatori, la moltiplicazione tra puntatori, la divisione tra puntatori, la moltiplicazione tra un
puntatore e una costante o variabile, la divisione tra un puntatore e una costante o variabile.
Sono ad esempio sbagliate tutte le seguenti istruzioni:
p2=p1+p2; // è illecita la somma tra due puntatori
pf1=pf1*pf2; // è illecito il prodotto tra due puntatori
X=p1/10;
// è illecita la divisione che coinvolge un puntatore
p1=(p2+1)*A; // p2+1 è lecita e dà come risultato un int*, che non può però
// essere moltiplicato per nessuna variabile
A=p1/pf2;
// è illecita la divisione tra puntatori
Invece, sono corrette, ad esempio, le seguenti istruzioni:
X = *p1*100;
// se p1 punta ad A, tale istruzione è equivalente a: X = A*10;
7
*p1*=*p1**p2;
Y = 65 / (*pf1*2);
// se p1 punta ad A, e p2 a B tale istruzione è equivalente a: A *= A*B;
// se pf1 punta ad X, tale istruzione è equivalente a: Y = 65 / (X*2);
Esercizi sull'aritmetica dei puntatori
6. Conversioni forzate di tipo
Il C/C++ è un linguaggio fortemente tipizzato, ossia opera un controllo molto rigido dei tipi di dato.
In generale, quindi, non è possibile assegnare un certo dato di tipo xxx ad una variabile che non sia
di tipo xxx.
In altre parole, se var1 è una variabile di tipo1 e var2 è di tipo2, non sono ammesse, in generale,
queste assegnazioni:
var1 = var2;
var2 = var1;
Queste assegnazioni sono accettate dal compilatore se tipo1 e tipo2 sono tipi numerici (char, int,
float, …); in tal caso, infatti, scatta una conversione automatica (detta “conversione implicita”) di
tipo. Per tutti gli altri tipi di dati e per i puntatori in particolare non scatta alcuna conversione
automatica.
E’ tuttavia possibile forzare il sistema ad operare una conversione (“conversione forzata di tipo” o
“casting”). Vi sono due sintassi che si possono utilizzare, per forzare l’istruzione var1 = var2; esse
sono:
var1 = (tipo1)var2;
oppure
var1 = tipo1(var2);
Analogamente, per forzare l’istruzione: var2 = var1; si può scrivere:
var2 = (tipo2)var1;
oppure
var2 = tipo2(var1);
Solitamente coi puntatori si usa la prima sintassi. Utilizzando le conversioni forzate, diventano
lecite anche istruzioni che non lo erano.
Ad esempio, le seguenti istruzioni illecite (vedi paragrafi precedenti):
p1 = &X;
p2 = 150;
p1 = &CH1;
pf1 = p1;
punt_punt_p1=&A;
punt_punt_p1=&p1;
diventano lecite se riscritte nel modo seguente:
p1 = (int*)&X;
p2 = (int*)150;
p1 = (int*)&CH1;
pf1 = (float*)p1;
punt_punt_p1=(int***)&A;
punt_punt_p1=(int***)&p1;
E’ importante osservare che le conversioni forzate NON cambiano la natura dei puntatori; ad
esempio, l’istruzione: pf1 = (float*)p1;
non cambia la natura di p1, che rimarrà sempre un
puntatore di tipo int*, né quella di pf1, che rimarrà di tipo float*. Più precisamente, se p1 punta ad
A, l’istruzione: pf1 = (float*)p1; farà puntare anche pf1 ad A; tuttavia pf1 “crederà” di vedere in
8
A una variabile di tipo float, quindi interpreterà i 32 bit di A come fossero i 32 bit in virgola mobile
di una variabile float, fornendo un valore completamente astruso.
I seguenti esempi possono essere ancora più illuminanti.
Esempio 1
Si consideri l’istruzione: p1 = (int*)&CH1;
con cui abbiamo forzato p1 a puntare a CH1; la successiva istruzione: *p1 = 0; non azzera
solamente CH1 ma anche i primi tre byte di X ! Infatti, p1 contiene 2008 (vedi paragrafo1), che è
l’indirizzo di CH1, ma “crede” di vedere una variabile int, che occupa i byte dal 2008 al 2011;
quindi viene assegnato 0 a tutta questa ipotetica variabile.
Esempio 2
Date le dichiarazioni:
int A=800, … ;
char CH1=’B’, *pc;
Si consideri l’istruzione: pc = (char*)&A;
con cui abbiamo forzato pc a puntare ad A; se adesso scrivessimo *pc = CH1; e andassimo a
stampare A, con l’istruzione printf (“%d”, A); ci aspetteremmo di vedere il valore 66, che è il
contenuto della variabile CH1 (si ricordi che 66 è il codice ASCII della lettera ‘B’); invece, con
nostra grande sorpresa vediamo che viene stampato il valore 834.
Il motivo è molto semplice. Il puntatore pc è pur sempre un puntatore a variabili char; pc contiene
2000, che è l’indirizzo di A (vedi paragrafo1), ma “crede” di vedere al posto di A una variabile
char, che occupa il solo byte 2000; quindi il valore 66 va a sovrascrivere solo il primo byte di A
(quello meno significativo).
Di conseguenza, il contenuto di A (formato complemento a 2 sui 32 bit), che inizialmente era:
00000000 00000000 00000011 00100000
Dopo l’istruzione *pc = CH1;
(corrispondente al valore 800)
valore 66
diventa:
00000000 00000000 00000011 01000010
(corrispondente al valore 834)
In definitiva, le conversioni forzate vanno usate dal programmatore solo in casi veramente
particolari e con grande consapevolezza delle conseguenze.
Esercizi sulle conversioni forzate
7. Legami tra vettori e puntatori
In C (C++) esiste uno stretto legame tra vettori e puntatori, tanto che si può affermare che sono la
stessa cosa. Infatti il nome di un vettore è in sé un puntatore al primo elemento del vettore.
Ad esempio, dato il vettore di 100 elementi interi:
int Vet [100];
si ha che Vet è un puntatore a Vet [0]. Potrei quindi modificare Vet [0] in due modi equivalenti:
1. con la sintassi classica dei vettori: Vet [0] = 15;
9
2. con la sintassi classica dei puntatori: *Vet = 15;
L’identità tra vettori e puntatori non finisce qui. Infatti un qualunque puntatore si può utilizzare
come se fosse il nome di un vettore che inizia dalla variabile puntata dal puntatore stesso.
Si considerino le seguenti istruzioni.
int * pi1, * pi2 ;
int Vet [100]={50, 95, 63, 71, 84, 68, 22, 13, 3, 49}; // ho inizializzato solo i primi 10
// elementi, gli altri vengono posti a 0
pi1 = &Vet [0];
// pi1 viene fatto puntare a Vet [0], cioè al primo elemento del vettore
// tale istruzione è equivalente a: pi1 = Vet ;
pi2 = &Vet [2];
// pi2 viene fatto puntare a Vet [2], cioè al terzo elemento del vettore
// tale istruzione è equivalente a: pi2 = Vet + 2;
L’istruzione precedente determina la situazione rappresentata nella seguente figura:
pi1
50
Vet [0]
44
Vet [1]
37
Vet [2]
29
pi2
84
…
…
Vet [99]
Potrei ora modificare gli elementi del vettore usando i puntatori pi1 e pi2 come se fossero nomi di
vettori tenendo però presente che per pi1 il vettore inizia da Vet [0], mentre per pi2 il vettore inizia
da Vet[2]. Esempi.
pi1 [1] = 44; // scrive 44 in Vet [1]
pi2 [0] = 37; // scrive 37 in Vet [2]
pi2 [1] = 29; // scrive 29 in Vet [3]
L’unica differenza importante tra il nome di un vettore e una variabile puntatore è che il nome di
un vettore è un puntatore costante, cioè NON può essere variato. Ad esempio, non è lecita
l’istruzione:
Vet = Vet + 5;
mentre è lecita la:
pi1 = pi1 + 3 ;
I puntatori sono molto spesso usati in combinazione coi vettori. Infatti, le variabili di un vettore
sono sempre allocate (cioè disposte) in memoria in modo consecutivo. Ad esempio, con la
dichiarazione:
int Vet [100];
vengono riservati in memoria 400 byte consecutivi per le 100 variabili di Vet. Se la prima variabile
Vet[0] occupasse i byte da 2500 a 2503, allora potremmo affermare che Vet[1] occupa i byte da
2504 a 2507, Vet[2] occupa i byte da 2508 a 2511, e così via, fino a Vet[99], che occupa i byte da
2896 a 2899.
10
Se pi1 è un puntatore che punta a Vet[0], ricordando quanto detto sull’aritmetica dei puntatori (vedi
paragrafo 5), con l’istruzione pi1++; pi1 andrà a puntare alla variabile successiva ( Vet[1] ) ; in
generale, con l’istruzione pi1+= n; pi1 andrà a puntare alla variabile che si trova n posti più
avanti rispetto a quella a cui stava puntando; analogamente, con l’istruzione pi1= n; pi1 andrà a
puntare alla variabile che si trova n posti più indietro rispetto a quella a cui stava puntando.
A tal proposito, date le dichiarazioni:
int Vet[100], *pi1=Vet;
si noti la differenza nei due seguenti casi.
Caso 1:
Caso 2:
pi1+=5; *pi1=20;
*(pi1+5)=20;
// assegno 20 a Vet[5]
// assegno 20 a Vet[5]
Nel primo caso modifico pi1 e lo faccio puntare a Vet[5]; dopodiché assegno 20 alla variabile da
esso puntata, cioè V[5].
Nel secondo assegno 20 a Vet[5] senza modificare pi1, che rimane a puntare a Vet[0]. Infatti pi1+5
equivale ad un ipotetico puntatore Px che punta a Vet[5]; l’istruzione *(pi1+5)=20; diventa
quindi *Px=20; ossia assegna 20 all’oggetto puntato da Px.
Nel secondo caso potrei usare anche Vet al posto di pi1, ossia scrivere *(Vet+5)=20;
Non potrei invece usare Vet al posto di pi1 nel primo caso poiché Vet – come abbiamo detto – è
immodificabile.
In base a quanto detto sopra, possiamo concludere che:
scrivere Vet [ n ] è equivalente a scrivere * ( Vet + n ) , qualunque sia il puntatore Vet.
Possiamo anche affermare che :
scrivere ( P + N ) [ M ]
è equivalente a scrivere P [ N+M ] , qualunque sia il puntatore P.
In definitiva, i puntatori sono comodi per “muoversi” lungo il vettore, andando a puntare via via
variabili diverse.
Attenzione: nel “muoversi” lungo il vettore NON c’è alcun controllo sullo “sforamento”; in altre
parole, è responsabilità del programmatore fare attenzione a non uscire dai limiti del vettore. Ad
esempio, se pi1 punta a Vet[0], con l’istruzione pi1 ; pi1 andrà a puntare alla (ipotetica)
variabile che occupa i byte di memoria precedenti a quelli occupati da Vet[0].
Come per i vettori, anche le variabili delle matrici sono allocate in modo consecutivo. Ad esempio,
data la dichiarazione:
float Mat[3][4];
le variabili sono disposte in memoria nel seguente ordine:
Mat[0][0], Mat[0][1], Mat[0][2], Mat[0][3], Mat[1][0], Mat[1][1], Mat[1][2], … , Mat[2][3].
Sfruttando questo fatto, la matrice Mat potrebbe essere eventualmente gestita come se fosse un
vettore di 12 elementi. Infatti, con la dichiarazione:
float * pf3 = &Mat[0][0];
11
il puntatore pf3, che punta al primo elemento della matrice, può essere usato come fosse il nome di
un vettore; di conseguenza:
pf3[0] corrisponde a Mat[0][0] ,
pf3[1] corrisponde a Mat[0][1] ,
pf3[2] corrisponde a Mat[0][2] ,
pf3[3] corrisponde a Mat[0][3] ,
pf3[4] corrisponde a Mat[1][0] ,
pf3[5] corrisponde a Mat[1][1] ,
pf3[6] corrisponde a Mat[1][2] ,
…
pf3[11] corrisponde a Mat[2][3] .
Quando ci si muove lungo un vettore tramite puntatori, si usano spesso gli operatori ++ e   . Si
ricordi che tali operatori si possono applicare in modo pre-fisso oppure in modo post-fisso. Se
vengono utilizzati in un’istruzione di assegnamento, l’effetto dell’una o dell’altra modalità è
diverso, come illustrato dai seguenti esempi.
Esempio 1
Siano date le seguenti dichiarazioni:
int * pi1, * pi2 ;
int Vet [100] ={50, 95, 63, 71, 84, 68, 22, 13, 3, 49};
e le seguenti e istruzioni:
pi1 = Vet ;
// pi1 punta a Vet [0]
pi2 = Vet +5; // pi1 punta a Vet [5]
Vet[6]=*(++pi1)+*(pi2);
Dopo tali istruzioni, Vet[6] passa da 22 a 163, pi1 punta a Vet[1], mentre pi2 punta a Vet[4]. Infatti,
in base al funzionamento degli operatori ++ e  , l’istruzione Vet[6]=*(++pi1)+*(pi2);
è equivalente alla seguente sequenza:
++pi1;
Vet[6]=*pi1+*pi2;
pi2 ;
// pi1 va a puntare Vet[1]
// Vet[6] = 95+68
// pi2 va a puntare Vet[4]
Esempio 2
Siano date le seguenti dichiarazioni:
int Vet [100] ={2, 4, 6, 8, 10, 12, 14, 16, 18, 20}, * pi1=Vet+2, * pi2=Vet+6 ;
e la seguente istruzione:
*(++pi1)=*(pi2) + *(pi1++);
Dopo tale istruzione, Vet[3] passa da 8 a 20, pi1 punta a Vet[4], mentre pi2 punta a Vet[5]. Infatti,
questa l’istruzione è equivalente alla seguente sequenza:
++pi1;
// pi1 va a puntare Vet[3]
12
 pi2;
*pi1=*pi2+*pi1;
pi1++;
// pi1 va a puntare Vet[5]
// Vet[3] = 12+8
// pi1 va a puntare Vet[4]
Esercizi sui legami tra vettori e puntatori
8. Vettori di puntatori
I puntatori sono variabili a tutti gli effetti, perciò è possibile dichiarare vettori di puntatori, cioè
vettori i cui elementi siano puntatori; ovviamente devono essere tutti puntatori dello stesso tipo, ad
esempio tutti di tipo int*, oppure tutti di tipo float* ecc…
Ad esempio, la seguente dichiarazione: int* VP[5]; definisce un vettore di 5 variabili, tutte
puntatori di tipo int*; ciascuno di questi puntatori può puntare ad una qualsiasi variabile di tipo int,
anche appartenente ad un vettore di interi. A titolo esemplificativo, si consideri la seguente
situazione:
int A=15, B=25, Vet[100]={2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22};
int* VP[5];
VP[0] = &A;
// il primo puntatore punta a A
VP[1] = &B;
// il secondo puntatore punta a B
VP[2] = &Vet[1]; // il terzo puntatore punta a Vet[1];
// si poteva anche scrivere VP[0] = Vet+1;
VP[3] = &Vet[5]; // il quarto puntatore punta a Vet[5];
// si poteva anche scrivere VP[0] = Vet+5;
VP[4] = &Vet[8]; // il quinto puntatore punta a Vet[8];
// si poteva anche scrivere VP[0] = Vet+8;
L’assegnamento degli indirizzi ai puntatori si può fare anche in fase di dichiarazione, nel seguente
modo:
int* VP[5]={&A, &B, Vet+1, Vet+5, Vet+8};
La situazione dei puntamenti è schematizzata nella figura seguente:
VP[0]
VP[1]
VP[2]
VP[3]
VP[4]
VP
15
25
A
B
Vet
2
4
6
8
10
12
14
16
18
20
22
0
…
0
Vet[0]
Vet[1]
Vet[2]
Vet[3]
Vet[4]
Vet[5]
Vet[6]
Vet[7]
Vet[8]
Vet[9]
Vet[10]
Vet[11]
…
Vet[99]
Nella figura è stato messo in evidenza anche il fatto che VP, essendo il nome di un vettore è
automaticamente un puntatore al primo elemento VP[0], analogamente a Vet, che è un puntatore a
Vet[0] (vedi paragrafo 7). Essendo VP[0] un puntatore di tipo int*, ne consegue che VP è un
puntatore a puntatore, quindi VP è un puntatore di tipo int**.
In questa situazione, si vengono a creare due livelli di puntamento:
 primo livello: da VP possiamo scendere negli elementi del vettore VP, usando le parentesi []
oppure l’operatore *; ad esempio, con VP[3] o *(VP+3) accedo al quarto elemento di VP.
13
 secondo livello: dagli elementi di VP possiamo scendere nelle variabili A e B o negli
elementi del vettore Vet, usando le parentesi [ ] oppure l’operatore *; ad esempio, con
*(VP[3]) o (VP[3])[0] accedo a Vet[5].
Si noti che l’applicazione delle parentesi [] o dell’operatore * fa scendere di un livello. Di
conseguenza, per accedere agli elementi di Vet (o alle variabili A e B) a partire da VP devo
applicare due volte l’operatore * oppure due volte le parentesi [] oppure un operatore * e una
parentesi [].
Esempi
**VP=1;
Tale istruzione assegna 1 a A; infatti, immaginiamo per un attimo di sostituire *VP con Px;
l’istruzione diventa quindi *Px=1; ma chi è Px? Px è *VP, quindi è l’oggetto puntato da VP,
ossia VP[0]; ma VP[0] è a sua volta un puntatore che punta ad A; quindi Px punta ad A; in
definitiva, l’istruzione *Px=1; assegna 1 ad A.
VP[2][3]=2;
Tale istruzione assegna 2 a Vet[4]; infatti, immaginiamo per un attimo di sostituire VP[2] con
Px; l’istruzione diventa quindi Px[3]=2; ma chi è Px in questo caso? Px è VP[2], quindi è il terzo
puntatore di VP; VP[2] punta a Vet[1]; quindi Px punta a Vet[1]; in definitiva, l’istruzione
Px[3]=2; assegna 2 alla variabile 3 posti più avanti rispetto a quella puntata da Px.
(*(VP+3))[6]=3;
Tale istruzione assegna 3 a Vet[11]; infatti, immaginiamo per un attimo di sostituire *(VP+3)
con Px; l’istruzione diventa quindi Px[6]=3; ma chi è Px in questo caso? Px è *(VP+3), ossia
VP[3] , quindi è il quarto puntatore di VP; VP[3] punta a Vet[5]; quindi Px punta a Vet[5]; in
definitiva, l’istruzione Px[6]=3; assegna 3 alla variabile 6 posti più avanti rispetto a quella
puntata da Px.
*(VP[4]+5)=4;
Tale istruzione assegna 4 a Vet[13]; infatti, immaginiamo per un attimo di sostituire VP[4] con
Px; l’istruzione diventa quindi *(Px+5)=4; ma chi è Px in questo caso? Px è VP[4], ossia il
quinto puntatore di VP; VP[4] punta a Vet[8]; quindi Px punta a Vet[8]; in definitiva,
l’istruzione *(Px+5)=4; assegna 4 alla variabile 5 posti più avanti rispetto a quella puntata da Px.
*(*(VP+3) 4)=5;
Tale istruzione assegna 5 a Vet[1]; infatti, immaginiamo per un attimo di sostituire *(VP+3) con
Px; l’istruzione diventa quindi *(Px4)=5; ma chi è Px in questo caso? Px è VP[3], ossia il
quarto puntatore di VP; VP[3] punta a Vet[5]; quindi Px punta a Vet[5]; in definitiva,
l’istruzione *(Px4)=5; assegna 5 alla variabile 4 posti più indietro rispetto a quella puntata da
Px.
( (VP+1)[3]+2) [4]=6;
Tale istruzione assegna 6 a Vet[14]; infatti (vedi paragrafo 7) (VP+1)[3] equivale a VP[4];
immaginiamo per un attimo di sostituire VP[4] con Px; l’istruzione diventa quindi (Px+2)[4]=6;
che equivale a Px[6]=6; ma chi è Px in questo caso? Px è VP[4], ossia il quinto puntatore di VP;
VP[4] punta a Vet[8]; quindi Px punta a Vet[8]; in definitiva, l’istruzione Px[6]=6; assegna 6
alla variabile 6 posti più avanti rispetto a quella puntata da Px.
14
9. Passaggio di vettori a funzioni
Data l’identità tra vettori e puntatori, un parametro formale di tipo vettore si può dichiarare anche
sotto forma di puntatore. Ad esempio le due seguenti dichiarazioni di funzione sono equivalenti.
void Fx( int Vet [ ] , int N ) { … }
void Fx( int * Vet , int N ) { … }
In effetti, all’interno della funzione posso usare Vet sia con la sintassi dei vettori sia con quella dei
puntatori, in entrambi i casi. Ad esempio è indifferente scrivere:
Vet [ i ] = xxx;
oppure:
*(Vet + i) = xxx;
10. Puntatori e matrici
Da quanto detto nei paragrafi 7 e 8, verrebbe da pensare che il nome di una matrice fosse un
puntatore a puntatore. Ad esempio, date le dichiarazioni:
int Mat[3][4], **pp1;
verrebbe spontaneo pensare che Mat fosse un puntatore di tipo int**, cioè dello stesso tipo di pp1.
Invece non è così; lo prova il fatto che la seguente assegnazione provoca un errore di compilazione:
pp1=Mat;
per essere dello stesso tipo di Mat, pp1 deve essere dichiarato nel seguente modo:
int (*pp1)[4];
Mat non è dunque un semplice puntatore a puntatore, bensì un puntatore a vettore di 4 elementi
interi (vedi figura); il suo tipo è int (*)[4] , quindi il numero di colonne della matrice fa parte del
tipo del puntatore.
Mat
Date le dichiarazioni:
int M1[15][8], M2[8][15], M3[8][8];
avremo che M1 ed M3 sono dello stesso tipo int (*)[8] , mentre M2 è di tipo diverso int (*)[15].
La cosa non deve sorprendere, poiché una matrice di R righe e C colonne non è altro che un vettore
di vettori, cioè un vettore di R elementi, ciascuno dei quali è a sua volta un vettore di C elementi.
M1 è dunque un puntatore alla prima riga della matrice.
15
La dichiarazione int (*pp1)[8]=M1; definisce un puntatore dello stesso tipo di M1 e lo fa puntare
ove punta M1, cioè alla prima riga della matrice M1. L’istruzione pp1++; fa avanzare pp1 di 32
byte, che è lo spazio occupato da una riga intera, e porta pp1 a puntare alla seconda riga.
11. Passaggio di matrici a funzioni
Per quanto detto nel paragrafo precedente, un parametro formale di tipo matrice si può dichiarare
anche sotto forma di puntatore. Ad esempio le due seguenti dichiarazioni di funzione sono
equivalenti.
void Fx( int Mat [ ][10] , int N ) { … }
void Fx( int (*Mat)[10] , int N ) { … }
Come appare evidente, mentre il numero di righe è ininfluente, il numero di colonne della matrice
va obbligatoriamente indicato, poiché fa parte del tipo. Ne consegue che a Fx posso passare solo
matrici che abbiano 10 colonne. Se devo passare, ad esempio, una matrice di 8 colonne, devo creare
un’altra funzione apposita.
12. Esercizi
12.1 Esercizi sulla dichiarazione di puntatori
Esercizio 12.1.1
Nel seguente frammento di programma, sono dichiarate alcune variabili e alcuni puntatori; vi sono
poi delle istruzioni che stampano, a titolo di esempio, il valore e l’indirizzo di C1. Si aggiungano le
istruzioni per stampare in modo analogo il valore e l’indirizzo delle variabili: C2, N, M, A e B.
Infine, si verifichi l’esito delle istruzioni al calcolatore.
NOTA: gli indirizzi sono numeri interi, quindi vanno stampati con lo specificatore “%d”.
char C1='5', C2= '5', *pc1=&C1, *pc2=&C2;
int N=10, *pi1=&N, M=30, *pi2=&M;
float A=1.5, B=9.9, *pf1=&A, *pf2=&B;
printf("\n%c",
printf("\n%d",
printf("\n%d",
printf("\n%d",
...
C1);
C1);
pc1);
&C1);
//
//
//
//
stampa
stampa
stampa
stampa
il valore di C1, come carattere (%c)
il valore di C1, come valore numerico (%d)
il valore di pc1, cioè l’indirizzo di C1
l’indirizzo di C1 (equivale alla precedente)
Esercizio 12.1.2
Assumendo che siano corrette le seguenti dichiarazioni:
char A='0', B= 'A', *C;
int D=1, E, *F, G, H, *I;
float *L, M=0, N, O, *P;
16
Determinare le istruzioni che generano errori di compilazione, tra quelle riportate di seguito, e si
verifichi l’esito al calcolatore.
a)
b)
c)
d)
e)
f)
g)
h)
N=A;
C=B;
F=D;
L=&G;
P=N;
I=&D;
C=&H;
F=I;
i)
j)
k)
l)
m)
n)
o)
p)
L=C;
F=&B;
C=&A;
P=&L;
I=&E;
B=N;
P=L;
F=E;
12.2 Esercizi sulla dichiarazione di puntatori a puntatori
Esercizio 12.2.1
Assumendo che siano corrette le seguenti dichiarazioni:
char A='0', B= 'A', *C, **D;
int **E, ***F, *G, H=2, I=0, L;
float M=7.5, N=10.0, *O, **P, Q, **R;
Determinare le istruzioni che generano errori di compilazione, tra quelle riportate di seguito, e si
verifichi l’esito al calcolatore.
a)
b)
c)
d)
e)
f)
g)
h)
E=&I;
O=&N;
F=&E;
P=E;
D=&&B;
P=&O;
G=&E;
L=&G;
i)
j)
k)
l)
m)
n)
o)
p)
D=&C;
R=P;
E=L;
B=Q;
O=&P;
E=&F;
G=&H;
E=&G;
Esercizio 12.2.2
Scrivere le dichiarazioni necessarie per creare la
situazione schematizzata in figura 12.2.2a, ove le
variabili A, B, C e D sono rispettivamente di tipo:
char, int, int, float.
-10
P5
P1
A
-10
P6
P2
B
-10
P9
P10
P7
P3
C
-10
P8
Figura 12.2.2a
17
P4
D
Scrivere poi le istruzioni per modificare i puntamenti
come schematizzato in figura 12.2.2b.
-10
P5
P1
A
-10
P6
P2
B
-10
P7
P9
P10
P3
C
-10
P8
P4
D
Figura 12.2.2b
Esercizio 12.2.3
Utilizzando lo stesso tipo di rappresentazione delle figura 12.2.2, si schematizzi la situazione
determinata dalle seguenti dichiarazioni e istruzioni. Si noti che alcune istruzioni modificano i
puntamenti definiti con le dichiarazioni.
char c1, *c2=&c1, c3, **c4=&c2, *c5, *c6;
int i1=0, i2, *i3, **i4, ***i5=&i4, ***i6=i5, **i7=i4;
float *f1, **f2, f3, ***f4=&f2, **f5=&f1, f6;
c5=c2;
f1=&f6;
f2=f5;
c5=&c3;
i4=&i3;
f5=f2;
c6=c5;
c5=c2;
c2=c6;
i3=&i2;
i5=&i7;
12.3 Esercizi sull’uso dei puntatori
Esercizio 12.3.1
Date le seguenti dichiarazioni:
char a, b, *c, **d;
int e, f, g, *h, *i, **l, **m, ***n;
float o, *p, *q, *r, **s, ***t;
Determinare le istruzioni che generano errori di compilazione, tra quelle riportate di seguito, e si
verifichi l’esito al calcolatore.
a)
b)
c)
d)
e)
f)
g)
h)
i=&h;
h=*l;
l=*n;
d=*c;
f=*q;
q=*s;
s=*t;
i=***n;
i)
j)
k)
l)
m)
n)
o)
p)
18
**t=*p;
***n=**l;
**d=*c;
m=*n;
**l=*i;
a=***t;
**l=**m;
d=**a;
Esercizio 12.3.2
Date le seguenti dichiarazioni:
int A, B, C, *pi1=&B, *pi2, *pi3=&A;
int **ppi1, **ppi2, ***pppi1=&ppi1;
float F, *pf1=&F;
fare la traccia dei seguenti frammenti di codice e dire cosa viene stampato a video. Infine, verificare
i risultati al calcolatore.
// FRAMMENTO 3
A=4; B=3, C=2, F=1;
pi1=pi3;
pi3=pi2;
**ppi2=**ppi1**pi1;
*pi1+=***pppi1;
B=**ppi2-*pf1;
*pf1+=***pppi1;
printf("\n%d %d %d %f", A,B,C,F);
// FRAMMENTO 1
A=10; B=20, C=30, F=2.0;
pi2=&C;
ppi1=&pi2;
ppi2=&pi3;
*pi1+=2;
*pi2=*pi2+*pi3;
*pi3=*pi1**pi2;
*pf1*=*pi1;
printf("\n%d %d %d %f", A,B,C,F);
// FRAMMENTO 4
A=5; B=10, C=15, F=20;
*ppi2=&B;
ppi2=*pppi1;
*pppi1=&pi1;
pppi1=&ppi2;
**ppi1-=3;
***pppi1+=7;
*pi3=*pi3**pf1;
F=**ppi1***ppi2;
printf("\n%d %d %d %f", A,B,C,F);
// FRAMMENTO 2
A=2; B=4, C=8, F=10;
*pi1=**ppi1*2;
**ppi1=**ppi2+50;
**ppi2=*pf1+3.5;
*pf1*=***pppi1;
printf("\n%d %d %d %f", A,B,C,F);
12.4 Esercizi sull’aritmetica dei puntatori
Esercizio 12.4.1
Assumendo che siano corrette le seguenti dichiarazioni:
char A='0', B= 'A', *C, **D;
int **E, **F, *G, H=2, I=0, *L;
float M=7.5, N=10.0, *O, **P, Q, **R;
Determinare le istruzioni che generano errori di compilazione, tra quelle riportate di seguito, e si
verifichi l’esito al calcolatore.
a)
b)
c)
d)
e)
f)
g)
G=G+H;
++P;
D=D*10;
P+=R;
C=&B-1;
B=P-R;
**E=**E+**F;
h)
i)
j)
k)
l)
m)
n)
19
L=G*H;
I=*E-*F;
Q=*O+**R;
*R=O+**P;
O=&M-&N;
C=*D+I;
H=&G-F;
12.5 Esercizi sulle conversioni forzate
. . . [ da completare ] . . .
12.6 Esercizi sui legami tra vettori e puntatori
. . . [ da completare ] . . .
12.7 Esercizi sui vettori di puntatori
Esercizio 12.7.1
a) Si scrivano le dichiarazioni e le istruzioni necessarie per creare la seguente situazione di
puntamenti, ove MAT è una matrice 6x6 di interi, V_PUNT è un vettore di puntatori e
P_PUNT è un puntatore a puntatore.
P_PUNT
V_PUNT
MAT
b) Determinare a quale cella della matrice vengono assegnati i valori seguenti.
1)
2)
3)
4)
5)
6)
7)
V_PUNT[2][7] = 1;
(*V_PUNT)[3] = 2;
*(V_PUNT[3] ) = 3;
**V_PUNT = 4;
(*(V_PUNT+4)) [10] = 5;
*(V_PUNT[5]–4) = 6;
*(*(V_PUNT+2)–11) = 7;
8)
9)
10)
11)
12)
**P_PUNT = 8;
P_PUNT[2][8] = 9;
*(P_PUNT[1]) = 10;
(*P_PUNT)[1] = 11;
(*(P_PUNT-2))[2] = 12;
20
13)
14)
*(P_PUNT[2]+6) = 13;
*(*(P_PUNT+1)-5) = 14;
15)
16)
17)
*((P_PUNT+1)[2]) = 15;
(*(P_PUNT+1))[2] = 16;
((P_PUNT-1)[4]-3)[7] = 17;
21
Download