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