Appunti tratti dal videocorso on-line di Algoritmi e Programmazione Avanzata By ALeXio 1-La memoria dinamica La scrittura di un programma (indipendentemente dal linguaggio adottato) deve sempre tener conto delle condizioni al contorno, ad esempio in termini di: o prestazioni (ossia velocità di esecuzione) o quantità di memoria richiesta o tempo per lo sviluppo o Quando un programma è in corso di esecuzione, nella memoria principale del sistema vi è un area riservata per i dati ed il codice del programma Esecuzione di un programma Inizialmente il programma è memorizzato nella memoria secondaria Quando riceve il comando di esecuzione, il sistema operativo trasferisce nella memoria principale il codice e riserva lo spazio per le strutture dati Durante l esecuzione, il processore esegue il codice ed opera sulle strutture dati Memoria dati La memoria destinata ai dati può essere suddivisa in due parti: o la memoria allocata staticamente o la memoria allocata dinamicamente Memoria allocata staticamente La memoria necessaria per le variabili globali in C resta occupata dal momento dell attivazione del programma sino al suo termine Non è possibile dimensionare tale memoria secondo le esigenze della singola attivazione Si parla quindi di allocazione statica della memoria #define MAX 1000 struct scheda{ int codice; char nome[20]; char cognome[20]; ; struct scheda vett[max]; vett è dimensionato per eccesso, in modo da poter soddisfare in ogni caso le esigenze del programma, anche se questo usa un numero minore di elementi, quindi in ogni caso vett occupa 42.000 byte Allocazione dinamica Molti linguaggi di alto livello supportano l allocazione dinamica della memoria Questo significa che il programmatore può inserire nel proprio codice delle chiamate a procedure di sistema che
o richiedono l allocazione di un area di memoria o richiedono il rilascio di un area di memoria Il programma è in grado di determinare, ogni volta che è lanciato, di quanta memoria ha bisogno Viene allora chiamata una procedura di sistema, che provvede all allocazione della memoria necessaria In tal modo si permette ad eventuali altri processi che lavorano in parallelo sullo stesso sistema di meglio utilizzare la memoria disponibile Durante l esecuzione, il programma ha bisogno di una quantità variabile di memoria Il programma usa ad ogni istante solo la memoria di cui ha bisogno, provvedendo periodicamente ad allocare o deallocare memoria Richiami sui puntatori Definizione e operatori * e & Le variabili di tipo puntatore permettono di accedere alla memoria in modo indiretto La definizione di una variabile di tipo puntatore richiede di definire il tipo della variabile a cui si punta. int *p; L operatore & permette di risalire dal nome di una variabile al suo puntatore: p = &a; L operatore * permette di accedere alla cella di memoria referenziata da una variabile puntatore: *p = 10; Operazioni sui puntatori o Assegnazione: p = q; p = NULL; o Incremento/decremento: p = p+5; p = p-10; p++; Se p è un puntatore ad intero, dopo questa istruzione p punta all'intero posto 5 interi dopo in memoria Puntatori e vettori In C il nome di una variabile di tipo vettore coincide con il puntatore al primo elemento del vettore Quindi puntatori e nomi di vettori sono intercambiabili Definizioni: int vett[max]; int *p; Inizializzazione: p = vett; p=&vett[0]; Forme equivalenti: vett[0]=10; *p=10; vett[10]=25; *(p+10)=25;
vett[i]=0; *(p+i)=0; *vett=27; p[0]=27; *(vett+3)=0; p[3]=0; Scansione e azzeramento di un vettore: int vett[n]; int *p; p=&vett[0]; for (i=0; i<n; i++) *(p++)=0; Puntatori a struct Qualora una variabile p sia di tipo puntatore a struct, ad essa è applicabile l'operatore ->: p->nome_campo (*p).nome_campo struct scheda{ int codice; char nome[20]; char cognome[20]; ; struct scheda *p; struct scheda vett[n]; p=&vett[0]; for (i=0; i<n, i++) vett[i].codice=0; Equivale a for (i=0; i<n, i++) { p->codice=0; Equivale a (*p).codice=0 p++; Procedure malloc e free Le librerie standard del linguaggio C mettono a disposizione del programmatore varie procedure per la gestione della memoria dinamica Le due principali sono o malloc per allocare memoria o free per deallocare memoria malloc Permette di allocare dinamicamente una certa quantità di memoria void *malloc (int n); malloc richiede al sistema operativo l allocazione di una zona di memoria di dimensione (in byte) pari ad n, e ritorna il puntatore all inizio della zona
void *malloc (int n); Il tipo void * corrisponde ad un puntatore ad un tipo generico malloc richiede al sistema operativo l allocazione di una zona di memoria di dimensione (in byte) pari ad n e ritorna il puntatore all inizio della zona int *punt; int n; punt = (int *)malloc(n); Richiede l allocazione di una zona di memoria di n byte Trasforma il puntatore generico ritornato da malloc in un puntatore a int if (punt == NULL) Verifica che l allocazione sia avvenuta regolarmente { printf ( Errore di allocazione\n ); exit(); Deallocazione Quando si desidera deallocare una zona di memoria si usa la procedura di sistema free: void free (void *) La procedura free rende libera la zona di memoria puntata dal parametro, che deve essere stata precedentemente allocata con una chiamata a malloc int *punt; int n; punt = (int *)malloc(n); if (punt == NULL) { printf ( Errore di allocazione\n ); exit(); free (punt); Dealloca la zona di memoria puntata da punt Allocazione dinamica di vettori Si può quindi: o allocare un vettore in modo dinamico tramite la procedura malloc o usare il puntatore ritornato per accedere al vettore, come si farebbe con il nome di un vettore o deallocare la memoria usata, quando questa non è più necessaria Si vuole scrivere una procedura alloca che: o legge da tastiera un numero n o alloca un vettore di n elementi di tipo struct scheda o inizializza ogni elemento del vettore
Procedura alloca int n; /* variabile globale */ struct scheda *alloca (void) { int i; struct scheda *p; scanf("%d", &n); p=(struct scheda *)malloc(n*sizeof(struct scheda)); L peratore sizeof ritorna il numero di byte necessari per la memorizzazione di una variabile del tipo specificato if (p==null) return (NULL); for (i=0; i<n; i++) { p[i].codice=0; strcpy(p[i].nome, ""); strcpy(p[i].cognome, ""); return (p); Allocazione dinamica di stringhe In C le stringhe sono memorizzate sotto forma di vettori di caratteri usando '\0' come carattere di fine stringa Quando si deve memorizzare una stringa di n caratteri si può quindi usare un vettore allocato staticamente di lunghezza MAX>n oppure allocare dinamicamente un vettore di lunghezza n+1 byte Si vuole scrivere una procedura leggi che legge da tastiera i dati relativi ad n schede e li memorizza nel vettore precedentemente allocato, allocando dinamicamente la memoria necessaria per i campi nome e cognome Definizione di struct scheda II o struct scheda{ int codice; char *nome; char *cognome; ; o Procedura leggi int leggi (struct scheda *p) { int i, val; char nome[max], cogn[max]; for (i=0, i<n; i++) { scanf ("%d %s %s\n", &val, nome, cogn); p[i].codice=val; p[i].nome=strdup(nome); if (p[i].nome == NULL) return (ERRORE); p[i].cogn=strdup(cogn); if (p[i].cogn == NULL) return (ERRORE); return (OK); I campi nome e cognome diventano puntatori
Procedura strdup o char *strdup (char *str) { int len; char *p; len=strlen (str); p=(char *)malloc((len+1)*sizeof(char)); if (p==null) return (NULL); strcpy (p, str); return (p); Si vuole scrivere una procedura libera, che dealloca il vettore di n strutture passato come parametro Prima di deallocare il vettore è necessario deallocare la memoria usata per le stringhe Procedura libera o void libera (struct scheda *p) { int i; for (i=0; i<n; i++) { free (p[i].nome); free (p[i].cogn); free (p); o Procedura libera (vers. 2) void libera (struct scheda *p) { int i; struct scheda *q; q=p; for (i=0; i<n; i++) { free (q->nome); free (q->cogn); q++; free (p); o Procedura libera (vers. 3) void libera (struct scheda *p) { struct scheda *q; for (q=p; q<p+n; q++) { free (q->nome); free (q->cogn); free (p); Liste Una lista è una struttura dati basata sull uso dei puntatori e delle procedure di allocazione/deallocazione dinamica della memoria Permette un maggiore flessibilità nell uso della memoria rispetto ad altre strutture dati (ad es. i vettori) al costo di una minore efficienza Una lista è una struttura dati in cui
o ogni elemento viene allocato/deallocato separatamente o ogni elemento è linkato agli altri (e quindi reso accessibile) attraverso puntatori o esiste una variabile (denominata testa) che permette di accedere al primo elemento Conseguenze o Ad ogni istante si utilizza solo la memoria corrispondente agli elementi effettivamente utilizzati o La memoria complessiva richiesta include alcune variabili aggiuntive o Per accedere a ciascun elemento è necessario percorrere la lista Lista semplice Non esiste nessun ordinamento particolare sugli elementi della lista Implementazione in C /* definizione */ struct scheda{ int codice; char *nome; char *cognome; struct scheda *succ; /* definizione testa */ struct scheda *testa=null; Operazioni possibili Le operazioni più comunemente eseguite su una lista sono o Inserimento o Ricerca o Cancellazione Inserimento in testa struct scheda *p; p=(struct scheda *)malloc(sizeof(struct scheda)); if (p==null) error(); /* gestisce la situazione di errore */ p->codice=val; p->nome=strdup(nome); if (p->nome==null) error(); /* gestisce la situazione di errore */ p->cognome=strdup(cognome); if (p->cognome==null) error(); /* gestisce la situazione di errore */ p->succ=testa; testa=p; Procedura inserisci int inserisci (struct scheda *t, int val, char *nome, char *cogn) { struct scheda *p;
p=(struct scheda *)malloc(sizeof(struct scheda)); if (p==null) return (ERRORE); p->codice=val; p->nome=strdup(nome); if (p->nome==null) return (ERRORE); p->cognome=strdup(cognome); if (p->cognome==null) return (ERRORE); p->succ=t; t=p; Il parametro t è modificato, ma il C supporta solo i parametri passati by value! return (OK); Quindi al suo termine questa versione di inserisci lascia il valore di testa invariato! QUINDI: ERRORE! Procedura inserisci int inserisci (struct scheda **t, int val, char *nome, char *cogn) La variabile testa è passata by reference, ossia la procedura riceve il puntatore alla variabile Programma chiamante struct scheda *testa; int ret, val; char nome[max], cognome[max]; scanf ("%d, %s %s\n", &val, nome, cognome); ret=inserisci (&testa, val, nome, cognome); if (ret == ERRORE) error(); Ricerca Si scandisce la lista sino a che si arriva al fondo oppure si trova l elemento cercato Ricerca struct scheda *ricerca (struct scheda *t, int val) { struct scheda *p; p=t; while (p!=null) { if (p->codice==val) return (p); p=p->succ; return (p); Programma chiamante struct scheda *testa, *p; int val;
scanf ("%d\n", &val); p=ricerca (testa, val); if (p == NULL) printf ("Elemento non trovato\n"); else printf("%d %s %s\n", p->codice, p->nome, p->cogn); Liste ordinate Se la procedura di inserimento inserisce il nuovo elemento nella posizione opportuna, la lista è mantenuta ordinata (rispetto ad un certo campo chiave) In tal modo è possibile semplificare le operazioni di ricerca, accedere agli elementi in ordine Inserimento in lista ordinata int inserisci_ord (struct scheda **t, int val, char *nome, char *cogn) { struct scheda *p, *q; /* allocazione nuovo elemento */ p = alloca( val, nome, cogn); if( p == NULL) return(errore); q = *t; /* inserimento in testa */ if( (q == NULL) (q->codice > val)) Lista vuota il nuovo elemento ha valore < del vecchio primo elemento { p->succ=*t; *t=p; return (OK); Inserimento in lista ordinata (2) /* inserimento in mezzo */ while( q->succ!= NULL) { if( q->succ->codice > val) { p->succ=q->succ; q->succ=p; return (OK); q = q->succ; /* inserimento in coda */ p->succ = NULL; q->succ = p; return(ok); Cancellazione La cancellazione di un elemento normalmente richiede o Un operazione di ricerca, che produce il puntatore all'elemento da cancellare o Un operazione di cancellazione vera e propria; questa richiede non solo il puntatore all'elemento da cancellare, ma anche quello all'elemento precedente
Cancellazione int cancella (struct scheda **t, int val) { struct scheda *p, *q; q = *t; if (q==null) /* lista vuota */ return (ERRORE); /* cancellazione in testa */ if (q->codice == val)) { free (q->nome); free (q->cogn); *t=q->succ; free (q); return (OK); Cancellazione (2) /* cancellazione in mezzo o in coda */ while( q->succ!= NULL) { if( q->succ->codice == val) { q->succ=q->succ->succ; free (q->succ->nome); free (q->succ->cogn); free (q->succ); return (0); q = q->succ; Sentinelle Può essere conveniente aggiungere alla lista degli elementi fittizi (detti sentinelle) che permettono di: o semplificare il codice per la gestione delle liste o aumentarne l efficienza (senza cambiare la complessità nel caso peggiore) Sentinelle: liste non ordinate In questo caso si usano solitamente due sentinelle (una in coda ed una in testa) Le sentinelle permettono di semplificare il codice (non si devono più considerare separatamente i casi di inserimento/cancellazione in testa e in coda) Sentinelle: liste ordinate In questo caso le due sentinelle contengono il valore minimo e quello massimo memorizzabili, in tal modo: o si semplifica il codice (non si devono più considerare separatamente i casi di inserimento/cancellazione in testa e in coda) o si rende più veloce l'esecuzione, in quanto si può eliminare il test di fine lista