I puntatori Un puntatore è una variabile che contiene un indirizzo di memoria. In questo indirizzo sta (di soliro) una variabile di una certa dimensione e di un certo tipo. Queste doppie variabili si dicono la prima puntante alla seconda. Non è facile gestire in modo corretto queste doppie variabili, e spesso anche dopo anni di esperienza questi strumenti sono causa ancora di errori. Ma l'uso dei puntatori è sicuramente indispensabile per un buon programma ed eccone un po' di ragioni: 1 - Dinamicità del programma 2 - Modifica di variabili attraverso funzioni 3 - Nuove strutture dati (vedi capitolo successivo) 4 - Si capisce subito se si è un programmatore o uno che prova a esserlo. Un puntatore si inizializza in questo modo: int *k; k è un puntatore ad una variabile di tipo int. Esiste una variabile k di una dimensione che non ci interessa (su sistemi x86 mediamente occupano 4 int), che al suo interno può ospitare un indirizzo di memoria. Un indirizzo di memoria è un punto preciso nella memoria utilizzabile dal nostro programma. Noi sappiamo che quando un programma parte il sistema operativo assegna ad esso uno spazio in ram che verrà usato come si vuole. Lì ci stanno le nostre variabili e altre cose che successivamente vedremo. Ogni singolo byte di questo spazio è mappato e numerato. In indirizzo di memoria indica proprio uno di questi byte. In questi byte ci possono stare le nostre variabili. Quando facciamo un puntatore creiamo una variabile atta a contenere un indirizzo di memoria ove poi andremo a collocare la nostra o le nostre variabili. Prima di vedere gli assegnamenti di indirizzi, guardiamo come gestirli. Esistono per questo scopo due operatori: * e &. Il primo l'abbiamo già visto quando abbiamo creato il nostro primo puntatore. Partirò dal secondo. & indica l'indirizzo di memoria a cui sta puntando il nostro puntatore. * indica la variabile puntata. void main(void) { int a, *b; // Due variabili. Un int normale e un puntatore ad int. a=4; // Associo un valore ad A. b=&a; // Dico a B di puntare all'indirizzo di A. printf("Leggo questo indirizzo di memoria: %p il valore %d\n",&a,*b); } Possiamo notare come l'operatore & è abblicabile anche ad una variabile non puntatore. Infatti con &a vedo dove realmente a sta. Ecco la raffigurazione fittizia della memoria per questo programm: ---INIZIO--- a <-------+ Questo indirizzo è &a. Il valore di a è a. ------------ | b -------+ Questo indirizzo è &b. L'indirizzo puntato è b. ------------ Il valore puntato da b è *b. Calcoli con i puntatori -- inizio -- main(void) { char *a,b[10]; a=b; // Lettera per lettera solo perché così vi mostro dei calcoli: *a='C'; // Imposto il valore dell'indirizzo puntato da A *(++a)='i'; // A++ mi permette di farlo puntare all'indirizzo successivo // in modo da poterci inserire un altro carattere. *(a+1)='a'; // A+1 Il valore successivo a quello puntato da A *(a+2)='o'; // A+2 *(a+3)=0; // A+3 Terminatore di stringa a--; // A-1 printf("%s\n",a); a+=4; // Aumento di 4 ovvero strlen("ciao"); while(a!=b) { a--; printf("%c",*a); } putchar('\n'); } -- fine -- Abbiamo quindi potuto notare come i puntatori possano essere visti come array. Potendo usare gli indirizzi successivi e precedenti a quello puntato, oppure modificando l'indirizzo a cui punta, posso gestire come variabili anche lo spazio limitrofo. Possiamo quindi intuire il concetto legato alla programmazione dinamica, ovvero usare lo spazio che ci serve e solo quello. Questo però va in contro a grossi problemi di stabilità del programma. Pensiamo al questo caso: int *a,*b; a=10; // indirizzo 10 b=12; // indirizzo 12 a+1=23; // carico un valore all'indirizzo 11 a+2=9; // carico un valore all'indirizzo 12 Questo esempio è errato, ma mostra chiaramente un problema notevole della programmazione dinamica. Andando avanti vedremo come evitare questi problemi. Ora quattro parole sugli array. Noi sappiamo che un array è un insieme di variabili dello stesso tipo, sequenziali che sono associate ad un solo nome. int array[12]; E' un array di 12 elementi identificato dal nome array e gestibile con i nomi array[0], array[1], etc. Nella memoria questo non vuol dire altro che uno spazio destinato a questo array. Possiamo quindi vederlo come un puntatore di dodici elementi. L'esempio successivo si riferisce proprio a questo caso. Usando l'algoritmo dello stack, ovvero LIFO (Last In - First Out) gestisco dei numeri sequenziali. Scopo del capitolo successivo è proprio lo studio degli algoritmi. Per avere una idea dello stack, posso parlarvi di una pila di piatti: se voglio metterne uno nuovo lo metto in cima, e se voglio prenderne uno prendo l'ultimo inserito. --- inizio --- #include #define MAX 10 int *p,coda[MAX]; // un array e un puntatore int inserisci(int a); void mostra(void); int main(void) { int numb; p=coda; // associo il puntatore allo stack do { printf("Inserisci un numero (con 0 esci): "); scanf("%d",&numb); if(inserisci(numb)) exit(0); mostra(); } while(1); } // Inserimento di un nuovo numero: int inserisci(int a) { if(p==coda+MAX) return 1; *p=a; // Carico il numero p++; // aumento il valore puntato return 0; // ritorno } // Visualizzazione dello stack void mostra(void) { int i; for(i=0;i #include int somma(int, int); int sottrai(int, int); void main(void) { int a,b; char c[10]; int (*p)(); printf("Dammi 2 numeri: "); scanf("%d%d",&a,&b); printf("Somma o Differenza ? :"); scanf("%s",c); switch(*c) { case 'S': case 's': p=somma; break; case 'D': case 'd': p=sottrai; break; default: return; } printf("valore: %d\n",p(a,b)); } int somma(int a, int b) { return a+b; } int sottrai(int a, int b) { return a-b; } --- fine --- Per capire questo esempio c'è da fare una premessa sugli eseguibili. Quando un eseguibile viene creato esso mappa ogni sua istruzione esattamente con lo stesso principio che viene usato per mappare lo spazio di memoria usato dalle variabili. Facile capire che lo spazio istruzione non è usabile per le variabili e vicersa. Nulla però di fare hacking: --- inizio --- int funzione(int, int); void main(void) { int (*p)(); p=funzione; p+=32; p(2,3); } int funzione(int a, int b) { printf("CIAO"); return 1; } --- fine --- Questo esempio non scriverà CIAO perché P punta non più a FUNZIONE, ma 32 byte dopo. Come si fare a sapere che 32 byte possono bastare a saltare la printf ma non è troppo? Usando un disassemblatore come gdb è piuttosto facile. Questo è un argomento affrontato fra 2 capitoli. Per concludere questo discorso con scherzetto, ecco un altro programma simpatico: --- inizio --- int funzione(int, int); int nascosta(int, int); void main(void) { int (*p)(); p=funzione; p+=36; p(2,3); } int funzione(int a, int b) { printf("CIAO"); return 1; } int nascosta(int a, int b) { printf("MAMMA"); return 1; } --- fine --- Questo programma scrive "MAMMA". MALLOC Sappiamo usare i puntatori. Sappiamo che è pericoloso e abbastanza facile andare a sovrascrivere spazi di dato occupato da altri puntatori. Esiste una funzione che serve a destinare uno spazio da noi deciso a quello che vogliamo noi e che ci assicura la non sovrapposizioni da altri puntatori (sempre se noi non lo vogliamo). La malloc ha come prototipo questo: void *malloc(int); Il fatto di ritornarci un void vuole solo dire che ci ritorna un puntatore non inizializzato. Per questo l'uso normale è questo: char *p; p=(char *)malloc(20*sizeof(char)); (char *) regola come analizzare l'output della funzione. E' gia' stato visto nei capitoli precendenti. 20 è il numero di char che voglio utilizzare. E' in byte. sizeof(char) l'ho moltiplicato per la dimensione dei char perchè non sempre essa è 1 byte. Su certe architetture (vedi alpha) occupano diversamente. Quando la memoria finisce, oppure se ci sono stati dei problemi di qualsiasi natura nella mallocazione, la malloc ritorna un NULL. Opposta alla malloc è la free. Questa rende libero per altro utilizzo un puntatore di cui non abbiamo più bisogno. free(p); Per forza di cose funziona sempre. Nel prossimo capito affronteremo strutture dati che grazie ai puntatori sono rese possibili.