Allocazione dinamica della memoria: usi e costumi del puntatore void* • Puntatori V

A
Questo articolo fa parte di una serie di articoli:

  1. Comprendere i puntatori C/C++
  2. L’altra faccia della medaglia
  3. Aritmetica dei puntatori, questa sconosciuta
  4. Raddoppiamo la difficoltà: le stringhe
  5. Allocazione dinamica della memoria: usi e costumi del puntatore void*

 

Allocazione dinamica della memoria: usi e costumi del puntatore void*

Nel precedente articolo abbiamo analizzato e proposto tramite numerosi esempi la gestione delle stringhe C-like tramite le variabili puntatore. In questo articolo esamineremo invece l’allocazione dinamica della memoria, la quale si basa completamente sull’uso dei puntatori e della loro aritmetica.

 

VOID*

Nel primo articolo abbiamo esaminato il motivo principale per cui ogni puntatore necessita di un tipo ben preciso, affermando che non è possibile avere un puntatore generico.
Il tipo ‘void*’, assegnabile ovviamente solo alle variabili puntatore, potrebbe essere una valida obiezione alla precedente affermazione. Vediamo quindi di chiarire meglio il concetto alla base di questo particolare tipo di puntatore e come possiamo utilizzarlo nei nostri programmi.

La keyword ‘void’ viene utilizzata per dichiarare e definire funzioni che non accettano parametri in ingresso e/o non restituiscono alcun valore in uscita.
Per quanto riguarda i puntatori invece, il tipo ‘void*’ ha un significato completamente diverso: un puntatore di questo tipo può puntare qualsiasi tipo di variabile, nativa o meno, ma non può venire dereferenziato. Per quale motivo?
Pensandoci bene il puntatore stesso non possiede informazioni riguardo al numero di byte che compongono la variabile puntata, poiché esso è ‘void*’.
Questo handicap invalida anche la possibilità di utilizzare buona parte dell’aritmetica dei puntatori. Rimangono valide operazioni come pointer++; pointer -= n; le quali considerano il tipo ‘void*’ identico al tipo ‘char*’, quindi spostano il puntatore di un solo (oppure n) byte.
Per poter utilizzare un puntatore ‘void*’ è perciò necessaria un’operazione di cast al tipo desiderato, prendendo le precauzioni necessarie affinché il puntatore ottenuto sia valido (allineato).
Possiamo quindi affermare che il puntatore ‘void*’ più che un puntatore generico è un puntatore grezzo.

Quando può essere necessario utilizzare questo particolare tipo di puntatore?
Analizziamo assieme una possibile implementazione di una famosa funzione messa a disposizione dal C: ‘memcpy’. Essa permette di copiare una porzione di memoria grande a piacere in un’altra.
Ovviamente è conveniente inviare l’indirizzo delle due locazioni di memoria, in particolare per quella di destinazione è fondamentale se vogliamo che venga effettivamente modificata.
Non vogliamo nemmeno scrivere n versioni della stessa funzione per le varie combinazioni dei tipi puntatore nativi, anzi preferibilmente desideriamo che la funzione possa agire anche con eventuali tipi puntatore definiti dall’utente.
Se riflettiamo attentamente, la funzione non ha nemmeno la necessità di conoscere il tipo dei dati ricevuti: è sufficiente che copi byte dopo byte la sorgente nella destinazione.

Ecco che entra in scena il puntatore ‘void*’. Esso può puntare qualsiasi variabile, indipendentemente dal tipo, ma non può modificare la variabile stessa senza un’intenzionale operazione di cast. Dato che abbiamo necessità di copiare ogni singolo byte, e quindi dereferenziare il puntatore, la scelta per l’operazione di cast ricade ovviamente sul puntatore ‘char*’.

void memcpy_(void *dest, void *src, size_t n)
{
   while(n--) *(char*)dest++ = *(char*)src++;
}

Lascio al lettore il piacere derivante da un sano ripasso della precedenza degli operatori.

 

ALLOCAZIONE DINAMICA DELLA MEMORIA

Un altro caso dove l’uso del puntatore ‘void*’ è fontamentale riguarda l’allocazione dinamica della memoria nel linguaggio C. Capita spesso infatti di non conoscere in fase di compilazione la grandezza necessaria per uno specifico contenitore, e sovradimensionarlo non è di certo la scelta migliore.
Sia il C che il C++ ci permettono di richiedere della memoria direttamente a run-time, sfruttando i dati inseriti dall’utente del nostro programma. In questo articolo analizzeremo alcune possibilità offerte dal C.

