Puntatori - Corso Base Android

Transcript

Puntatori - Corso Base Android
Puntatori
Maurizio Cozzetto
Aggiornamento del 12 maggio 2011
1
Introduzione
I puntatori costituiscono una delle più potenti caratteristiche del linguaggio C/C++ e consentono ai programmi di
manipolare le cosiddette strutture dati dinamiche, ossia quelle strutture che possono crescere e decrescere durante
l’esecuzione del programma, come le liste concatenate, le code, le pile ecc. I puntatori vanno maneggiati con cura
perchè costituiscono un’arma a doppio taglio. Infatti, pur essendo estremamente potenti, è facile incappare in errori di
programmazione (alcuni dei quali possono persino bloccare il sistema) perchè non sono previsti particolari controlli da parte
del compilatore: infatti la responsabilità di quanto avviene nel programma è delegata esclusivamente al programmatore.
2
Definizione di puntatore
Un puntatore è una variabile che contiene l’indirizzo di memoria di un’altra variabile di un certo tipo, cioè indica dove è
memorizzato il dato di un’altra variabile. Per esempio, dichiariamo x come variabile intera (in questo momento il valore
di x potrebbe essere casuale, magari anche 0, ma non è detto, dipende dal compilatore):
int x ;
Possiamo dichiarare un puntatore ad un intero in questo modo:
int * p ;
Leggiamo: p punta ad un intero. Il nome di una variabile fa direttamente riferimento a un valore (come nel caso della
variabile di nome x), mentre il puntatore fa riferimento a tale valore in maniera indiretta. Per indicare il valore “puntato”
da p (o collocato in memoria all’indirizzo contenuto in p), scriviamo *p. L’operatore unario * si chiama operatore di
deriferimento o operatore di indirezione.
I puntatori dovrebbero essere inizializzati all’atto della loro dichiarazione e ciò può essere fatto contestualmente
scrivendo:
int * p = NULL ;
NULL è una costante simbolica definita nel file di intestazione <stddef.h> (che viene incluso in molti altri file di
intestazione come ad esempio <stdio.h>). Di solito NULL vale 0 che è l’unico valore intero che può essere assegnato a
una variabile puntatore.
Possiamo dichiarare e assegnare a p l’indirizzo di memoria di x scrivendo (come visto prima, Fig. 2.1-2.2-2.3):
int * p =& x ;
oppure possiamo dichiarare prima la variabile puntatore p e poi assegniamo ad essa l’indirizzo di memoria di x:
int * p ;
p =& x ;
Figura 2.1: Puntatore ad un intero (sono rappresentati gli indirizzi di memoria)
1
Figura 2.2: Puntatore ad un intero (rappresentazione simbolica mediante una freccia)
Figura 2.3: Rappresentazione di un puntatore ulteriormente semplificata
L’operatore unario & si chiama invece operatore di indirizzo e fa riferimento all’indirizzo di memoria della variabile x
(nei processori a 32 bit gli indirizzi di memoria sono rappresentati come valori a 32 bit senza segno, nei processori a 64
bit come valori a 64 bit senza segno).
Gli operatori & e * sono in qualche modo uno l’inverso o il complemento dell’altro, come dimostra il seguente
frammento di codice:
# include < iostream >
# include < cstdlib >
using namespace std ;
int main () {
int x =9;
int * p =& x ;
// visualizza il valore 9
cout << " x = " << x << endl ;
// visualizza un indirizzo di memoria
cout << " p = " << p << endl ;
// visualizza il valore 9
cout << " *(& x )= " << *(& x ) << endl ;
// visualizza un indirizzo di memoria
cout << " &(* p )= " << &(* p ) << endl ;
}
return 0;
Risultato dell’esecuzione:
x =9
p =0 xbfb7ea0c
*(& x )=9
&(* p )=0 xbfb7ea0c
Possiamo visualizzare sul monitor il contenuto di memoria del puntatore p usando l’istruzione (linguaggio C):
printf ( " % p " ,p );
oppure più semplicemente (linguaggio C++):
cout << p ;
2
Il risultato è rappresentato in forma esadecimale (per esempio 0xa9b3ffcd).
E’ possibile anche assegnare ad un puntatore un valore costante (che quindi rappresenta un indirizzo di memoria) ma
è necessario un cast esplicito. Se si modifica il valore puntato da p come nel frammento di codice seguente, si potrebbe
ottenere un messaggio di errore (di solito Segmentation Fault), cioè il tentativo di accedere a delle aree di memoria
protette dal sistema.
int * p =( int *) 0 x8a234bcd ;
* p = 90;
3
Operatore sizeof
Possiamo ricavare direttamente la dimensione in byte di una variabile puntatore (o di un tipo) usando l’operatore sizeof():
cout << sizeof ( int ); // visualizza 2 o 4 , dipende dal compilatore
cout << sizeof ( p ); // visualizza 4 se il processore è a 32 bit
4
Legame tra puntatori e array
Esiste un legame strettissimo tra array e puntatori. Supponiamo di aver dichiarato e inizializzato un array di interi di
dimensione 5:
int v []={2 , -4 ,6 , -3 ,10};
Definiamo inoltre una variabile puntatore p a un intero e facciamola puntare al primo elemento dell’ array v:
int * p =& v [0];
Più semplicemente possiamo scrivere:
int * p = v ;
Il nome di un array in realtà è l’indirizzo di memoria del suo primo elemento. Quindi le due notazioni:
& v [0]
e
v
sono equivalenti.
Aritmetica dei puntatori
E’ possibile definire per i puntatori un insieme ristretto di operazioni aritmetiche, in particolare sono definite la somma di
un puntatore con un valore intero (positivo, negativo o nullo) che fornisce come risultato ancora un puntatore, l’incremento
di un puntatore e il decremento (che dà come risultato ancora un puntatore), la differenza tra due puntatori (che fornisce
un numero intero), il confronto tra puntatori (che dà un valore di verità vero o falso) e l’assegnazione. Queste operazioni
acquistano particolare importanza proprio in relazione agli array.
Somma e assegnamento
Sia v l’indirizzo di memoria del primo elemento di un array di interi (come nel caso dell’esempio precedente).
L’espressione:
v+i
dove i è un valore intero compreso tra 0 e n-1 (n è la dimensione dell’array), denota per definizione un nuovo puntatore
che punta all’elemento dell’array che occupa la posizione iesima. Cioè il suo indirizzo di memoria è così calcolato:
v + sizeof ( int )* i
Ne consegue che le due notazioni
*( v + i )
3
e
v[i]
sono equivalenti.
La notazione
p ++;
ha lo stesso significato di
p = p +1;
Quindi, se l’indirizzo di memoria di p è per esempio 4200 e p punta ad un intero, la notazione p+1 non fornisce come
risultato il valore 4201, come nell’aritmetica convenzionale, bensì il valore 4200 incrementato di 2 o 4 (cioè la dimensione
in byte del tipo base int), quindi 4202 o 4204.
Analogo significato ha la notazione p–.
Prestiamo però attenzione al fatto che non possiamo scrivere:
v ++; // Errore !
perchè v è definito come valore costante.
Sottrazione tra puntatori
Se a e b sono due puntatori che puntano a due elementi qualsiasi dello stesso array v:
int * a = v + i ;
int * b = v + j ;
con i e j ciascuno compreso tra 0 e n-1, definiamo la differenza tra i due puntatori a e b, e la indichiamo con
a-b
il numero
i-j
cioè la differenza tra gli indici i e j. Questo numero può essere positivo, negativo o nullo a seconda che sia rispettivamente
i>j, cioè quando v[i] segue v[j], i<j cioè quando v[i] precede v[j] o i=j cioè quando v[i] è uguale a v[j].
Confronto fra puntatori
Il risultato di un confronto tra due puntatori (un valore booleano) è la diretta conseguenza della differenza tra due
puntatori. Quindi la notazione
p<q
equivale a
p -q <0
p>q equivale a p-q>0 ecc. Possiamo confrontare inoltre tra di loro due puntatori p e q scrivendo
p == q
(due puntatori sono uguali se essi puntano alla stessa variabile). Sono lecite inoltre le seguenti espressioni:
p == NULL
o anche
p != NULL
Si possono usare anche gli operatori <= e >=.
4
Figura 4.1: Rappresentazione simbolica dello scorrimento in avanti degli elementi di un array
Scorrimento di un array in avanti
Possiamo scorrere in avanti tutti gli elementi dell’array v di lunghezza n prendendo in esame il primo elemento, poi il
successivo e così via, usando l’aritmetica dei puntatori, mediante un ciclo che incrementa un indice (un numero intero
positivo o nullo):
for ( int i =0; i < n ; i ++)
// visualizza l ' indice e l ' elemento generico v [ i ]
cout << i << " " << *( v + i ) << endl ;
oppure con un ciclo che incrementa invece un puntatore (Fig. 4.1):
for ( int * p = v ; p < v + n ; p ++)
// visualizza l ' indice e l ' elemento generico * p del array
cout << p - v << " " << * p << endl ;
Scorrimento di un array all’indietro
In maniera analoga possiamo scorrere all’indietro un array scrivendo:
for ( int i =n -1; i >=0; i - -)
// visualizza l ' indice e l ' elemento generico v [ i ]
cout << i << " " << *( v + i ) << endl ;
oppure scrivendo:
for ( int * p = v +n -1; p >= v ; p - -)
// visualizza l ' indice e l ' elemento generico * p
cout << p - v << " " << * p << endl ;
5
Stringhe
Il C++ prevede due meccanismi di elaborazione delle stringhe. La classe string del C++ memorizza una sequenza
arbitraria di caratteri e supporta una serie di comode funzioni come la concatenazione, l’estrazione di una “sottostringa”
da un’altra stringa, il confronto tra stringhe ecc. C++ però eredita dal linguaggio C un livello più elementare di gestione
delle stringhe. Nel linguaggio C, infatti, una stringa è in realtà un array di caratteri che termina con il carattere nullo
’\0’ (una particolare sequenza di escape). In questo caso si parla di C-stringhe (C-string). Può capitare di doversi
interfacciare con funzioni che ricevono e restituiscono valori char * per cui conviene sapere come gestire correttamente
situazioni di questo tipo.
Dichiarazione e inizializzazione di una stringa
Una stringa può essere definita e inizializzata come un array di caratteri o con una variabile di tipo char *. Per esempio:
char saluto []= " Ciao ! ";
5
oppure
char * saluto = " Ciao ! ";
Un altra possibilità è scrivere:
char saluto []={ 'C ' , 'i ' , 'a ' , 'o ' ,'! ' , ' \0 ' };
Occorre ricordarsi in quest’ultimo caso di inserire anche il carattere ’\0’ di terminazione della stringa (terminatore
nullo), mentre, nei due casi precedenti, l’allocazione del terminatore è implicita (quindi in ogni caso, la stringa saluto è
costituita da 6 caratteri, cioè deve essere sempre previsto un carattere in più per il terminatore nullo).
Una stringa può essere assegnata a un array di caratteri anche utilizzando la funzione scanf(). Per esempio se s è un
array di 30 caratteri:
char s [30];
l’istruzione
scanf ( " % s " ,s );
consentirà l’immissione da tastiera al massimo di 29 caratteri (’\0’ occuperà l’ultima posizione) finchè non si incontra
una newline o uno spazio. Non occorre usare l’operatore & perchè s è già un indirizzo di memoria (indirizzo di memoria
del primo elemento del array).
Lunghezza di una stringa
La seguente funzione restituisce il numero di caratteri di una stringa (lunghezza della stringa). La lunghezza della stringa
per definizione non comprende il carattere nullo.
int len ( char * s ) {
int l =0;
for ( char * p = s ; * p ; p ++)
l ++;
return l ;
}
La notazione
*p
se usata come espressione booleana sostituisce:
* p != ' \0 '
Se sfruttiamo invece esclusivamente l’aritmetica dei puntatori, la funzione diventa:
int len ( char * s ) {
char * p = s ;
for (; * p ; p ++) ;
return p - s ;
}
In realtà il C dispone già di una funzione per il calcolo della lunghezza di una stringa (oltre che di altre funzioni di
gestione delle stringhe) che si chiama strlen() il cui prototipo di funzione è int strlen(char *), ma lo studio di queste
funzioni va oltre lo scopo di questo articolo.
6
Esempi: algoritmi di ricerca
Algoritmo di ricerca lineare
Possiamo riscrivere come esercizio qualcuno degli algoritmi notevoli affrontati lo scorso anno nell’ottica dei puntatori.
Proviamo con l’algoritmo di ricerca lineare (o sequenziale):
# include ...
...
// cerco l ' intero x all ' interno di un array v di lunghezza n
int ricLinearePunt ( int * v , int n , int x ) {
6
}
for ( int * p = v ; p < v + n ; p ++)
if (* p == x )
// p - v restituisce l ' indice
return p - v ;
return -1;
int main () {
...
}
Algoritmo di ricerca binaria
Anche l’algoritmo di ricerca binaria non presenta particolari difficoltà; l’unica differenza con l’algoritmo classico è l’utilizzo
dei puntatori l, r, e p. Il valore dell’indice i viene calcolato sfruttando l’aritmetica dei puntatori.
# include ...
...
// cerco l ' intero x all ' interno di un array v di lunghezza n ordinato
int ricBinariaPunt ( int * v , int n , int x ) {
int * l = v ; // l " punta " all ' elemento di sinistra
int * r = v +n -1; // r " punta " all ' elemento di destra
while (l <= r ) { // confronto tra puntatori
int i = (l - v +r - v )/2; // trovo l ' indice dell ' elemento " mediano "
// usando l ' aritmetica dei punt .
int * p = v + i ; // p " punta " all ' elemento centrale dell ' array v
if ( x == * p ) // se ho trovato l ' elemento che sto cercando
return i ; // restituisco la posizione
}
}
if ( x < * p ) // se x si trova a sinistra , effettuo la ricerca
// nel sottointervallo sinistro
r = v +i -1;
else
l = v + i +1; // altrimenti cerco nel sottointervallo di destra
// elemento non trovato
return -1;
int main () {
...
}
7
Allocazione di memoria
Variabili statiche e automatiche
Le variabili di cui abbiamo parlato finora rispettano due semplici regole:
1. La memoria per le variabili globali (quelle dichiarate fuori dal main() per intenderci) rimane assegnata per tutta
l’esecuzione del programma
2. La memoria assegnata a una variabile dichiarata all’interno di un blocco viene occupata nel momento in cui il flusso
d’esecuzione entra nel blocco e viene liberata nel momento in cui esce . L’area di memoria occupata prende il
nome di stack. Ad esempio, la memoria per i parametri formali e le variabili locali di una funzione viene occupata
quando si invoca la funzione e viene liberata quando l’esecuzione termina.
7
Chiamiamo automatica una variabile a cui viene assegnata memoria nel momento in cui il flusso d’esecuzione entra in
un blocco; la memoria viene liberata all’uscita dal blocco.
Chiamiamo invece statica una variabile la cui memoria rimane impegnata per tutta l’esecuzione del programma. Le
variabili globali sono statiche mentre le variabili dichiarate all’interno di un blocco sono automatiche. All’interno di un
blocco, una variabile può essere dichiarata statica usando la parola riservata static; scriviamo:
static int x ;
Le variabili statiche dichiarate all’interno di un blocco conservano all’uscita il loro valore. Potremmo considerarle
come delle variabili “ibride”, cioè variabili locali che conservano alcune proprietà delle variabili globali. Per esempio,
consideriamo la funzione prova():
void prova () {
static int x =0;
int y =10;
x = x +2;
y = y +1;
cout << " x = " << x << " e y = " << y << endl ;
}
La variabile statica x viene inizializzata e allocata solo una volta. Se invocassimo la funzione prova() 5 volte, i valori
di x saranno 2,4,6,8,10 perchè la memoria riservata ad x rimane impegnata per tutta la durata del programma, mentre
la variabile y viene allocata ogni volta che viene invocata la funzione test (il suo valore quindi è sempre 11).
int main () {
for ( int i =0; i <5; i ++)
prova ();
return 0;
}
Risultato dell’esecuzione:
x =2 e y =11
x =4 e y =11
x =6 e y =11
x =8 e y =11
x =10 e y =11
Memoria dinamica
Accanto alle variabili automatiche e statiche, troviamo le variabili dinamiche.
Le variabili dinamiche intanto non seguono questo meccanismo di allocazione/deallocazione automatica, per cui il
programmatore ha l’onere di allocarle e deallocarle.
Per allocare una variabile di memoria di tipo dinamico relativa a un intero, ricorriamo alla funzione del C malloc()
(memory allocation); si scrive in C:
( int *) malloc ( sizeof ( int ));
oppure in C++:
static_cast < int * >( malloc ( sizeof ( int ));
Il cast si rende necessario in quanto il prototipo della funzione malloc() è (void *) malloc(size_t numeroByte). Il
puntatore universale (void *) permette di allocare dati di tipo diverso mentre size_t è sinonimo di unsigned o unsigned
long. numeroByte di solito è nella forma sizeof().
Una notazione ancora più semplificata (C++) è la seguente:
new int ;
Le variabili dinamiche vengono create all’interno di un’area di memoria denominata heap. Quindi l’effetto dell’esecuzione dell’istruzione precedente è allocare nello heap 2 o 4 byte.
Stack e heap si contendono la stessa area di memoria in realtà, per cui se aumenta lo stack diminuisce lo heap e
viceversa.
Il problema posto dalle variabili dinamiche è che esse in realtà non hanno un nome: si rende pertanto necessario usare
un puntatore per fare riferimento ad esse, altrimenti sarebbe impossibile usarle.
8
int * p = new int ; // allocazione dinamica in C ++ di un intero " puntato " da p
Non è detto che nello heap ci sia spazio sufficiente per cui è necessario controllare che l’allocazione sia andata a buon
fine:
if ( p != NULL ) {
// tutto ok !
} else {
// ci sono problemi di allocazione
}
Se vogliamo allocare un array dinamico, in C++ scriviamo:
int * p = new int [ n ]; // con n intero positivo
mentre la notazione in C è leggermente più complessa:
int * p =( int *) calloc (n , sizeof ( int ));
ed è sempre necessario un cast per i motivi detti precedentemente. La funzione calloc() (probabilmente l’origine del
nome è contiguous allocation) ha due argomenti: la dimensione dell’area da allocare e l’occupazione di memoria del
singolo elemento.
Possiamo “raggiungere” l’elemento i-esimo con l’aritmetica dei puntatori.
Possiamo riallocare un’area di memoria precedentemente allocata (puntata per esempio da p), sia per aumentarne la
dimensione che per diminuirla con realloc() (realloc() è una funzione del C).
// rialloca dinamicamente l ' area puntata precedentemente
// da p riassegnando m locazioni (m > n o m < n ) , n >0
p =( int *) realloc (p , m * sizeof ( int ));
Se m<n, abbiamo una perdita dei dati, mentre se m>n i dati vengono conservati e vengono allocate nuove variabili
(inizializzate a 0 nel caso di interi).
A titolo di esempio, riportiamo di seguito l’uso combinato delle tre funzioni malloc(), calloc() e realloc() e il risultato
dell’esecuzione del programma.
# include < cstdlib >
# include < iostream >
using namespace std ;
void stampa ( int k , int * v , int n ) {
cout << " Stampa " << k << endl ;
for ( int i =0; i < n ; i ++)
cout << i << " " << v [ i ] << endl ;
}
int main ( int argc , char * argv []) {
// allochiamo un intero
// per brevità omettiamo il controllo p == NULL
int * p = ( int *) malloc ( sizeof ( int ));
// assegniamo il valore -32 ( scelto a caso )
* p = -32;
// deallochiamo l ' area puntata da p
delete p ;
p = NULL ;
int m =10;
// allochiamo adesso un array
p = ( int *) calloc (m , sizeof ( int ));
stampa (1 ,p , m );
9
// carichiamo i numeri pari
for ( int i =0; i < m ; i ++)
p [ i ]=2* i ;
stampa (2 ,p , m );
// diminuiamo il numero di elementi
m =5;
// riallochiamo l ' area
p = ( int *) ( realloc (p , m * sizeof ( int )));
stampa (3 ,p , m );
// aumentiamo ancora il numero di elementi
m =8;
// riallochiamo ancora una volta
p = ( int *) ( realloc (p , m * sizeof ( int )));
}
stampa (4 ,p , m );
return EXIT_SUCCESS ;
Risultato dell’esecuzione del codice:
Stampa
0 0
1 0
2 0
3 0
4 0
5 0
6 0
7 0
8 0
9 0
Stampa
0 0
1 2
2 4
3 6
4 8
5 10
6 12
7 14
8 16
9 18
Stampa
0 0
1 2
2 4
3 6
4 8
Stampa
0 0
1 2
2 4
3 6
4 8
5 0
6 0
7 0
1
2
3
4
10
Dangling pointer
Le variabili dinamiche vanno necessariamente deallocate, per prevenire diversi problemi che si possono verificare, con
l’istruzione:
delete p ; // dealloca l ' area
o l’istruzione
delete [] p ; // dealloca l ' area occupata da un array
Osserviamo che l’istruzione precedente ha il solo effetto di contrassegnare come “libera” la zona di memoria a cui
si riferisce tale puntatore. Quindi la variabile p può ancora contenere l’indirizzo dello spazio di memoria reso libero; in
tal caso, diciamo che il puntatore è “penzolante” (dangling pointer ). Nel seguito, se si accede a tale spazio di memoria
usando quel puntatore senza aver assegnato ad esso l’indirizzo di una zona di memoria valida, in relazione allo specifico
sistema di esecuzione, si potrà assistere alla terminazione prematura del programma, con un messaggio di errore, oppure
avverrà un accesso a una zona di memoria non valida, con il probabile risultato di corrompere i dati del programma. Una
possibile strategia “difensiva” nei confronti di questo tipo di errori prevede di assegnare il valore NULL a un puntatore,
dopo che questo sia stato oggetto di un’operazione delete.
p = NULL ;
Memory leak
Analizziamo ora le seguenti istruzioni:
int * p = new int ;
* p =23;
p = new int ;
* p =41;
Prima viene allocata una variabile dinamica di tipo intero puntata da p a cui assegniamo il valore 23; successivamente
creiamo una nuova variabile dinamica e usiamo ancora p per puntare alla nuova area di memoria; in seguito assegniamo
all’area di memoria puntata da p il valore 41. Che cosa è accaduto al precedente spazio di memoria, quello a cui puntava
p? Esso rimane ancora allocato. Si tratta di un fenomeno che prende il nome di “memory leak” (letteralmente “perdita
di memoria”): uno spazio di memoria inutilizzato che non può essere allocato per altri scopi. Può sembrare poca cosa
avere in memoria 2 o 4 byte impegnati ma non utilizzati. Tuttavia, se il programma viene eseguito per lungo tempo
(come nel caso dei server che rimangono accesi 24 ore su 24 per 7 giorni alla settimana), oppure se viene allocata una
grande quantità di memoria, per esempio all’interno di un ciclo, il programma può esaurire facilmente tutta la memoria
disponibile a causa dell’allocazione continua di nuove variabili dinamiche, arrivando a una conclusione prematura e brusca.
Quando una variabile dinamica non serve più deve essere distrutta (prima con delete e poi imponendo p=NULL).
8
Puntatori a strutture
Consideriamo il seguente frammento di codice:
# include ...
struct Libro {
string isbn ;
string titolo ;
// in C invece si scrive :
// char isbn [18];
// char titolo [40];
// ecc
string autore ;
float prezzo ;
};
int main () {
Libro * p = new Libro ;
11
// in C si scrive Libro * p =( Libro *) malloc ( sizeof ( Libro ));
if ( p != NULL ) {
// ok
} else {
// impossibile allocare i dati
return -1;
}
...
// uso la struttura ( vedi istruzioni seguenti )
...
// dealloco la struttura puntata da p
delete p ;
p = NULL ;
}
return 0;
Come si vede è possibile allocare nello heap variabili di qualunque tipo. Possiamo individuare i singoli campi della
struttura con la notazione *p.nomeCampo:
* p . isbn = " 978 -88 -8433 -029 -1 " ;
// in C si usa la funzione strcpy
// strcpy (* p . isbn ,"978 -88 -8433 -029 -1");
* p . titolo =" Linguaggio C ++ " ;
...
Una diversa notazione molto diffusa è la seguente p->nomeCampo (il simbolo p-> si legge p puntato):
p - > isbn = " 978 -88 -8433 -029 -1 " ;
p - > titolo = " Linguaggio C ++ " ;
...
Prestiamo attenzione al frammento di codice seguente:
# include ...
int main () {
Libro l ;
...
l . isbn = " 978 -88 -8433 -029 -1 " ;
l . titolo = " Linguaggio C ++ " ;
...
}
l è una struttura allocata nello stack quindi è deallocata automaticamente uscendo dal main.
9
Puntatori come valori di ritorno
La memoria dinamica può essere usata con profitto nelle situazioni in cui una funzione debba restituire più valori di tipo
omogeneo. Basta infatti conoscere l’indirizzo di memoria del primo elemento per identificare tutto l’ insieme.
Consideriamo per esempio la funzione che restituisce i primi n valori della successione di Fibonacci quali elementi di
un array. Il codice della funzione potrebbe somigliare al seguente:
// funzione che riceve un valore intero
// e restituisce un puntatore
int * fib ( int n ) {
// f è un array locale
int f [ n ];
f [0]=1;
f [1]=1;
12
for ( int i =2; i <= n ; i ++)
f [ i ]= f [i -1]+ f[i -2];
}
return f ;
int main () {
int n = 10;
int * f = fib ( n );
for ( int i =0; i < n ; i ++)
cout << i << " " << f [ i ] << endl ;
}
return 0;
Il compilatore intanto segnala un errore di “warning”: “address of a local variable returned”. Richiamando poi la
funzione con n=10 otteniamo, contrariamente a quanto ci aspettiamo, una serie di valori casuali. Come mai?
0
1
2
3
4
5
6
7
8
9
1
2784272
3818720
15146996
134520896
15153600
-1075496200
14735920
3818720
15154300
Il motivo è che l’array dichiarato all’interno della funzione, essendo per sua natura locale, viene preso in carico dal
sistema di gestione automatico della memoria e, visto che le variabili locali vengono deallocate automaticamente uscendo
dalle funzioni, la variabile f relativa al contesto main() punterà ad un’area di memoria non più valida.
Una possibile soluzione è usare un array statico stabilendo, però, per la dimensione, un valore massimo.
int * fib ( int n ) {
static int f [20];
// ...
}
return f ;
Oppure, ancora meglio, possiamo ricorrere ad un array dinamico.
int * fib ( int n ) {
// alloca dinamicamente un ' area di lunghezza n
int * f = new int [ n ];
// ...
}
return f ;
int main () {
int n = 10;
int * f = fib ( n );
for ( int i =0; i < n ; i ++)
cout << i << " " << f [ i ] << endl ;
delete [] f ;
f = NULL ;
13
return 0;
}
Il valore di f va cancellato dopo aver ottenuto i valori e non prima.
Risultato dell’esecuzione:
0
1
2
3
4
5
6
7
8
9
1
1
2
3
5
8
13
21
34
55
Stesse considerazioni vanno fatte se la funzione restituisce invece un puntatore ad una struttura. Se la funzione
restituisce come valore di ritorno una struttura, non ci sono particolari accorgimenti da adottare.
10
Array di puntatori
In alcune circostanze, può essere utile considerare sequenze di puntatori (array di puntatori) organizzati in maniera
opportuna.
Consideriamo ad esempio un elenco di libri e di voler ordinare i libri in base a diversi criteri: prima vogliamo ordinarli
in base all’isbn (in senso crescente), poi in base al titolo (in base quindi all’ordinamento alfabetico) e infine in base al
prezzo (per esempio dal prezzo più alto al prezzo più basso, cioè ordinati in senso decrescente). Invece di scambiare di
volta in volta i singoli libri, possiamo ottimizzare gli ordinamenti semplicemente scambiando i singoli riferimenti ai libri
in base ai criteri scelti.
Abbiamo certamente una maggiore occupazione di memoria (per la presenza degli array di puntatori), ma abbiamo
anche il vantaggio di lasciare intatta la tabella di partenza senza dover scambiare fisicamente ogni volta i record (Fig.
10.1).
# include < iostream >
using namespace std ;
const int MAX = 4;
struct Libro {
string isbn ;
string titolo ;
float prezzo ;
};
// inizializza l ' array di puntatori
// ai " valori iniziali " della tabella
void inizPunt ( Libro * q [] , Libro v []) {
for ( int i =0; i < MAX ; i ++)
q [ i ]=& v [ i ];
}
// ordinamento crescente per codice isbn
void naiveSortPuntIsbn ( Libro * q []) {
Libro * temp = NULL ;
// tengo fisso un elemento e lo confronto con
// tutti gli altri
// poi ripeto per gli altri elementi
for ( int i =0; i < MAX -1; i ++)
for ( int j = i +1; j < MAX ; j ++)
14
}
}
// confronto i valori degli isbn MA scambio solo i puntatori
if ( q [ i ] - > isbn > q [ j ] - > isbn ) {
temp = q [ i ];
q [ i ]= q [ j ];
q [ j ]= temp ;
// ordinamento per prezzo
void naiveSortPuntPrezzo ( Libro * q []) {
Libro * temp = NULL ;
}
// tengo fisso un elemento e lo confronto con
// tutti gli altri
// poi ripeto per gli altri elementi
for ( int i =0; i < MAX -1; i ++)
for ( int j = i +1; j < MAX ; j ++)
// confronto i prezzi MA scambio solo i puntatori
if ( q [ i ] - > prezzo < q [ j ]- > prezzo ) {
temp = q [ i ];
q [ i ]= q [ j ];
q [ j ]= temp ;
}
// visualizzo gli elementi dell ' array
void stampa ( Libro v []) {
for ( int i =0; i < MAX ; i ++)
cout << i << " " << v [ i ]. isbn << " , "
<< v [ i ]. titolo << " , " << v [ i ]. prezzo << endl ;
}
void stampaPunt ( Libro * q []) {
for ( int i =0; i < MAX ; i ++)
cout << i << " " << q [ i ] - > isbn << " , "
<< q [ i ] - > titolo << " , " << q [ i ] - > prezzo << endl ;
}
int main () {
cout << " Array di puntatori " << endl ; // prints Array di puntatori
Libro libri [ MAX ]={
{ " 6521 " ," Linguaggio C " ,30} ,
{ " 4976 " ," Linguaggio Java " ,40} ,
{ " 5522 " ," Java Avanzato " ,70} ,
{ " 3421 " ," Linguaggio Ruby " ,25}
};
// definiamo i puntatori
Libro * p [ MAX ];
Libro * q [ MAX ];
// inizializziamo i puntatori
inizPunt (p , libri );
inizPunt (q , libri );
cout << " Array non ordinato ************************** " << endl ;
// visualizzo gli elementi prima dell ' ordinamento
stampa ( libri );
cout << " Ordino per isbn ( ordinamento crescente ) " << endl ;
// li ordino per isbn
15
Figura 10.1: Array di puntatori
naiveSortPuntIsbn ( p );
cout << " Array ordinato in base all ' isbn ************ " << endl ;
// stampo gli elementi dopo l ' ordinamento
stampaPunt ( p );
cout < < " Ordino per prezzo ( ordinamento decrescente ) " << endl ;
// li ordino per prezzo
naiveSortPuntPrezzo ( q );
cout << " Array ordinato in base al prezzo ************ " << endl ;
// stampo gli elementi dopo l ' ordinamento
stampaPunt ( q );
return 0;
}
Risultato dell’esecuzione:
Array di puntatori
Array non ordinato **************************
0 6521 , Linguaggio C , 30
1 4976 , Linguaggio Java , 40
2 5522 , Java Avanzato , 70
3 3421 , Linguaggio Ruby , 25
Ordino per isbn ( ordinamento crescente )
Array ordinato in base all ' isbn ************
0 3421 , Linguaggio Ruby , 25
1 4976 , Linguaggio Java , 40
2 5522 , Java Avanzato , 70
3 6521 , Linguaggio C , 30
Ordino per prezzo ( ordinamento decrescente )
Array ordinato in base al prezzo ************
0 5522 , Java Avanzato , 70
1 4976 , Linguaggio Java , 40
2 6521 , Linguaggio C , 30
3 3421 , Linguaggio Ruby , 25
11
Puntatori a puntatori
Per complicare ulteriormente le cose, è anche possibile dichiarare puntatori a puntatori.
Un puntatore a un puntatore consiste in una variabile di tipo puntatore (relativa a un tipo conosciuto, per esempio
int) che punta a un’altra variabile (relativa allo stesso tipo), la quale punta ad una variabile sempre del medesimo tipo
(cioè int nel nostro esempio, Fig. 11.1).
16
Figura 11.1: Puntatore a puntatore
# include < iostream >
using namespace std ;
int main () {
cout << " Puntatori a puntatori " << endl ; // prints Puntatori a puntatori
// dichiarazione di una variabile x intera inizializzata a 16
int x =16;
// dichiarazione di un puntatore a un intero e assegnazione
int * p =& x ;
// visualizzazione del valore all ' indirizzo contenuto in p
cout << " p = " << p << endl ;
cout << " * p = " << * p << endl ;
// dichiarazione di un puntatore a un puntatore
int ** q =& p ;
cout << " q = " << q << endl ;
cout << " * q = " << * q << endl ;
// visualizzazione del valore contenuto in x attraverso q
cout << " ** q = " << ** q << endl ;
cout << " Modifica del valore di x attraverso un doppio indirizzamento " << endl ;
// modifica del valore contenuto in x attraverso q
** q =7;
}
// visualizzazione del valore contenuto in x attraverso q
cout << " ** q = " << ** q << endl ;
return 0;
Risultato dell’esecuzione:
Puntatori a puntatori
p =0 xbfc19c58
* p =16
q =0 xbfc19c54
* q =0 xbfc19c58
** q =16
Modifica del valore di x attraverso un doppio indirizzamento
** q =7
Puntatori a puntatori come membri di strutture
Analizziamo il seguente programma (Fig. 11.2):
# include < iostream >
using namespace std ;
struct Autore {
17
};
string nome ;
string cognome ;
int eta ;
struct Libro {
string isbn ;
string titolo ;
// puntatore a puntatore , Fig . 11.2
Autore ** autori ;
};
void stampaAutore ( Autore a ) {
cout << " Stampa autore ******* " << endl ;
cout << a . nome << " " << a . cognome << endl ;
}
Autore prova1 () {
Autore a ={ " Ciccio " ," Formaggio " ,23};
return a ;
}
Autore * prova2 () {
Autore * a = new Autore ;
a - > cognome = " Rossi " ;
a - > nome = " Fabio " ;
a - > eta =32;
return a ;
}
Autore * prova3 () {
static Autore a ={ " Luigi " ," Neri " ,43};
return & a ;
}
void stampaLibro ( Libro l ) {
cout << " Stampa libro ********* " << endl ;
cout << " Codice isbn " << l . isbn << " , titolo " << l . titolo << endl ;
cout << " Autori : " << endl ;
for ( int i =0; i <3; i ++)
stampaAutore (* l . autori [ i ]);
cout << " Fine stampa autori " << endl ;
}
int main () {
cout << " Autori e libri " << endl ; // prints Autori e libri
Autore scorzoni ={ " Fabrizia " ," Scorzoni " ,40};
Autore cozzetto ={ " Maurizio " ," Cozzetto " ,52};
Autore malik ={ " Maluk " ," Malik " ,45};
Autore * autori [3]={& scorzoni ,& cozzetto ,& malik };
Libro libroMaiScritto = { " 832892 " ," Programmare in Java " , autori };
stampaLibro ( libroMaiScritto );
Autore a = prova1 ();
stampaAutore ( a );
Autore * b = prova2 ();
stampaAutore (* b );
delete b ;
18
Figura 11.2: Puntatore a puntatore (membro di una struttura)
b = NULL ;
Autore * c = prova3 ();
stampaAutore (* c );
return 0;
}
Risultato dell’esecuzione:
Autori e libri
Stampa libro *********
Codice isbn 832892 , titolo Programmare in Java
Autori :
Stampa autore *******
Fabrizia Scorzoni
Stampa autore *******
Maurizio Cozzetto
Stampa autore *******
Maluk Malik
Fine stampa autori
Stampa autore *******
Ciccio Formaggio
Stampa autore *******
Fabio Rossi
Stampa autore *******
Luigi Neri
12
Array di puntatori come membri di strutture
Consideriamo ora il seguente codice (Fig. 12.1):
# include < iostream >
using namespace std ;
struct Autore {
string nome ;
string cognome ;
int eta ;
};
struct Libro {
string isbn ;
19
string titolo ;
Autore * autori [3]; // array di puntatori
};
void stampaAutore ( Autore a ) {
cout << " Stampa autore ******* " << endl ;
cout << a . nome << " " << a . cognome << endl ;
}
void stampaLibro ( Libro l ) {
cout << " Stampa libro ********* " << endl ;
cout << " Codice " << l . isbn << " " << l . titolo << endl ;
cout << " Autori " << endl ;
for ( int i =0; i <3; i ++)
stampaAutore (* l . autori [ i ]);
cout << " Fine stampa autori " << endl ;
}
int main () {
cout << " Autori e libri " << endl ; // prints Autori e libri
Autore scorzoni ={ " Fabrizia " ," Scorsoni " ,40};
Autore cozzetto ={ " Maurizio " ," Cozzetto " ,52};
Autore malik ={ " Maluk " ," Malik " ,45};
Libro libroMaiScritto = { " 832892 " ," Programmare in Java " ,{& scorzoni ,& cozzetto ,& malik }};
stampaLibro ( libroMaiScritto );
return 0;
}
Risultato dell’esecuzione:
Autori e libri
Stampa libro *********
Codice 832892
Programmare in Java
Autori
Stampa autore *******
Fabrizia Scorsoni
Stampa autore *******
Maurizio Cozzetto
Stampa autore *******
Maluk Malik
Fine stampa autori
Osserviamo che gli autori nei due programmi sono memorizzati in due modalità tra loro diverse: nel primo programma,
l’array degli autori non è memorizzato all’interno della struttura Libro (infatti vi è solo un puntatore che punterà ad un
array i cui valori a loro volta sono dei puntatori agli autori) bensì all’esterno, mentre nel secondo caso, l’array dei puntatori
agli autori è memomizzato all’interno della struttura Libro (gli autori veri e propri sono sempre archiviati all’esterno della
struttura). C’è comunque una certa differenza.
13
Confronto fra classi e strutture
I dati strutturati di tipo struct sono molto simili a quelli di tipo class. Come nelle classi, i membri di una struttura
possono essere funzioni e possono essere presenti costruttori e un distruttore. L’unica differenza tra struct e class è che,
per impostazione predefinita, tutti i membri di una struttura di tipo struct sono public, mentre quelli di una struttura di
tipo class sono private. La definizione di struct in C++ è simile alla definizione di struct in C, linguaggio dal quale deriva.
Proprio in virtù di tale evoluzione, una definizione di struct che sia valida in C lo è anche in C++. Tuttavia, in C++ le
potenzialità delle strutture di tipo struct sono state estese, aggiungendo la possibilità di definirvi funzioni, costruttori e
distruttore: in C++ classi e strutture hanno le stesse potenzialità. Ciò nonostante, i programmatori limitano l’uso delle
definizioni struct a strutture che rispondano alla sintassi prevista dal linguaggio C, senza usare funzioni. In altre parole,
20
Figura 12.1: Array di puntatori quali membri di una struttura
se tutte le variabili di una classe sono pubbliche e la classe non ha funzioni membro, solitamente la si definisce mediante
il costrutto sintattico struct.
# include < iostream >
# include < cstdlib >
using namespace std ;
struct Contatto {
public :
string nome ;
string cognome ;
char sex ;
int eta ;
};
Contatto ();
Contatto ( string nome , string cognome );
void stampa ();
Contatto :: Contatto () {
nome = " Mario " ;
cognome = " Rossi " ;
sex = 'M ';
eta =32;
}
Contatto :: Contatto ( string nome , string cognome ) {
this - > nome = nome ;
this - > cognome = cognome ;
}
void Contatto :: stampa () {
cout << " Stampa ****** " << endl ;
cout << nome << endl ;
cout << cognome << endl ;
}
Contatto crea () {
// funziona anche senza static
static Contatto a ;
a . cognome = " Capperi " ;
21
a . nome = " Romualdo " ;
a . sex = 'M ';
a . eta =23;
return a ;
}
Contatto * crea2 () {
Contatto * a = new Contatto ;
a - > cognome = " Capperi " ;
a - > nome = " Romualdo " ;
a - > sex = 'M ';
a - > eta =23;
return a ;
}
int main () {
// diverse notazioni sintattiche
Contatto b ;
b . stampa ();
Contatto c ( " Ciccio " ," Bello " );
c . stampa ();
Contatto a = crea ();
a . stampa ();
Contatto * d = crea2 ();
(* d ). stampa ();
d - > stampa ();
delete d ;
d = NULL ;
return 0;
}
14
Vector
La libreria STL
La libreria STL (Standard Template Library) è una raccolta di classi per la gestione di strutture di dati (chiamate anche
collezioni o contenitori), di classi iteratore, di classi contenenti algoritmi generici (per la ricerca, per l’ordinamento e altro)
e funzioni varie di appoggio (soprattutto per l’overloading degli operatori). Le classi della libreria STL sono generiche.
La definizione include le parentesi angolari delle variabili di tipo.
Classe vector
Uno dei limiti che caratterizzano gli array è la dimensione che rimane fissa dopo la creazione dell’array stesso: vi può
essere memorizzato solo un numero prefissato di elementi. Inoltre l’inserimento di un elemento in una specifica posizione
dell’array può richiedere lo scorrimento di altri elementi e, analogamente, la rimozione di un elemento può di nuovo
richiedere lo scorrimento di altri elementi, dal momento che, solitamente, non si lasciano posizioni vuote tra le posizioni
che contengono dati (di solito, le posizioni vuote si trovano alla fine dell’array).
Oltre agli array, il linguaggio C++ consente di realizzare liste (di cui ci occuperemo diffusamente in un altro articolo)
mediante la classe vector. Diversamente da un array, un vettore può aumentare e diminuire la propria dimensione durante
l’esecuzione del programma, per cui non ci si deve preoccupare del numero di elementi presenti al suo interno.
L’enunciato seguente dichiara che interi è un vettore vuoto e che i suoi elementi sono di tipo int.
vector < int > interi ;
Con riferimento alla struttura Libro definita nella pagine precedenti, abbiamo:
22
Figura 14.1: Inserimento di un libro in un vector
Figura 14.2: Inserimento di 2 ulteriori libri in un vector
vector < Libro > libri ;
La classe vector mette a disposizione varie operazioni per la manipolazione dei dati all’interno di un vettore. In
particolare, abbiamo l’inserimento (d’ora in poi consideriamo la struttura Libro):
Libro l ={ " 389 -432 -3422 -33 -2 " ," Programmazione in C ++ " ,40};
libri . push_back ( l ); // viene inserita una copia di l alla fine di libri , Fig . 14.1
Inseriamo ancora 2 libri:
Libro m ={ " 123 -665 -2343 -76 -1 " ," Programmazione in Java " ,50};
libri . push_back ( m ); // viene inserita una copia di m alla fine di libri
Libro n ={ " 382 -829 -8281 -30 -2 " ," Programmazione in Ruby " ,40};
libri . push_back ( n ); // viene inserita una copia di n alla fine di libri , Fig . 14.2
Per accedere all’elemento di indice i di un vettore v si possono usare le notazioni v[i] oppure v.at(i): quest’ultima è
da preferire in quanto il metodo at() lancia l’eccezione out_of_range se i non è un valore compreso tra 0 e v.size()-1 (il
metodo size() restituisce il numero di elementi del vettore).
Possiamo scorrere quindi tutti gli elementi del vettore con un ciclo del tipo:
for ( int i =0; i < libri . size (); i ++)
stampa ( libri . at ( i ));
dove stampa() è la funzione:
void stampa ( Libro l ) {
cout << l . isbn << " , " << l . titolo << " , " << l . prezzo << endl ;
}
23
Per inserire un elemento nella lista, possiamo anche usare il metodo insert():
libri . insert ( libri . begin () , l );
Il metodo insert() ha due argomenti: il primo è un iteratore che ne specifica la prima posizione mediante il metodo
begin() e il secondo è l’elemento da inserire. Il metodo insert() restituisce un iteratore che punta all’elemento appena
inserito. In C++, gli iteratori sono visti come una generalizzazione del concetto di puntatore: diversamente dai puntatori,
gli iteratori non dipendono dal tipo dell’elemento del contenitore. L’associazione di un oggetto iteratore al contenitore
avviene mediante l’uso dell’operatore di risoluzione dell’ambito di visibilità ::
vector < Libro >:: iterator it = insert ( libri . begin () , l );
vector possiede gli iteratori iterator, const_iterator, reverse_iterator e const_reverse_iterator il cui studio però esula
dagli scopi del presente articolo.
Altro metodo utile è il metodo end() che, contrariamente a quanto suggerisce il suo nome, restituisce l’iteratore che
punta alla posizione successiva a quella dell’ultimo elemento del vettore.
Per cancellare un elemento di cui è noto l’iteratore (che viene fornito come argomento), usiamo il metodo erase(),
che sposta inoltre tutti gli elementi da qui fino alla fine di una posizione verso sinistra, e decrementa la dimensione. Per
cancellare un elemento di posizione i, scriviamo:
libri . erase ( libri . begin ()+ i );
In particolare per cancellare l’ultimo elemento del vettore, scriviamo:
libri . erase ( libri . end () -1);
Per sapere se ci sono elementi nel vettore, usiamo il metodo empty() che restituisce un valore booleano:
if ( libri . empty ()) {
// il vettore è vuoto
}
else {
// ci sono elementi
}
Riportiamo un importante esempio riassuntivo; in esso, troviamo anche l’utilizzo di importanti funzioni (serve l’include
dell’header <algorithm>) quali sort(), search(), binary_search() ecc:
# include < iostream >
# include < algorithm >
# include < vector >
using namespace std ;
struct Libro {
string isbn ;
string titolo ;
double prezzo ;
};
void stampaLibro ( Libro l ) {
cout << l . isbn << " , " << l . titolo << " , " << l . prezzo << " euro " << endl ;
}
void stampaTabella ( vector < Libro > libri ) {
for ( unsigned int i =0; i < libri . size (); i ++)
stampaLibro ( libri [ i ]);
// versione iterativa di stampa che fa uso degli iteratori
// for ( vector < Libro >:: const_iterator it = libri . begin (); it != libri . end (); ++ it )
//
stampaLibro (* it );
}
// funzione di confronto usata per l ' ordinamento
bool confrontaIsbn ( const Libro a , const Libro b ) {
return a . isbn < b . isbn ;
}
24
// funzione di confronto usata per la ricerca
bool confrontaIsbn2 ( const Libro a , const Libro b ) {
return a . isbn == b . isbn ;
}
// funzione usata per l ' ordinamento rispetto ai prezzi
bool confrontaPrezzi ( const Libro a , const Libro b ) {
return a . prezzo < b . prezzo ;
}
int main () {
cout << " Vector di libri " << endl ;
vector < Libro > libri ;
cout << " ++++++++ Numero di elementi = " ;
cout << libri . size () << endl ;
cout << " ++++++++ Aggiungo un libro " << endl ;
Libro a = { " RM -932 -11 " ," Visual Basic " ,32.0};
libri . push_back ( a );
stampaLibro ( a );
cout << " ++++++++ Aggiungo un libro " << endl ;
Libro b ={ "WC -453 -24 " ," Unified Modeling Language " ,44.50};
libri . push_back ( b );
stampaLibro ( b );
cout << " ++++++++ Aggiungo un libro " << endl ;
Libro c ={ "XF -100 -46 " ," Ruby / Groovy " ,33.70};
libri . push_back ( c );
stampaLibro ( c );
cout << " ++++++++ Aggiungo un libro " << endl ;
Libro d ={ "CJ -670 -54 " ," Visual C # " ,45.70};
libri . push_back ( d );
stampaLibro ( d );
cout << " ++++++++ Numero di elementi = " ;
cout << libri . size () << endl ;
cout << " ++++++++ Stampa di tutti i libri " << endl ;
stampaTabella ( libri );
// se si vuole usare un " iteratore "
// vector < int >:: iterator it = v . begin ()+2;
// v . erase ( it );
// il libro all ' indirizzo libri . begin ()+2
// (* si legge all ' indirizzo di )
Libro l = *( libri . begin ()+2);
cout << " ++++++++ Elemento da cancellare in posizione 2 " ;
cout << " ( a partire da 0 quindi è il terzo elemento ) " << endl ;
cout << " l . isbn = " << l . isbn << endl ;
cout << " ++++++++ Visualizzo i dati del libro da cancellare " << endl ;
stampaLibro ( l );
libri . erase ( libri . begin ()+2);
cout << " ++++++++ Libro cancellato " << endl ;
cout << " ++++++++ Numero di elementi = " ;
25
cout << libri . size () << endl ;
cout << " ++++++++ Stampa di tutti i libri " << endl ;
stampaTabella ( libri );
cout << " ++++++++ Ricerca ( lineare ) di un libro " << endl ;
string isbn = " WC -453 -24 " ;
// supponiamo di conoscere per ipotesi solo l ' isbn
Libro p = { isbn , " " ,0.0};
// creo un array con un solo elemento
Libro libri2 []={ p };
// cerco il libro , l ' incremento di libri2 è unitario
// visto che cerco un solo elemento
vector < Libro >:: const_iterator it =
search ( libri . begin () , libri . end () , libri2 , libri2 +1 , confrontaIsbn2 );
if ( it == libri . end ()) {
cout << " Libro non trovato " << endl ;
return -1;
} else {
int i = it - libri . begin ();
cout << " Libro con codice isbn " << isbn ;
cout << " individuato in posizione " << i << endl ;
stampaLibro ( libri . at ( i ));
}
// se voglio ordinare
cout << " +++++++ Ordinamento ( per codice isbn ) " << endl ;
// libri . begin () è un iteratore che " punta " al primo elemento
// ed libri . end () alla posizione successiva all ' ultimo elemento
// la funzione confrontaIsbn () rappresenta il criterio in base
// al quale si ordina ( in questo caso il codice isbn )
sort ( libri . begin () , libri . end () , confrontaIsbn );
cout << " ++++++++ Stampa di tutti i libri " << endl ;
stampaTabella ( libri );
cout << " +++++++ Ricerca binaria " << endl ;
bool t = binary_search ( libri . begin () , libri . end () , l , confrontaIsbn2 );
if ( t ) {
} else {
}
vector < Libro >:: const_iterator low =
lower_bound ( libri . begin () , libri . end () , l , confrontaIsbn2 );
int i = low - libri . begin ();
cout << " Libro con codice " << isbn << " individuato ";
cout << " alla posizione " << i << endl ;
stampaLibro ( libri . at ( i ));
cout << " Il libro non esiste " << endl ;
cout << " ++++++++ Stampa di tutti i libri " << endl ;
stampaTabella ( libri );
// cambio l ' isbn del terzo libro
cout << " ++++++++ Cambio il titolo e il prezzo del secondo " ;
cout << " libro e il titolo " << endl ;
cout << " ( l ' isbn rimane lo stesso ) " << endl ;
26
libri . at (1). titolo = " Effective Java " ;
libri . at (1). prezzo =23.4;
cout << " ++++++++ Stampa di tutti i libri " << endl ;
stampaTabella ( libri );
cout << " ++++++++ Ordinamento ( in base al prezzo ) " << endl ;
sort ( libri . begin () , libri . end () , confrontaPrezzi );
}
cout << " ++++++++ Stampa di tutti i libri " << endl ;
stampaTabella ( libri );
return 0;
Risultato dell’esecuzione:
Vector di libri
++++++++ Numero di elementi =0
++++++++ Aggiungo un libro
RM -932 -11 , Visual Basic , 32 euro
++++++++ Aggiungo un libro
WC -453 -24 , Unified Modeling Language , 44.5 euro
++++++++ Aggiungo un libro
XF -100 -46 , Ruby / Groovy , 33.7 euro
++++++++ Aggiungo un libro
CJ -670 -54 , Visual C # , 45.7 euro
++++++++ Numero di elementi =4
++++++++ Stampa di tutti i libri
RM -932 -11 , Visual Basic , 32 euro
WC -453 -24 , Unified Modeling Language , 44.5 euro
XF -100 -46 , Ruby / Groovy , 33.7 euro
CJ -670 -54 , Visual C # , 45.7 euro
++++++++ Elemento da cancellare in posizione 2
( a partire da 0 quindi è il terzo elemento ) l . isbn = XF -100 -46
++++++++ Visualizzo i dati del libro da cancellare
XF -100 -46 , Ruby / Groovy , 33.7 euro
++++++++ Libro cancellato
++++++++ Numero di elementi =3
++++++++ Stampa di tutti i libri
RM -932 -11 , Visual Basic , 32 euro
WC -453 -24 , Unified Modeling Language , 44.5 euro
CJ -670 -54 , Visual C # , 45.7 euro
++++++++ Ricerca ( lineare ) di un libro Libro con codice isbn
WC -453 -24 individuato in posizione 1
WC -453 -24 , Unified Modeling Language , 44.5 euro
+++++++ Ordinamento ( per codice isbn )
++++++++ Stampa di tutti i libri
CJ -670 -54 , Visual C # , 45.7 euro
RM -932 -11 , Visual Basic , 32 euro
WC -453 -24 , Unified Modeling Language , 44.5 euro
+++++++ Ricerca binaria
Libro con codice WC -453 -24 individuato alla posizione 0
CJ -670 -54 , Visual C # , 45.7 euro
++++++++ Stampa di tutti i libri
CJ -670 -54 , Visual C # , 45.7 euro
RM -932 -11 , Visual Basic , 32 euro
WC -453 -24 , Unified Modeling Language , 44.5 euro
++++++++ Cambio il titolo e il prezzo del secondo libro
e il titolo ( l ' isbn rimane lo stesso )
++++++++ Stampa di tutti i libri
CJ -670 -54 , Visual C # , 45.7 euro
RM -932 -11 , Effective Java , 23.4 euro
WC -453 -24 , Unified Modeling Language , 44.5 euro
27
++++++++ Ordinamento ( in base al prezzo )
++++++++ Stampa di tutti i libri
RM -932 -11 , Effective Java , 23.4 euro
WC -453 -24 , Unified Modeling Language , 44.5 euro
CJ -670 -54 , Visual C # , 45.7 euro
15
Conclusioni
Riportiamo a conclusione dell’articolo due esempi particolarmente interessanti. Tutta la logica viene espressa usando
quasi esclusivamente i puntatori.
Esempio 1
Come esempio di utilizzo corretto di allocazione dinamica, riportiamo il codice di una funzione che inverte una stringa
(in C):
# include ...
....
char * inverti ( char * s ) {
// se passo un puntatore NULL , ritorno NULL
if ( s == NULL )
return NULL ;
// troviamo la lunghezza della stringa
int l = strlen ( s );
// allochiamo l +1 caratteri ( aggiungo un carattere in più
// per il terminatore della stringa ) nello heap
char * t =( char *) calloc ( l +1 , sizeof ( char )); // oppure char * t = new char [ l +1];
// in caso di mancata allocazione
if ( t == NULL )
return NULL ;
// u è un puntatore che si sposta " in avanti " ( in t )
char * u = t ;
// p è un puntatore che si sposta " indietro " ( in s )
for ( char * p = s +l -1; p >= s ; p - -) {
// copio il carattere puntato da p in quello puntato da u
* u =* p ;
u ++;
}
// copio il terminatore nell ' ultimo carattere
* u = ' \0 ';
}
// non dealloco l ' area nello heap per
// renderla disponibile all ' esterno
return t ;
int main () {
// creo una stringa
char a []= " Ciao come va ? " ;
// ne creo un ' altra nello heap " girata "
char * b = inverti ( a );
28
// la visualizzo
printf ( " % s \ n " ,b );
// dealloco l ' area puntata da b nello heap
// non sarebbe strettamente necessario
// visto che il programma sta per terminare
// tuttavia è preferibile farlo come
// " best practise "
delete [] b ;
b = NULL ;
return 0;
} // fine main
Il prototipo della funzione char *inverti(char *) restituisce un puntatore a un carattere come valore di ritorno.
Esempio 2
Come ulteriore esempio, riportiamo un programma di conversione decimale-binario che usa intensamente i puntatori.
Il programma proposto ovviamente ha un valore puramente didattico ma in ogni caso è in grado di fornire migliori
risultati di quelli offerti per esempio da OpenCalc (provare per credere!): l’array che conterrà la sequenza dei resti viene
dimensionato dinamicamente (aggiungendo un solo elemento alla volta quando si presenta l’esigenza) senza dover allocare
preventivamente un numero massimo di elementi. In questo esempio, utilizziamo la funzione realloc() per non perdere
ogni volta i valori calcolati precedentemente.
/*
* File :
main . cpp
* Author : maurizio
*
* Created on 28 aprile 2010 , 11.39
*/
# include < stdlib .h >
# include < iostream >
// serve per la costante ULONG_MAX
# include < limits .h >
using namespace std ;
int * decToBin ( unsigned long x , int * l );
void stampaRev ( int * v , int n );
int main ( int argc , char ** argv ) {
// unsigned long x = ULONG_MAX ; // ULONG_MAX vale 4294967295
unsigned long x =102001;
cout << " x = " << x << endl ;
// dimensione dell ' area che verrà allocata dinamicamente
int n =0;
// conversione decimale - binario
// b è un puntatore che punterà
// all ' area di memoria allocata dinamicamente
// dalla funzione decToBin ()
// n è la sua dimensione ( calcolata anche questa dinamicamente )
int * b = decToBin (x ,& n );
// stampo al contrario
cout << " b = " ;
stampaRev (b ,n );
// dealloco la memoria dinamica ( non sarebbe necessario
// visto che il programma sta per terminare )
29
delete [] b ;
b = NULL ;
}
return ( EXIT_SUCCESS );
// converte l ' intero lungo x e calcolando la lunghezza dell ' array risultato
int * decToBin ( unsigned long x , int * l ) {
// contatore
int n =0;
// alloco dinamicamente la prima volta
// solo un valore intero
int * b =( int *) calloc ( n +1 , sizeof ( int ));
// per non appesantire il codice non ho inserito
// le if di controllo su b == NULL
do {
// quoziente e resto
int q = x /2;
int r = x %2;
// copio il resto nella memoria dinamica
*( b + n )= r ;
// prossimo elemento
n ++;
// copio la dimensione della memoria dinamica
*l=n;
// aumento la memoria dinamica ancora di un intero
b =( int *) realloc (b , sizeof ( int )*( n +1));
// se ho terminato copio l ' indirizzo di memoria della dimensione n
// e interrompo
if ( q ==0) {
l =& n ;
break ;
}
x=q;
} while ( true );
}
// restituisco il puntatore all ' area di memoria dinamica
return b ;
// stampa il vettore di interi al contrario
void stampaRev ( int * v , int n ) {
for ( int i =n -1; i >=0; i - -)
cout << *( v + i );
cout << endl ;
}
16
Bibliografia
H. Deitel-P. Deitel, Corso completo di programmazione, Terza edizione, Apogeo, 39 euro
30
C. Horstman, Fondamenti di C++, McGraw-Hill, 41,50 euro
F. Scorzoni, Programmazione in C++, Loescher, 19,00 euro
D. S. Malik, Programmazione in C++, Apogeo, 45 euro
M. Della Puppa, Mnuale di programmazione orientata agli oggetti, Hoepli, 20,50 euro
17
Ringraziamenti
Ringrazio i miei colleghi Alessandro Bugatti e Alberto Regosini e il mio amico Francesco Sarasini per i preziosi suggerimenti.
31