INFORMATICA Puntatori e memoria dinamica
Puntatori I  puntatori   sono variabili che contengono indirizzi di memoria. Sono specificati tramite l’operatore unario ‘ * ’ specificato accanto al nome della variabile. Sintassi: <tipo> ‘*’ <identificatore> Per ogni tipo di variabile “ puntata  ”, esiste un “ tipo  ” diverso di puntatore: Esempi: int x;     int *px;  /*  puntatore a un intero breve  */ double y;     double *py;   /*  puntatore a un double  */
Puntatori: esempio int  x=5,  *px; … px = &x; dopo quel frammento di programma px  contiene l’indirizzo di  x  (cioè 10016) . Pertanto  x  e  *px   sono la stessa  cosa! Memoria 5 x px Indirizzi 10016 28104 10016
Puntatori Gli operatori  ‘ * ’ e  ‘ & ’  sono uno l’inverso dell’altro L’applicazione consecutiva in qualunque ordine ad un puntatore fornisce lo stesso risultato *&px   e  &*px   sono la stessa cosa ! In termini di operatori: ‘ * ’=  operatore di  indirezione Opera su un indirizzo  Ritorna il valore contenuto in quell’indirizzo ‘ & ’ =  operatore di  indirizzo Opera su una variabile Ritorna l’indirizzo di una variabile Quindi: *(&px):   il contenuto della cella che contiene l’indirizzo di  px &(*px):   l’indirizzo della variabile puntata da  px
Puntatori Nelle istruzioni di I/O per i puntatori è definito un apposito descrittore di campo,  %p , il quale converte in un intero esadecimale la variabile a cui è riferito. Questo descrittore può essere usato sia con la  scanf  che con la  printf , tuttavia la sua utilità in input è riservata a casi particolarissimi di accesso diretto a specifiche locazioni di memoria. In output invece può essere utilizzato per visualizzare l’indirizzo di memoria delle variabili.
Puntatori Esempio: #include <stdio.h> #include <stdlib.h> int main() { int a, *aptr; a = 256; aptr = &a; printf (“\nL'indirizzo di a è %p&quot;,  &a);  /* indirizzo di a  */ printf (&quot;\nIl valore di  aptr è %p&quot;,  aptr);  /* indirizzo di a  */ printf (&quot;\nIl valore di  a  è %p&quot;, a);  /* valore di a in Hex.  */ printf (&quot;\nIl valore di  *aptr è %p&quot;,  *aptr);  /* valore di a in Hex*/ printf (&quot;\n&*aptr = %p ----- *&aptr = %p&quot;,  &*aptr, *&aptr); /* indirizzo di a  */ }
Operazioni sui puntatori Sui puntatori sono definite solo alcune operazioni: Incremento/decremento int  *px; px++;   px--;   /*  sono operazioni lecite  */ Somma/sottrazione di un  valore intero int *px   px += 3;  px -= 5;   /*  sono operazioni lecite  */ Assegnazione di un puntatore ad un altro dello  stesso tipo int *px,  *py;   px = py;  /* è un  operazione lecita  */
Operazioni sui puntatori Sottrazione tra due puntatori (NON la somma) dello  stesso tipo double  *px,  *py;   px – py  /*  è un  operazione lecita e produce un risultato intero  (quanti elementi ci sono tra i due indirizzi) */ Confronto tra puntatori dello  stesso tipo int  *px, *py;   px == py  px != py  px > py  /*  sono  operazione lecite */ Confronto tipico:  px == NULL
Operazioni sui puntatori:  errori tipici L’assegnazione di un valore a un puntatore  è permessa, ma è sconsigliata. int *px; px = 5;  /*  L’accesso a quella cella potrebbe essere vietato!  */ S ottrarre o confrontare puntatori di tipo diverso. Unica eccezione: uso di puntatori  void*  che sono considerati come dei puntatori “jolly”. Usare l’aritmetica dei puntatori su puntatori che non si riferiscono ad un vettore. Gli operatori relazionali (<,>, etc.) hanno senso solo se riferiti ad un “oggetto” comune.
Aritmetica dei puntatori Le operazioni sui puntatori non avvengono secondo l’aritmetica intera  ma dipendono dal tipo a cui si fa riferimento . Incremento/decremento di un puntatore: Il risultato è ottenuto moltiplicando il valore dell’incremento o decremento per la dimensione (numero di byte) del tipo cui si fa riferimento. Esempio: int *px; /*  assumiamo che px sia uguale a 1000  */ px += 3; px  non vale 1003, bens ì: 1000 + 3*sizeof(int) = 1006
Aritmetica dei puntatori Sottrazione tra puntatori: Il risultato corrisponde al numero di elementi del tipo cui si fa riferimento compresi tra i due puntatori. Esempio: int *px, *py, diff; /* assumiamo che px sia uguale a 1012 e py uguale a 1000  */ diff = px – py; diff  non vale 12, bens ì: (1012-1000)/sizeof(int) = 6
Aritmetica dei puntatori: Esempi long *p1, *p2; int j; char *p3;  p2 = p1 + 4;   /*  legittimo: p2 = p1 + (4*sizeof(long))  */ j = p2 - p1;   /*  a j viene assegnato 4 */ j = p1 - p2;   /* a j viene assegnato    4 */ p1 = p2 - 2;   /* OK, i tipi dei puntatori sono compatibili */ p3 = p1 - 1;   /*  NO i tipi dei puntatori sono diversi  */ j = p1 - p3;   /*  NO i tipi dei puntatori sono diversi  */ NOTA :   l’aritmetica dei puntatori dipende dalla macchina Quanti byte occupa un puntatore? TurboC    puntatore = 2 byte
Puntatori e vettori La relazione tra puntatori e vettori in C  è molto stretta.   Il nome di un vettore rappresenta l’indirizzo del primo elemento del vettore stesso! Esempio:  int  a[8],  *aptr; L’assegnazione  aptr = a;  fa puntare  aptr  al  primo elemento del vettore  a  (cioè a[0]). a[0]  a[1]  a[2]  a[3]  a[4]  a[5]  a[6]  a[7] aptr a aptr = a ..... .....
Puntatori e vettori: analogie L’analogia tra puntatori e vettori si basa sul fatto che, dato un vettore  a[ ] a[i]  e  *(aptr+i)  sono la stessa cosa! Pertanto ci sono almeno due modi per accedere al generico elemento  i  del vettore: Usando il nome del vettore (tramite indice) Usando un puntatore (tramite scostamento o  offset ) Interpretazione: aptr + i  = indirizzo dell’elemento che si trova  i  posizioni dall’inizio *(aptr + i)  = contenuto di questo elemento
Puntatori e vettori: analogie Esempio: il programma che segue riempie un vettore di 5 elementi leggendone i valori da tastiera e successivamente lo visualizza due volte; la prima in modo tradizionale, la seconda utilizzando un puntatore  a_ptr. #include <stdio.h> #include <stdlib.h> int main() { int a[5], *a_ptr; int i;
Puntatori e vettori: analogie for (i = 0; i < 5; i++) {  /* legge i valori da inserire nel vettore  */ printf (&quot;\nInserisci a[%d] = &quot;, i); scanf (&quot;%d&quot;, &a[i]); } /*  visualizza il vettore  */ for (i = 0; i < 5; i++)  /* in modo “normale”  */ printf (&quot;\nValore di a[%d] = %d&quot;, i, a[i]); a_ptr = a; for (i = 0; i < 5; i++)  /* utilizzando un puntatore (modo A) */ printf (&quot;\nValore di a[%d] = %d&quot;, i, a_ptr[i]); for (i = 0; i < 5; i++)  /* utilizzando un puntatore  (modo B) */ printf (&quot;\nValore di a[%d] = %d&quot;, i, *(a_ptr+i)); }
Puntatori e stringhe Stringa è un vettore di  char  terminato da ’ \0 ‘. La relazione tra puntatori e stringhe è sostanzialmente identica a quella con vettori generici. Differenza: char *p = “abcd”; char s[5] = “abcd”; ‘ a’ ‘ b’ ‘ c’ ‘ d’ ‘ \0’ s 1000 1001 1002 1003 1004 p 2200 2201 ... 6400 6401 6402 6403 6405 6400 ‘ a’ ‘ b’ ‘ c’ ‘ d’ ‘ \0’
Puntatori e  struct Il C fornisce uno speciale operatore ( l’operatore  freccia:  -> )   per accedere ai membri di una struttura tramite un puntatore alla struttura stessa: Utile per passaggio di  struct  per indirizzo! Esempio: struct complex  {  double re;  double im; }  complesso,  *pc;
Puntatori e  struct Come abbiamo visto a suo tempo si accede ai campi con  complesso.re,  complesso.im  Utilizzando il puntatore alla struttura, agli stessi campi si accede con pc->re,  pc->im naturalmente dopo aver assegnato l’indirizzo di  complesso  a  pc ! pc = &complesso; NOTA: come già detto a proposito dei vettori,  pc->re  e  (*pc).re  sono equivalenti.
Puntatori e  struct:  Esempio struct complex  {  double re;  double im; };  typedef struct complex complesso; /*  Funzione add_cplx: somma due numeri complessi: a = b + c  */ void add_cplx  (complesso *a, complesso *b, complesso *c) { a->re = b->re + c->re; a->im = b->im + c->im;   }
Gestione della memoria Fino ad ora abbiamo sempre dichiarato variabili di  dimensioni note,  e la cui  durata in vita  è vincolata alle   regole di esecuzione di un programma scritto in linguaggio C. Ad esempio una variabile dichiarata nel  main  vive per tutta la durata del programma; una variabile dichiarata in una funzione vive solo per la durata dell’esecuzione della funzione, se la funzione viene richiamata un’altra volta la stessa variabile di prima viene creata di nuovo e vivrà solo per la durata di quell’esecuzione. Le variabili così definite, siano esse variabili semplici oppure vettori oppure ancora altre strutture dati, si dicono  variabili statiche .
Gestione della memoria Per usare, ad esempio, un vettore, è necessario specificarne la dimensione in fase di definizione. La dichiarazione, per esempio, int peso [100]; alloca un vettore di cento interi, che, a seconda di  dove  avviene la definizione, vive per la durata della funzione in cui viene definito ( variabile automatica ) o per tutta la durata del programma. Questo implica che si conosca in anticipo e con precisione, (a  compile-time , si dice, o in pratica quando si scrive il programma) il numero dei dati da trattare.
Gestione della memoria E se invece non se ne conosce la dimensione? In pratica si ricorre alla  sovra-allocazione  : si  dimensiona la variabile (ovvero il vettore) in maniera tale da contenere il più grande numero immaginabile di dati in ingresso. Esempio #1: acquisendo linee di testo da tastiera, si può ipotizzare (ma è un ipotesi come un'altra!) che le linee non superino mai i 100 caratteri.  E se una linea supera quel limite?   Si può al più mandare un messaggio d'errore all'utente ma gestire quell’errore all’interno del programma è quasi sempre complicato.
Gestione della memoria Esempio #2: si supponga di dover leggere e registrare in memoria (ad esempio, in un vettore) dati da un file di dimensioni ignote; si può ipotizzare (ma è un ipotesi come un'altra!) che i dati non superino un numero molto grande, per esempio un milione. Questo caso è ancora più grave di quello prospettato nell’esempio #1, in quanto questa situazione anomala non è praticamente mai gestibile nel programma e anche l’eventuale segnalazione all’utente del problema non conduce a nessuna contromisura efficace. Certo, si possono scrivere messaggi di errore a schermo, ma di fatto è un fallimento del programma di fronte a dati  perfettamente legittimi .
Gestione della memoria Cosa si può fare? Per risolvere il problema molti linguaggi ad alto livello consentono di  allocare memoria secondo il bisogno che emerge al momento dell'esecuzione del programma. Questa opportunità è nota col nome di  allocazione dinamica della memoria La dimensione della variabile  viene decisa a run-time (cioè, al momento dell'esecuzione del programma)  a seconda del numero di dati in ingresso.
Gestione della memoria Esempio: leggendo dei record da un file posso richiedere, prima di ogni lettura, l’allocazione di uno spazio di memoria grande a sufficienza per contenere i dati che leggerò. … while (!feof(fp))  { alloca memoria per un record; fscanf (fp, …);  /*  leggi il record  */ … } Ad ogni ciclo si incrementa lo spazio di memoria riservato alla mia base dati!
Memoria dinamica Come funziona il meccanismo? Tramite puntatori! Ogni volta che richiedo una nuova area di memoria mi viene restituito un puntatore all’area di memoria che mi è stata assegnata. ecc. ptr 1 record 1 record 2 ptr 2
Memoria dinamica Resta ancora da definire quanto grande deve essere lo spazio di memoria che deve essere richiesto. Dal momento che tutti gli  oggetti di un certo tipo occupano esattamente lo stesso spazio di memoria , possiamo determinare con precisione lo spazio di memoria (il numero di byte) necessario a ospitare i dati. Per conoscerne con precisione la quantità basta usare l'operatore  sizeof  il quale restituisce il numero di byte che occupa il tipo di dato, o la struttura dati, indicata quale parametro tra le parentesi.
Memoria dinamica L'operatore  sizeof  può agire o su un tipo, ad esempio,  sizeof (double) o su una variabile, un vettore, un’espressione, una  struct , ad esempio, sizeof ( vettore_dati ) in ogni caso restituisce un intero corrispondente al numero di byte occupato dall’argomento.  Per definizione,  sizeof  applicato al tipo  char  restituisce 1. Non può essere applicato a vettori incompleti (senza indicazione della dimensione), a funzioni, o al tipo  void .
Memoria dinamica Ecco alcuni esempi di applicazione di  sizeof  nell’ipotesi che gli oggetti di tipo  short  occupino 2 byte e quelli di tipo  long  4 byte. sizeof (char) 1  sizeof (long int) 4 short s;  ...sizeof (s) 2 short s;  ...sizeof (s + 24) 2 (il risultato dell’addizione è di tipo int) ! long int vett[10];  ...sizeof (vett) 40 double num_real;  ... sizeof (num_real) 8 double vett_r [20];  ... sizeof (vett_r) 160
Memoria dinamica Programma di esempio per determinare le dimensioni dei principali tipi di dati C: #include <stdio.h> int main()  { printf (&quot;\t Dimensione dei tipi:\n&quot;); printf (&quot;char \t short \t int \t long \t float \t double \n&quot;); printf (&quot;%3d \t %3d \t %3d \t %3d \t %3d \t %3d\n&quot;, sizeof (char),  sizeof (short), sizeof (int), sizeof (long), sizeof (float), sizeof (double) ); }
Memoria dinamica Quando  sizeof  è applicato ad un' espressione, l'espressione è analizzata al momento della compilazione per determinare lo spazio occupato, ma non è valutata. Per esempio, l'esecuzione di: sizeof (j++) non provocherà l’incremento di  j , ma verrà valutata solamente la dimensione in byte del risultato.
Memoria dinamica Per ottenere l’allocazione di spazio in memoria sono disponibili delle funzioni di libreria. Permettono ad un programma di richiedere anche ripetutamente l'allocazione di una regione “ fresca  ” di memoria e di deallocarla più tardi (liberarla) se tale regione non serve più.  Regioni esplicitamente deallocate sono riciclate dallo  storage manager  del sistema operativo per soddisfare ulteriori richieste del programma corrente o di un qualunque altro programma che faccia richiesta di allocazione di memoria.
Memoria dinamica Quando un programma richiede l’allocazione di un certo spazio di memoria, il sistema verifica l’esistenza di uno spazio di celle contigue disponibile pari a quello richiesto e, se lo trova, lo occupa e ne restituisce l’indirizzo al chiamante ( puntatore alla memoria ). Questo puntatore sarà di tipo  void * , ma è garantito che sia adeguatamente allineato per qualsiasi tipo di dati. E’ necessario che il chiamante faccia un  cast   per convertirlo nell’opportuno tipo di puntatore ( int ,  double , ecc.). Le funzioni sono dichiarate nell'header file  <stdlib.h> .
Funzione malloc La creazione di una variabile dinamica avviene prelevando dalla memoria un blocco di dimensione opportuna tramite una funzione di allocazione ( malloc ). Prototipo della funzione: void  *malloc (unsigned int  dimensione ); La funzione restituisce un puntatore a  void  (puntatore a memoria non strutturata) oppure il valore  NULL  se l'operazione di allocazione non ha avuto successo (ad esempio per mancanza di memoria disponibile). Per utilizzare correttamente l'area allocata occorre effettuare un'operazione di  cast  del puntatore verso il tipo di dato a cui l’area è destinata.
Funzione malloc Esempio : … int dimension = 50; char *stringa; … stringa = (char *) malloc (dimension * sizeof (char)); if (stringa == NULL) { printf ( “Errore: memoria non disponibile\n”); return ; } strcpy (stringa, “questa e’ una stringa”); printf (“%s\n”, stringa); …
Memoria dinamica Una prima soluzione al problema della definizione della lunghezza dei vettori ignota al momento della scrittura del programma può pertanto trovare soluzione, purché l’utente sia in grado di fornirla all’avvio del programma. Esempio: programma che richiede il numero di elementi interi da leggere, alloca lo spazio in memoria, legge gli elementi e infine li visualizza. Simile a: programma che richiede il numero di elementi interi da leggere, li legge e li salva in un vettore e infine li visualizza.
Memoria dinamica #include <stdio.h> #include <stdlib.h> int main() { int *punt, i, num_el; /*  richiede il numero di elementi del vettore  */ printf (&quot;\nNumero di elementi da leggere: &quot;); scanf (&quot;%d&quot;, &num_el); /*  richiede l’allocazione di memoria sufficiente a contenere i dati pari a num_el * il numero di byte occupato da ogni elemento  */ punt = (int *) malloc (num_el * sizeof (int));
Memoria dinamica if (punt == NULL) { printf ( “Errore: memoria non disponibile\n”); return; } /* legge num_el elementi da tastiera  e li salva nello spazio allocato */ for (i = 0; i < num_el; i++) { printf (&quot;\nInserisci l'elemento di indice %d = &quot;, i); scanf (&quot;%d&quot;,  &punt[i] ); } /* visualizza il vettore  */ for (i = 0; i < num_el; i++) printf (&quot;\nElemento %d = %d&quot;, i,  punt[i] );
Memoria dinamica /*  Utilizzando la definizione di puntatore la lettura e visualizzazione dei dati può essere effettuata anche nel modo seguente  */ /* legge num_el elementi da tastiera  e li salva nello spazio allocato */ for (i = 0; i < num_el; i++) { printf (&quot;\nInserisci l'elemento di indice %d = &quot;, i); scanf (&quot;%d&quot;,  (punt + i) ); } /* visualizza il vettore  */ for (i = 0; i < num_el; i++) printf (&quot;\nElemento %d = %d&quot;, i,  *(punt + i) ); }
Funzione free Per liberare la memoria allocata dinamicamente, si utilizza la funzione  free . Il blocco di memoria deallocato viene restituito allo  storage manager  del sistema operativo e potrà essere riutilizzato successivamente dai programmi che faranno richiesta di memoria. Prototipo: void free (void *punt) il parametro della funzione è il puntatore all’area di memoria allocata in precedenza tramite  malloc .
Esempio Si realizzi un programma che legga da un file la classifica di un campionato di calcio. Il numero di squadre è scritto nella prima riga del file. Successivamente il programma dovrà visualizzare la classifica sul monitor. Esempio del file di input 8 Moncalieri 66 Nichelino 62 Ivrea 60 Settimo 58 Borgaro 56 Carmagnola 54 Orbassano 52 Rivoli 50
Esempio Analisi: per memorizzare le squadre e il relativo punteggio useremo una  struct  con due campi: nome della squadra (stringa) e punti ( int ). Il programma dovrà richiedere il nome del file, aprirlo e leggere il primo valore, ovvero il numero di squadre che seguono. In base a questo numero richiederà l’allocazione del numero di byte necessario per memorizzare l’intero file ovvero tutte le squadre e il loro punteggio. Il numero di byte da richiedere sarà uguale a: num. squadre * sizeof (struttura con i due campi)
Esempio main() { typedef struct { char nome_sq[30]; int punti; } classifica; classifica *vett_class;  /*  puntatore alla struttura  */ char nomefile[30]; int num_squadre, i; FILE *filedati;
Esempio /*  richiede il nome del file contenente la classifica  */ printf (&quot;\nIntroduci il nome del file: &quot;); scanf (&quot;%s&quot;, nomefile); /*  apre il file  */ if ((filedati = fopen (nomefile,&quot;r&quot;)) == NULL) {   printf (&quot;Errore apertura: %s\n&quot;, nomefile);   exit (0); } /*  legge il primo dato, ovvero il numero di squadre che seguono  */ fscanf (filedati, &quot;%d&quot;, &num_squadre); /*  richiede l’allocazione della memoria necessaria per memorizzare il resto del file */ vett_class = (classifica *) malloc (num_squadre * sizeof (classifica));
Esempio if (vett_class == NULL) { printf (&quot;Errore allocazione memoria\n&quot;); exit (1); } /*  legge il file e lo memorizza nel vettore dinamico  */ for (i = 0 ; i < num_squadre ; i++) fscanf (filedati, &quot;%s%d&quot;, &vett_class[i].nome_sq, &vett_class[i].punti); fclose (filedati); /*  Visualizza sul monitor il vettore dinamico  */ printf (&quot;\n\nSquadra  Punti\n\n&quot;); for (i = 0; i < num_squadre; i++) printf(&quot;%-12s  %d\n&quot;, vett_class[i].nome_sq, vett_class[i].punti); /*  rilascia la memoria  */ free (vett_class); }
Memoria dinamica Spesso succede però che la quantità di memoria necessaria per memorizzare i dati non è nota neppure nel momento in cui inizia l’esecuzione del programma. Tuttavia la quantità di memoria che un programma può allocare dipende solo dalla memoria disponibile.  Per risolvere definitivamente il problema bisogna pensare di costruire strutture dati che “crescono” durante l’esecuzione del programma e quindi capaci di adattarsi alle necessità man mano che queste emergono.
Strutture dati complesse Mettendo insieme la versatilità delle  struct  e la possibilità di richiedere quanta memoria si desidera (nell’ambito della memoria disponibile) si riesce a costruire delle strutture dati assai flessibili e complesse. Non essendoci particolari limiti alla definizione di un campo di una  struct  possiamo pensare di definirlo di tipo puntatore, in particolare puntatore a una  struct  dello stesso tipo. Se in quel campo inseriamo l’indirizzo di un’altra struttura possiamo creare una struttura dati, nota come  lista concatenata , simile a un vettore e che può crescere al bisogno fino al limite della memoria disponibile
Strutture dati complesse Supponiamo ad esempio di dover leggere da un file una sequenza di numeri interi la cui dimensione non è nota al momento della scrittura del programma. Si potrebbe pensare a una lista concatenata utilizzando una  struct  con due campi: un campo  int  per il valore letto dal file e un campo  puntatore  per concatenarsi al valore successivo. La struct sarà del tipo: struct  { int dato; void *punt; }
Strutture dati complesse Ad ogni lettura dal file scriveremo il valore del dato nel primo campo (quello  int ) utilizzando poi il campo puntatore per inserire l’indirizzo della prossima area di tipo  struct  che richiederemo con la  malloc  e destinata a contenere il prossimo dato. Si ottiene una struttura di questo tipo: dato 1 dato 2 dato 3 dato n-1 dato n testa punt punt punt punt NULL
Strutture dati complesse La lista concatenata è una struttura sequenziale: per leggere l’ennesimo dato bisogna scorrere gli  n-1  dati che precedono.  La lunghezza della lista è limitata solo dalla memoria a disposizione. Poiché non c’è limite al numero di campi di una struttura, si può pensare a tutta una serie di strutture dati in cui l’unico limite è la fantasia del programmatore. Naturalmente esistono delle strutture dati “ canoniche ” che vengono regolarmente utilizzate nei programmi e che si chiamano  stack ,  code ,  alberi , ecc.
Strutture dati complesse Ad esempio pensando a una  struct  con due campi puntatori potremo costruire un albero di questo tipo: d1 d2 d3 d4 d5 d6 d7 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 ecc.

13 Puntatori E Memoria Dinamica

  • 1.
    INFORMATICA Puntatori ememoria dinamica
  • 2.
    Puntatori I puntatori sono variabili che contengono indirizzi di memoria. Sono specificati tramite l’operatore unario ‘ * ’ specificato accanto al nome della variabile. Sintassi: <tipo> ‘*’ <identificatore> Per ogni tipo di variabile “ puntata ”, esiste un “ tipo ” diverso di puntatore: Esempi: int x;  int *px; /* puntatore a un intero breve */ double y;  double *py; /* puntatore a un double */
  • 3.
    Puntatori: esempio int x=5, *px; … px = &x; dopo quel frammento di programma px contiene l’indirizzo di x (cioè 10016) . Pertanto x e *px sono la stessa cosa! Memoria 5 x px Indirizzi 10016 28104 10016
  • 4.
    Puntatori Gli operatori ‘ * ’ e ‘ & ’ sono uno l’inverso dell’altro L’applicazione consecutiva in qualunque ordine ad un puntatore fornisce lo stesso risultato *&px e &*px sono la stessa cosa ! In termini di operatori: ‘ * ’= operatore di indirezione Opera su un indirizzo Ritorna il valore contenuto in quell’indirizzo ‘ & ’ = operatore di indirizzo Opera su una variabile Ritorna l’indirizzo di una variabile Quindi: *(&px): il contenuto della cella che contiene l’indirizzo di px &(*px): l’indirizzo della variabile puntata da px
  • 5.
    Puntatori Nelle istruzionidi I/O per i puntatori è definito un apposito descrittore di campo, %p , il quale converte in un intero esadecimale la variabile a cui è riferito. Questo descrittore può essere usato sia con la scanf che con la printf , tuttavia la sua utilità in input è riservata a casi particolarissimi di accesso diretto a specifiche locazioni di memoria. In output invece può essere utilizzato per visualizzare l’indirizzo di memoria delle variabili.
  • 6.
    Puntatori Esempio: #include<stdio.h> #include <stdlib.h> int main() { int a, *aptr; a = 256; aptr = &a; printf (“\nL'indirizzo di a è %p&quot;, &a); /* indirizzo di a */ printf (&quot;\nIl valore di aptr è %p&quot;, aptr); /* indirizzo di a */ printf (&quot;\nIl valore di a è %p&quot;, a); /* valore di a in Hex. */ printf (&quot;\nIl valore di *aptr è %p&quot;, *aptr); /* valore di a in Hex*/ printf (&quot;\n&*aptr = %p ----- *&aptr = %p&quot;, &*aptr, *&aptr); /* indirizzo di a */ }
  • 7.
    Operazioni sui puntatoriSui puntatori sono definite solo alcune operazioni: Incremento/decremento int *px; px++; px--; /* sono operazioni lecite */ Somma/sottrazione di un valore intero int *px px += 3; px -= 5; /* sono operazioni lecite */ Assegnazione di un puntatore ad un altro dello stesso tipo int *px, *py; px = py; /* è un operazione lecita */
  • 8.
    Operazioni sui puntatoriSottrazione tra due puntatori (NON la somma) dello stesso tipo double *px, *py; px – py /* è un operazione lecita e produce un risultato intero (quanti elementi ci sono tra i due indirizzi) */ Confronto tra puntatori dello stesso tipo int *px, *py; px == py px != py px > py /* sono operazione lecite */ Confronto tipico: px == NULL
  • 9.
    Operazioni sui puntatori: errori tipici L’assegnazione di un valore a un puntatore è permessa, ma è sconsigliata. int *px; px = 5; /* L’accesso a quella cella potrebbe essere vietato! */ S ottrarre o confrontare puntatori di tipo diverso. Unica eccezione: uso di puntatori void* che sono considerati come dei puntatori “jolly”. Usare l’aritmetica dei puntatori su puntatori che non si riferiscono ad un vettore. Gli operatori relazionali (<,>, etc.) hanno senso solo se riferiti ad un “oggetto” comune.
  • 10.
    Aritmetica dei puntatoriLe operazioni sui puntatori non avvengono secondo l’aritmetica intera ma dipendono dal tipo a cui si fa riferimento . Incremento/decremento di un puntatore: Il risultato è ottenuto moltiplicando il valore dell’incremento o decremento per la dimensione (numero di byte) del tipo cui si fa riferimento. Esempio: int *px; /* assumiamo che px sia uguale a 1000 */ px += 3; px non vale 1003, bens ì: 1000 + 3*sizeof(int) = 1006
  • 11.
    Aritmetica dei puntatoriSottrazione tra puntatori: Il risultato corrisponde al numero di elementi del tipo cui si fa riferimento compresi tra i due puntatori. Esempio: int *px, *py, diff; /* assumiamo che px sia uguale a 1012 e py uguale a 1000 */ diff = px – py; diff non vale 12, bens ì: (1012-1000)/sizeof(int) = 6
  • 12.
    Aritmetica dei puntatori:Esempi long *p1, *p2; int j; char *p3; p2 = p1 + 4; /* legittimo: p2 = p1 + (4*sizeof(long)) */ j = p2 - p1; /* a j viene assegnato 4 */ j = p1 - p2; /* a j viene assegnato  4 */ p1 = p2 - 2; /* OK, i tipi dei puntatori sono compatibili */ p3 = p1 - 1; /* NO i tipi dei puntatori sono diversi */ j = p1 - p3; /* NO i tipi dei puntatori sono diversi */ NOTA : l’aritmetica dei puntatori dipende dalla macchina Quanti byte occupa un puntatore? TurboC  puntatore = 2 byte
  • 13.
    Puntatori e vettoriLa relazione tra puntatori e vettori in C è molto stretta. Il nome di un vettore rappresenta l’indirizzo del primo elemento del vettore stesso! Esempio: int a[8], *aptr; L’assegnazione aptr = a; fa puntare aptr al primo elemento del vettore a (cioè a[0]). a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] aptr a aptr = a ..... .....
  • 14.
    Puntatori e vettori:analogie L’analogia tra puntatori e vettori si basa sul fatto che, dato un vettore a[ ] a[i] e *(aptr+i) sono la stessa cosa! Pertanto ci sono almeno due modi per accedere al generico elemento i del vettore: Usando il nome del vettore (tramite indice) Usando un puntatore (tramite scostamento o offset ) Interpretazione: aptr + i = indirizzo dell’elemento che si trova i posizioni dall’inizio *(aptr + i) = contenuto di questo elemento
  • 15.
    Puntatori e vettori:analogie Esempio: il programma che segue riempie un vettore di 5 elementi leggendone i valori da tastiera e successivamente lo visualizza due volte; la prima in modo tradizionale, la seconda utilizzando un puntatore a_ptr. #include <stdio.h> #include <stdlib.h> int main() { int a[5], *a_ptr; int i;
  • 16.
    Puntatori e vettori:analogie for (i = 0; i < 5; i++) { /* legge i valori da inserire nel vettore */ printf (&quot;\nInserisci a[%d] = &quot;, i); scanf (&quot;%d&quot;, &a[i]); } /* visualizza il vettore */ for (i = 0; i < 5; i++) /* in modo “normale” */ printf (&quot;\nValore di a[%d] = %d&quot;, i, a[i]); a_ptr = a; for (i = 0; i < 5; i++) /* utilizzando un puntatore (modo A) */ printf (&quot;\nValore di a[%d] = %d&quot;, i, a_ptr[i]); for (i = 0; i < 5; i++) /* utilizzando un puntatore (modo B) */ printf (&quot;\nValore di a[%d] = %d&quot;, i, *(a_ptr+i)); }
  • 17.
    Puntatori e stringheStringa è un vettore di char terminato da ’ \0 ‘. La relazione tra puntatori e stringhe è sostanzialmente identica a quella con vettori generici. Differenza: char *p = “abcd”; char s[5] = “abcd”; ‘ a’ ‘ b’ ‘ c’ ‘ d’ ‘ \0’ s 1000 1001 1002 1003 1004 p 2200 2201 ... 6400 6401 6402 6403 6405 6400 ‘ a’ ‘ b’ ‘ c’ ‘ d’ ‘ \0’
  • 18.
    Puntatori e struct Il C fornisce uno speciale operatore ( l’operatore freccia: -> ) per accedere ai membri di una struttura tramite un puntatore alla struttura stessa: Utile per passaggio di struct per indirizzo! Esempio: struct complex { double re; double im; } complesso, *pc;
  • 19.
    Puntatori e struct Come abbiamo visto a suo tempo si accede ai campi con complesso.re, complesso.im Utilizzando il puntatore alla struttura, agli stessi campi si accede con pc->re, pc->im naturalmente dopo aver assegnato l’indirizzo di complesso a pc ! pc = &complesso; NOTA: come già detto a proposito dei vettori, pc->re e (*pc).re sono equivalenti.
  • 20.
    Puntatori e struct: Esempio struct complex { double re; double im; }; typedef struct complex complesso; /* Funzione add_cplx: somma due numeri complessi: a = b + c */ void add_cplx (complesso *a, complesso *b, complesso *c) { a->re = b->re + c->re; a->im = b->im + c->im; }
  • 21.
    Gestione della memoriaFino ad ora abbiamo sempre dichiarato variabili di dimensioni note, e la cui durata in vita è vincolata alle regole di esecuzione di un programma scritto in linguaggio C. Ad esempio una variabile dichiarata nel main vive per tutta la durata del programma; una variabile dichiarata in una funzione vive solo per la durata dell’esecuzione della funzione, se la funzione viene richiamata un’altra volta la stessa variabile di prima viene creata di nuovo e vivrà solo per la durata di quell’esecuzione. Le variabili così definite, siano esse variabili semplici oppure vettori oppure ancora altre strutture dati, si dicono variabili statiche .
  • 22.
    Gestione della memoriaPer usare, ad esempio, un vettore, è necessario specificarne la dimensione in fase di definizione. La dichiarazione, per esempio, int peso [100]; alloca un vettore di cento interi, che, a seconda di dove avviene la definizione, vive per la durata della funzione in cui viene definito ( variabile automatica ) o per tutta la durata del programma. Questo implica che si conosca in anticipo e con precisione, (a compile-time , si dice, o in pratica quando si scrive il programma) il numero dei dati da trattare.
  • 23.
    Gestione della memoriaE se invece non se ne conosce la dimensione? In pratica si ricorre alla sovra-allocazione : si dimensiona la variabile (ovvero il vettore) in maniera tale da contenere il più grande numero immaginabile di dati in ingresso. Esempio #1: acquisendo linee di testo da tastiera, si può ipotizzare (ma è un ipotesi come un'altra!) che le linee non superino mai i 100 caratteri. E se una linea supera quel limite? Si può al più mandare un messaggio d'errore all'utente ma gestire quell’errore all’interno del programma è quasi sempre complicato.
  • 24.
    Gestione della memoriaEsempio #2: si supponga di dover leggere e registrare in memoria (ad esempio, in un vettore) dati da un file di dimensioni ignote; si può ipotizzare (ma è un ipotesi come un'altra!) che i dati non superino un numero molto grande, per esempio un milione. Questo caso è ancora più grave di quello prospettato nell’esempio #1, in quanto questa situazione anomala non è praticamente mai gestibile nel programma e anche l’eventuale segnalazione all’utente del problema non conduce a nessuna contromisura efficace. Certo, si possono scrivere messaggi di errore a schermo, ma di fatto è un fallimento del programma di fronte a dati perfettamente legittimi .
  • 25.
    Gestione della memoriaCosa si può fare? Per risolvere il problema molti linguaggi ad alto livello consentono di allocare memoria secondo il bisogno che emerge al momento dell'esecuzione del programma. Questa opportunità è nota col nome di allocazione dinamica della memoria La dimensione della variabile viene decisa a run-time (cioè, al momento dell'esecuzione del programma) a seconda del numero di dati in ingresso.
  • 26.
    Gestione della memoriaEsempio: leggendo dei record da un file posso richiedere, prima di ogni lettura, l’allocazione di uno spazio di memoria grande a sufficienza per contenere i dati che leggerò. … while (!feof(fp)) { alloca memoria per un record; fscanf (fp, …); /* leggi il record */ … } Ad ogni ciclo si incrementa lo spazio di memoria riservato alla mia base dati!
  • 27.
    Memoria dinamica Comefunziona il meccanismo? Tramite puntatori! Ogni volta che richiedo una nuova area di memoria mi viene restituito un puntatore all’area di memoria che mi è stata assegnata. ecc. ptr 1 record 1 record 2 ptr 2
  • 28.
    Memoria dinamica Restaancora da definire quanto grande deve essere lo spazio di memoria che deve essere richiesto. Dal momento che tutti gli oggetti di un certo tipo occupano esattamente lo stesso spazio di memoria , possiamo determinare con precisione lo spazio di memoria (il numero di byte) necessario a ospitare i dati. Per conoscerne con precisione la quantità basta usare l'operatore sizeof il quale restituisce il numero di byte che occupa il tipo di dato, o la struttura dati, indicata quale parametro tra le parentesi.
  • 29.
    Memoria dinamica L'operatore sizeof può agire o su un tipo, ad esempio, sizeof (double) o su una variabile, un vettore, un’espressione, una struct , ad esempio, sizeof ( vettore_dati ) in ogni caso restituisce un intero corrispondente al numero di byte occupato dall’argomento. Per definizione, sizeof applicato al tipo char restituisce 1. Non può essere applicato a vettori incompleti (senza indicazione della dimensione), a funzioni, o al tipo void .
  • 30.
    Memoria dinamica Eccoalcuni esempi di applicazione di sizeof nell’ipotesi che gli oggetti di tipo short occupino 2 byte e quelli di tipo long 4 byte. sizeof (char) 1 sizeof (long int) 4 short s; ...sizeof (s) 2 short s; ...sizeof (s + 24) 2 (il risultato dell’addizione è di tipo int) ! long int vett[10]; ...sizeof (vett) 40 double num_real; ... sizeof (num_real) 8 double vett_r [20]; ... sizeof (vett_r) 160
  • 31.
    Memoria dinamica Programmadi esempio per determinare le dimensioni dei principali tipi di dati C: #include <stdio.h> int main() { printf (&quot;\t Dimensione dei tipi:\n&quot;); printf (&quot;char \t short \t int \t long \t float \t double \n&quot;); printf (&quot;%3d \t %3d \t %3d \t %3d \t %3d \t %3d\n&quot;, sizeof (char), sizeof (short), sizeof (int), sizeof (long), sizeof (float), sizeof (double) ); }
  • 32.
    Memoria dinamica Quando sizeof è applicato ad un' espressione, l'espressione è analizzata al momento della compilazione per determinare lo spazio occupato, ma non è valutata. Per esempio, l'esecuzione di: sizeof (j++) non provocherà l’incremento di j , ma verrà valutata solamente la dimensione in byte del risultato.
  • 33.
    Memoria dinamica Perottenere l’allocazione di spazio in memoria sono disponibili delle funzioni di libreria. Permettono ad un programma di richiedere anche ripetutamente l'allocazione di una regione “ fresca ” di memoria e di deallocarla più tardi (liberarla) se tale regione non serve più. Regioni esplicitamente deallocate sono riciclate dallo storage manager del sistema operativo per soddisfare ulteriori richieste del programma corrente o di un qualunque altro programma che faccia richiesta di allocazione di memoria.
  • 34.
    Memoria dinamica Quandoun programma richiede l’allocazione di un certo spazio di memoria, il sistema verifica l’esistenza di uno spazio di celle contigue disponibile pari a quello richiesto e, se lo trova, lo occupa e ne restituisce l’indirizzo al chiamante ( puntatore alla memoria ). Questo puntatore sarà di tipo void * , ma è garantito che sia adeguatamente allineato per qualsiasi tipo di dati. E’ necessario che il chiamante faccia un cast per convertirlo nell’opportuno tipo di puntatore ( int , double , ecc.). Le funzioni sono dichiarate nell'header file <stdlib.h> .
  • 35.
    Funzione malloc Lacreazione di una variabile dinamica avviene prelevando dalla memoria un blocco di dimensione opportuna tramite una funzione di allocazione ( malloc ). Prototipo della funzione: void *malloc (unsigned int dimensione ); La funzione restituisce un puntatore a void (puntatore a memoria non strutturata) oppure il valore NULL se l'operazione di allocazione non ha avuto successo (ad esempio per mancanza di memoria disponibile). Per utilizzare correttamente l'area allocata occorre effettuare un'operazione di cast del puntatore verso il tipo di dato a cui l’area è destinata.
  • 36.
    Funzione malloc Esempio: … int dimension = 50; char *stringa; … stringa = (char *) malloc (dimension * sizeof (char)); if (stringa == NULL) { printf ( “Errore: memoria non disponibile\n”); return ; } strcpy (stringa, “questa e’ una stringa”); printf (“%s\n”, stringa); …
  • 37.
    Memoria dinamica Unaprima soluzione al problema della definizione della lunghezza dei vettori ignota al momento della scrittura del programma può pertanto trovare soluzione, purché l’utente sia in grado di fornirla all’avvio del programma. Esempio: programma che richiede il numero di elementi interi da leggere, alloca lo spazio in memoria, legge gli elementi e infine li visualizza. Simile a: programma che richiede il numero di elementi interi da leggere, li legge e li salva in un vettore e infine li visualizza.
  • 38.
    Memoria dinamica #include<stdio.h> #include <stdlib.h> int main() { int *punt, i, num_el; /* richiede il numero di elementi del vettore */ printf (&quot;\nNumero di elementi da leggere: &quot;); scanf (&quot;%d&quot;, &num_el); /* richiede l’allocazione di memoria sufficiente a contenere i dati pari a num_el * il numero di byte occupato da ogni elemento */ punt = (int *) malloc (num_el * sizeof (int));
  • 39.
    Memoria dinamica if(punt == NULL) { printf ( “Errore: memoria non disponibile\n”); return; } /* legge num_el elementi da tastiera e li salva nello spazio allocato */ for (i = 0; i < num_el; i++) { printf (&quot;\nInserisci l'elemento di indice %d = &quot;, i); scanf (&quot;%d&quot;, &punt[i] ); } /* visualizza il vettore */ for (i = 0; i < num_el; i++) printf (&quot;\nElemento %d = %d&quot;, i, punt[i] );
  • 40.
    Memoria dinamica /* Utilizzando la definizione di puntatore la lettura e visualizzazione dei dati può essere effettuata anche nel modo seguente */ /* legge num_el elementi da tastiera e li salva nello spazio allocato */ for (i = 0; i < num_el; i++) { printf (&quot;\nInserisci l'elemento di indice %d = &quot;, i); scanf (&quot;%d&quot;, (punt + i) ); } /* visualizza il vettore */ for (i = 0; i < num_el; i++) printf (&quot;\nElemento %d = %d&quot;, i, *(punt + i) ); }
  • 41.
    Funzione free Perliberare la memoria allocata dinamicamente, si utilizza la funzione free . Il blocco di memoria deallocato viene restituito allo storage manager del sistema operativo e potrà essere riutilizzato successivamente dai programmi che faranno richiesta di memoria. Prototipo: void free (void *punt) il parametro della funzione è il puntatore all’area di memoria allocata in precedenza tramite malloc .
  • 42.
    Esempio Si realizziun programma che legga da un file la classifica di un campionato di calcio. Il numero di squadre è scritto nella prima riga del file. Successivamente il programma dovrà visualizzare la classifica sul monitor. Esempio del file di input 8 Moncalieri 66 Nichelino 62 Ivrea 60 Settimo 58 Borgaro 56 Carmagnola 54 Orbassano 52 Rivoli 50
  • 43.
    Esempio Analisi: permemorizzare le squadre e il relativo punteggio useremo una struct con due campi: nome della squadra (stringa) e punti ( int ). Il programma dovrà richiedere il nome del file, aprirlo e leggere il primo valore, ovvero il numero di squadre che seguono. In base a questo numero richiederà l’allocazione del numero di byte necessario per memorizzare l’intero file ovvero tutte le squadre e il loro punteggio. Il numero di byte da richiedere sarà uguale a: num. squadre * sizeof (struttura con i due campi)
  • 44.
    Esempio main() {typedef struct { char nome_sq[30]; int punti; } classifica; classifica *vett_class; /* puntatore alla struttura */ char nomefile[30]; int num_squadre, i; FILE *filedati;
  • 45.
    Esempio /* richiede il nome del file contenente la classifica */ printf (&quot;\nIntroduci il nome del file: &quot;); scanf (&quot;%s&quot;, nomefile); /* apre il file */ if ((filedati = fopen (nomefile,&quot;r&quot;)) == NULL) { printf (&quot;Errore apertura: %s\n&quot;, nomefile); exit (0); } /* legge il primo dato, ovvero il numero di squadre che seguono */ fscanf (filedati, &quot;%d&quot;, &num_squadre); /* richiede l’allocazione della memoria necessaria per memorizzare il resto del file */ vett_class = (classifica *) malloc (num_squadre * sizeof (classifica));
  • 46.
    Esempio if (vett_class== NULL) { printf (&quot;Errore allocazione memoria\n&quot;); exit (1); } /* legge il file e lo memorizza nel vettore dinamico */ for (i = 0 ; i < num_squadre ; i++) fscanf (filedati, &quot;%s%d&quot;, &vett_class[i].nome_sq, &vett_class[i].punti); fclose (filedati); /* Visualizza sul monitor il vettore dinamico */ printf (&quot;\n\nSquadra Punti\n\n&quot;); for (i = 0; i < num_squadre; i++) printf(&quot;%-12s %d\n&quot;, vett_class[i].nome_sq, vett_class[i].punti); /* rilascia la memoria */ free (vett_class); }
  • 47.
    Memoria dinamica Spessosuccede però che la quantità di memoria necessaria per memorizzare i dati non è nota neppure nel momento in cui inizia l’esecuzione del programma. Tuttavia la quantità di memoria che un programma può allocare dipende solo dalla memoria disponibile. Per risolvere definitivamente il problema bisogna pensare di costruire strutture dati che “crescono” durante l’esecuzione del programma e quindi capaci di adattarsi alle necessità man mano che queste emergono.
  • 48.
    Strutture dati complesseMettendo insieme la versatilità delle struct e la possibilità di richiedere quanta memoria si desidera (nell’ambito della memoria disponibile) si riesce a costruire delle strutture dati assai flessibili e complesse. Non essendoci particolari limiti alla definizione di un campo di una struct possiamo pensare di definirlo di tipo puntatore, in particolare puntatore a una struct dello stesso tipo. Se in quel campo inseriamo l’indirizzo di un’altra struttura possiamo creare una struttura dati, nota come lista concatenata , simile a un vettore e che può crescere al bisogno fino al limite della memoria disponibile
  • 49.
    Strutture dati complesseSupponiamo ad esempio di dover leggere da un file una sequenza di numeri interi la cui dimensione non è nota al momento della scrittura del programma. Si potrebbe pensare a una lista concatenata utilizzando una struct con due campi: un campo int per il valore letto dal file e un campo puntatore per concatenarsi al valore successivo. La struct sarà del tipo: struct { int dato; void *punt; }
  • 50.
    Strutture dati complesseAd ogni lettura dal file scriveremo il valore del dato nel primo campo (quello int ) utilizzando poi il campo puntatore per inserire l’indirizzo della prossima area di tipo struct che richiederemo con la malloc e destinata a contenere il prossimo dato. Si ottiene una struttura di questo tipo: dato 1 dato 2 dato 3 dato n-1 dato n testa punt punt punt punt NULL
  • 51.
    Strutture dati complesseLa lista concatenata è una struttura sequenziale: per leggere l’ennesimo dato bisogna scorrere gli n-1 dati che precedono. La lunghezza della lista è limitata solo dalla memoria a disposizione. Poiché non c’è limite al numero di campi di una struttura, si può pensare a tutta una serie di strutture dati in cui l’unico limite è la fantasia del programmatore. Naturalmente esistono delle strutture dati “ canoniche ” che vengono regolarmente utilizzate nei programmi e che si chiamano stack , code , alberi , ecc.
  • 52.
    Strutture dati complesseAd esempio pensando a una struct con due campi puntatori potremo costruire un albero di questo tipo: d1 d2 d3 d4 d5 d6 d7 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 ecc.