Tramite la funzione ‘malloc’ potremmo richiedere al sistema operativo un determinato numero di byte, numero che questa funzione richiede come parametro, i quali verranno allocati sottoforma di blocco contiguo. L’unico modo che questa funzione ha per restituire il suddetto blocco è tramite un puntatore al primo elemento. Di quale tipo? Considerando che la funzione è da utilizzare per richiedere spazio indipendentemente dal tipo di variabile che lo andrà ad occupare, l’unica scelta possibile è proprio il ‘void*’.
Utilizzando il C++ è necessario eseguire il cast su questo puntatore prima di poterlo assegnare ad una variabile puntatore. In C, invece, questa operazione di cast è implicita. Lo standard ci assicura che questo puntatore è allineato in modo che possa essere assegnato a qualsiasi tipo di variabile, anche alle variabili di tipo non nativo. Se necessario è anche possibile definire un allineamento specifico, ma non ci occuperemo di questo caso.
È bene sapere che nel caso in cui ‘malloc’ non è in grado di allocare lo spazio richiesto, perché ad esempio esso è troppo elevato, restituisce un puntatore nullo. Questo ci permette di prendere le necessarie contromisure.

Nel terzo articolo abbiamo visto come le variabili puntatore possono essere utilizzate in sostituzione degli array tramite l’aritmetica dei puntatori. Perciò sarà possibile interagire con il blocco di memoria allocato dinamicamente, sfruttando l’indirizzo restituito da ‘malloc’, come se fosse un array.
Esaminiamo un caso pratico:

#include <stdio.h>
#include <stdlib.h>

int main()
{
  int dimensione = 0;
  
  // inserisco la dimensione desiderata per il
  // contenitore che andremo ad allocare dinamicamente
  scanf("%d",&dimensione);
  
  // il seguente puntatore memorizzerà il puntatore 
  // al blocco di memoria allocato dinamicamente
  int * pointer = NULL;
  
  // quanti byte dobbiamo richiedere?
  // il numero di interi da memorizzare è pari a 'dimensione'
  // e ogni intero necessita di sizeof(int) byte
  // il cast in C++ è da effettuare a int*
  pointer = (int*) malloc(dimensione * sizeof(int));
  
  // se malloc fallisce nell'allocazione dello spazio richiesto
  // termino il programma
  if(!pointer) return 0;
   
  // utilizzo lo spazio allocato come se fosse un array
  // tramite il puntatore 'pointer'
  // sfruttando l'aritmetica dei puntatori
  int i;
  for(i = 0; i < dimensione; i++)
  {
    pointer[i] = i * i;
    printf("%d ", pointer[i]);
  }
  printf("%c",'\n');

  // libero lo spazio precedentemente allocato
  // restituendolo al sistema operativo
  free(pointer);
  pointer = NULL;
  
  return 0;
}

Rimane da analizzare la chiamata alla funzione ‘free’. Perché è necessario liberare lo spazio allocato? Le variabili allocate dinamicamente, a differenza di quelle allocate staticamente, non terminano la loro esistenza alla fine del loro scope. L’unico modo per rilasciare la memoria occupata, oltre a terminare il programma in esecuzione, è quello di informare il sistema operativo che essa è di nuovo disponibile, chiamando la funzione ‘free’. Il parametro richiesto, come è possibile notare nell’esempio, è l’indirizzo del primo byte allocato dinamicamente, ovvero l’indirizzo fornitoci da ‘malloc’. È perciò molto importante non perdere, nel susseguirsi delle varie istruzioni, il valore di questo indirizzo.
È bene sottolineare che la funzione ‘free’ non modifica in alcun modo il valore del puntatore, il quale però conterrà un indirizzo non più valido. Per questo è buona pratica trasformarlo in un puntatore nullo.

 

CONCLUSIONE

Ovviamente abbiamo solo scalfito le opportunità che l’allocazione dinamica della memoria ci mette a disposizione. Lo scopo di questo articolo, il quale conclude la serie sui puntatori, è infatti quello di dare al lettore un’infarinatura base sull’argomento. Le variabili puntatore abbracciano un ampio spazio sia in C che in C++ e probabilmente seguiranno in futuro altri articoli su soggetti più specifici; nel frattempo spero che quanto detto fin’ora vi abbia aiutato a comprendere meglio questo piccolo universo.

 

[Scarica l’articolo in formato PDF]

 

A proposito di me

Andrea Simone Costa

Classe 1997, Toscano DOCP, Asimov oriented.
Si è diplomato come perito elettronico, ma ha ben presto tradito le origini per immergersi nell'universo JavaScript. Ama condividere ciò che ha imparato in modo semplice e pragmatico, senza mai ricadere nel banale, coltivando segretamente il sogno di insegnare.

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti