Allocazione di memoria

  1. quali zone di memoria si possono usare (riservate ad uso esclusivo del programma)
  2. funzione malloc: dà una zona di memoria con queste caratteristiche
  3. esempio di creazione di memoria per contenere un intero
  4. vale ancora la regola che *p è una variabile intera, ma si può usare solo se questa zona di memoria è stata riservata

Un puntatore è una variabile che contiene un indirizzo di memoria. Finora, abbiamo usato i puntatori solo per memorizzare in essi le posizioni di altre variabili. In realtà, è possibile memorizzare in un puntatore un valore qualsiasi. D'altra parte, quando si usa *p dove p è un puntatore, occorre essere certi che la zona di memoria puntata da p si possa utilizzare, ossia:

  1. il sistema operativo ci permette di accedere a questa zona;
  2. il contenuto di questa zona non viene modificata dal programma in modo inaspettato.

L'unico modo per realizzare queste due condizioni visto fino ad ora è stato quello di memorizzare in p l'indirizzo di una variabile con istruzioni del tipo di p=&a. In questo caso, infatti, si è sicuri che la zona di memoria è accessibile al programma, dato che è la zona di memoria di una sua variabile; inoltre, non può venire modificata dal programma, a meno che non si facciano delle modifiche su a.

Esiste però un altro meccanismo che soddisfa questi due requisiti, ed è quello di chiedere al sistema operativo di riservare una nuova zona di memoria. In altre parole, si chiama una funzione che crea una nuova zona di memoria e ne restituisce l'indirizzo iniziale. Questa funzione garantisce che:

  1. la zona di memoria è accessibile al programma;
  2. se si creano nuove variabili (per esempio con una chiamata di funzione), non saranno memorizzate in questa zone;
  3. ogni nuova chiamata alla funzione crea una zona di memoria nuova, distinta dalle precedenti.

Il primo punto garantisce che il programma può effettivamente usare queste locazioni di memoria. Il secondo e il terzo punto dicono che non ci saranno altre variabili o altri puntatori che sono associati automaticamente alla stessa zona di memoria. Questo serve a garantire che la zona di memoria non verrà cambiata inaspettatamente dalla modifica di una variabile che non avevamo messo in relazione con quella zona.

La funzione che crea una nuova zona di memoria si chiama malloc. Ha un argomento, che è il numero di byte che la zona da allocare deve contenere. Per esempio malloc(10) assegna al programma una nuova zona di memoria grande 10 byte. Il valore di ritorno di questa funzione è l'indirizzo iniziale di questa zona. Quindi, malloc(10), oltre a creare una zona di 10 byte, restituisce l'indirizzo del primo di questi byte, cioè l'indirizzo più basso in questa zona.

Il programma alloca.c crea una zona di memoria larga abbastanza da contenere un intero. L'indirizzo di ritorno viene assegnato alla variabile di tipo puntatore a intero p.

/*
  Allocazione di memoria.
*/

#include<stdlib.h>

int main(void) {
  int *p;

  p=malloc(sizeof(int));

  *p=12;

  (*p)++;

  printf("*p vale %d\n", *p);

  return 0;
}

Qui accanto vediamo lo stato iniziale della memoria: la variabile puntatore p non contiene nessun valore significativo, e nel programma non sono definite altre variabili.

La prima istruzione del programma è la chiamata alla funzione malloc. In particolare, l'argomento è sizeof(int), quindi la istruzione p=malloc(sizeof(int)) crea una zona di memoria abbastanza grande da contenere un intero, e l'indirizzo iniziale di questa zona viene restituito e memorizzato nella variabile puntatore p.

Nella figura si vede lo stato della memoria dopo questa chiamata: la funzione malloc ha riservato una zona di memoria di quattro byte (ipotizziamo come sempre che un intero è rappresentato con quattro byte) per il programma; la variabile p contiene l'indirizzo della prima locazione di questa zona.

Possiamo ora usare la notazione freccia per indicare quale è l'indirizzo memorizzato nella variabile p. Si ricordi che la notazione con la freccia è semplicemente un trucco grafico per rendere più chiare la figure che riguardano i puntatori, ma che quello che in effetti si trova scritto nelle variabili puntatore è un indirizzo, ossia un numero, e che la variabili puntatore, per il resto, si comportano come tutte le variabili normali.

Un'osservazione importante: la variabile puntatore p contiene l'indirizzo di una zona di memoria grande quanto basta per contenere un intero. Inoltre, la variabile p è di tipo puntatore a intero. Quindi, si può ancora pensare a *p come alla variabile intera la cui zona di memoria è quella identificata dalla punta della freccia del disegno di sopra.

Il altre parole, dopo aver fatto p=malloc(sizeof(int)), è come se avessimo creato una nuova variabile di tipo intero *p, che possiamo quindi usare come una qualsiasi altra variabile intera: possiamo per esempio assegnare ad essa un valore con *p=12, incrementarla con (*p)++, e stampare il suo valore con printf("*p vale %d\n", *p); (il motivo delle parentesi per (*p)++ sarà chiarito più avanti).

Il punto fondamentale da ricordare è che è possibile creare una zona di memoria sia dichiarando una variabile che chiamando la funzione malloc. In quest'ultimo caso, il risultato della funzione va memorizzato in un puntatore.

Riassumendo, la regola generale è: se p è una variabile puntatore a intero, allora *p è come se fosse una variabile intera. Questa variabile si può però usare soltanto se in p c'è un indirizzo di memoria di una zona che si può usare. Questo avviene soltanto se:

Il seguente programma duemalloc.c mostra due possibili modi di mettere in una variabile puntatore degli indirizzi di zone di memoria che si possono usare.

/*
  Allocazione di memoria.
*/

#include<stdlib.h>

int main(void) {
  int *p, *q;
  int a;

  p=malloc(sizeof(int));
  *p=12;
  (*p)=(*p)+12;
	
  q=&a;
  *q=4;
  (*q)=(*q)+4;

  printf("Valori di *p e *q: %d e %d\n", *p, *q);

  return 0;
}

L'indirizzo che viene memorizzato in p è il valore di ritorno di malloc, quindi l'indirizzo di una zona di memoria che è riservata dalla funzione malloc. Nella variabile q viene invece memorizzato l'indirizzo della variabile a. Si noti che questa operazione fa sí che le modifiche a *q siano anche modifiche ad a, dato che la zona di memoria è la stessa.

Rispetto all'esempio di prima nomalloc.c, in cui la variabile puntatore è indeterminata, qui la modifica ad a è prevedibile, ed avviene tutte le volte (la cosa da evitare è quella in cui il comportamento di un programma dipenda da valori iniziali non determinati).