Per tornare all'indice degli argomenti clicca qui linguaggio C++

Gestione della memoria

A seconda di come e dove vengono definite, si possono avere diversi tipi di variabili:

  • variabili globali: definite fuori da ogni funzione (anche dal main) o classe
  • variabili locali (o automatiche): definite dentro una funzione (o classe)
  • variabili statiche: per approfondimento vedere static
  • variabili dinamiche: termine usato per definire l'allocazione di memoria durante l'esecuzione (run time). N.B: var. dinamiche non è il contrario di var. statiche

Un programma contiene istruzioni da eseguire. Un programma in esecuzione sul sistema operativo viene chiamato processo. Il file eseguibile, prima dell'esecuzione, viene trasferito dalla memoria di massa alla memoria centrale, dove viene chiamato immagine del processo. Nella memoria centrale il file eseguibile è diviso in diverse sezioni (section): text section, data setion e stack section. Il sistema operativo può gestire ogni sezione, dividendola ulteriormente in segmenti (segment) (anche non contigui tra loro). Tutto ciò è necessario perché un sistema operativo multitasking, che esegue più processi contemporaneamente, deve poter offrire ad ogni processo le limitate risorse hardware (come la memoria centrale) garantendo anche la sicurezza. È possibile approfondire questi argomenti studiando i sistemi operativi.

Segment

Un segmento rappresenta un'area di memoria contigua pari a 64 kB

Text segment

Area che contiene le istruzioni eseguibili e le costanti. Durante l'esecuzione del programma l'area è di dimensione fissa e di sola lettura. Viene chiamato anche “code segment”.

Data segment

Area che contiene le variabili cosiddette globali e quelle statiche che sono state inizializzate esplicitamente dal programmatore. Questa area è di dimensione costante nel tempo, ma a differenza del precedente non è di sola lettura, poiché è possibile modificare i valori delle variabili in runtime. Le variabili in quest'area sono visibili a tutte le funzioni per tutta la durata del programma.

BBS segment

(Block Started by Symbol) area che contiene le variabili cosiddette globali e quelle statiche che, a differenza del precedente caso, non sono state inizializzate dal programmatore e che quindi vengono inizializzate automaticametne a zero dal compilatore. Le altre caratteristiche sono identiche a quelle del Data Segment (vedere l'inizializzazione delle variabili…)

Heap segment

Significa letteralmente mucchio. È detta anche memoria dinamica perché all'interno di questa area (di dimensione non fissa) lo spazio viene riservato e liberato dinamicamente, in runtime, cioè durante l'esecuzione. Questo avviene sotto il controllo del programmatore, a seconda delle richieste dell'utente e delle istruzioni presenti nel programma. Per questi compiti il programmatore usa l'operatore new e l'operatore delete. Nel linguaggio C++ questa memoria è riservata e liberata con una gestione manuale (operatore new e operatore delete). Nel linguaggio JAva l'operatore delete non esiste.

Stack

Significa letteralmente pila. Come in una pila, gli elementi che compongono lo stack vengono riservati in modo ordinato e liberati in ordine inverso. È detta anche memoria automatica perché la gestione è automatica, sotto il controllo del sistema operativo. Lo stack contiene le variabili cosiddette locali e i parametri (come: int i) dichiarati nell'ambito dell'esecuzione di una funzione. Le variabili presenti in questa area sono visibili solo alle rispettive funzioni e solo per l'intervallo di tempo necessario alla funzione. Ogni elemento viene liberato automaticamente al termine della funzione (punto indicato con la parentesi graffa chiusa).

Confronto delle caratteristiche

Una variabile locale (nello stack) ha una vita pari a quella del periodo di esecuzione della funzione. Una variabile allocata dinamicamente (nell'heap) con l'operatore new può avere una vita anche più lunga, perché non viene distrutta fino a che non si usa l'operatore delete. Può durare di più ma anche di meno…

Quando non interviene il programmatore:

  • Le variabili globali, statiche e gli oggetti sono inizializzati automaticamente a zero.
  • Rimangono non inizializzate le altre variabili, cioè:
    • quelle locali (allocate nello stack)
    • quelle dinamiche (allocate con new nell'heap)

Puntatori a... (qualcosa)

Prima di spiegare i puntatori è necessario ricordare la distinzione tra la cosiddetta memoria automatica e memoria dinamica.

  • Memoria automatica Le variabili automatiche (cosiddette locali) sono gestite automaticamente nello stack e quando non servono più vengono automaticamente “distrutte”.
  • Memoria dinamica Durante l'esecuzione del programma può essere necessario allocare (riservare) dinamicamente della memoria. Questo accade per le operazioni dove non si possono prevedere in anticipo le richieste di un utente.

Per riservare una zona di memoria dinamicamente si usa l'operatore new.

New

L'operatore new fa parte della libreria standard del C++, ma è una delle poche eccezioni in cui non è necessario specificare il namespace std.

Esempio del codice necessario per poter usare new

#include <new>
new float; // riserva la quantità di memoria necessaria per un numero in virgola mobile

Cosa restituisce l'operatore new? Restituisce l'indirizzo iniziale della memoria che è appena stata riservata.

Dove memorizzare un indirizzo? Quale tipo di dato è utile per memorizzare un indirizzo di memoria? Anche se un indirizzo di memoria è un numero, non si usa il tipo int, ma un tipo di dato speciale chiamato puntatore a…. Nel precedente esempio si deve usare un “puntatore a float”. Per dichiarare tale puntatore basta aggiungere l'operatore asterisco * dopo il nome del tipo:

float* numero;       //definizione della variabile puntatore a...
numero = new float;  // inizializzazione della variabile puntatore a..

Nel precedente esempio il programma ha riservato due aree di memoria:

  • “numero” come variabile automatica, per contenere un indirizzo
  • “new float” come variabile dinamica, per contenere un numero in virgola mobile

Le due operazioni possono essere eseguite anche in un'unica volta:

float* numero = new float;

La seconda variabile (nell'heap) sarebbe inutilizzabile senza la prima (nello stack), ma chi è che controlla la “distruzione” della prima? Poiché “numero” è una variabile automatica essa si libera automaticamente quando termina la funzione che la contiene, quindi l'area allocata dinamicamente potrebbe diventare inutilizzabile nel caso in cui questo accadesse. Nell'esempio visto, il programmatore deve usare delete prima che si verifichi questo evento “disastroso”:

delete numero;

Riassumendo, questo è l'ordine in cui si devono fare le cose

  • inizia la funzione
    1. definire un puntatore (nella mem. automatica)
    2. riservare la memoria dinamica con new e inizializzare il puntatore
    3. utilizzare a proprio piacere l'area dinamica…
    4. liberare la memoria dinamica con delete
  • termina la funzione (si libera anche il puntatore)

Oppure, in altre parole, ricordarsi di liberare tutte le variabili dinamiche create per evitare problemi di gestione della memoria o falle (memory leak). I puntatori quindi sono variabili usate prevalentemente per controllare la memoria dinamica (heap).

Un consiglio

Nonostante quanto detto nelle precedenti righe, sarebbe meglio non creare/distruggere variabili dinamiche in una funzione. L'utilizzo dei puntatori è un comune causa di errori o bug nei programmi, soprattutto se questi sono utilizzati per un lungo tempo. La strategia consigliata è di usare l'operatore new solo nei costruttori degli oggetti e l'operatore delete solo nei distruttori. Tali puntatori non dovrebbero essere nemmeno “esposti”, cioè dovrebbero essere di tipo private.

I puntatori (e gli array) lavorano sulla memoria a basso livello, cioè senza quasi nessun controllo, e possono causare oscuri malfunzionamenti e bug! I vector vanno preferiti perché hanno un'efficienza ancora sufficientemente elevata e sono più facili da usare (ci sono più controlli).

Creando un vector (o uno fstream) come oggetto locale, la distruzione sarà eseguita automaticamente al termine della funzione e così si posso evitare i puntatori. In ogni caso, non usare direttamente new e delete, ma creare oggetti con new nel costruttore e delete nel distruttore.

La memoria dinamica, rispetto alla memoria automatica, può essere utile per contenere oggetti molto grandi o di dimensioni non note a priori, oppure per contenere oggetti che devono durare a lungo (più della durata degli oggetti locali (automatici) di una funzione).

I puntatori non vengono usati solo per accedere alla memoria heap. A volte i puntatori permettono di accedere anche a zone della memoria (stack) altrimenti inaccessibili, come quando si vuole che una funzione conosca l'indirizzo di una variabile usata da un'altra per poterla modificare. Ma questo utilizzo non dovrebbe essere necessario e probabilmente si basa su un cattivo stile di programmazione. A volte i puntatori permettono di puntare alle funzioni, consentendo di eseguire funzioni diverse per condizioni diverse che si possono verificare in tempo di esecuzione (running time), in altre parole i puntatori consentono anche il polimorfismo, a cui sarà dedicato un capitolo a parte.

int a1[5];

In linguaggio C scrivere “a1” equivale a scrivere l'indirizzo del primo elemento dell'array “&a1[0]” , quindi, quando si passa un array ad una funzione, in realtà si passa sempre il valore del suo indirizzo (come un puntatore). Per gli array vedere anche il capitolo tipi_di_dato_strutturato_array.

La differenza tra una variabile puntatore (int* p) e un indirizzo (&a1[0]) è che la prima può cambiare valore e puntare qualsiasi area di memoria, mentre il secondo, essendo l'indirizzo del primo elemento di un array, non può essere cambiato.

int *p = a1

Se il puntatore punta all'area di memoria di un array, si può utilizzare una sintassi simile a quella di un array:

p[0]=3; 

Nelle dichiarazioni delle funzioni si può usare equivalentemente una qualsiasi delle seguenti due sintassi: rif. standard 13.1-3.3

int fun(int p[]);
int fun(int *p);

Il passaggio di un array tramite una funzione, avviene sempre tramite un puntatore, ma un array non è un puntatore…

differenza tra

return &variabile; // (dangling pointer) resituisce l'indirizzo di una 
                   // var che si autodistrugge(locale,automatica)
return puntatore; // restituisce un indirizzo (la copia del contenuto di 
                  // un puntatore) che contiene l'indirizzo di (si spera) 
                  // un'area allocata con new, che sopravvive alla funzione.

È introdotto nel linguaggio C++ per necessità. Ovviamente non esistono oggetti void. Il tipo void viene usato solo per indicare “nessun valore restituito” da una funzione.

Il puntatore void* è usato solo per poter puntare qualsiasi cosa, che il programma non sa cosa sia (mentre il programmatore deve sapere cosa e'). Il tipo void* serve solo per copiare un indirizzo di memoria a basso livello e non consente di lavorare sul contenuto della memoria.

Questo significa che per poter lavorare sul contenuto bisogna effettuare un casting verso un altro tipo di puntatore. Mentre il casting tra altri tipi può essere implicito, per i puntatori è necessario usare il casting esplicito… Mentre esistono conversioni implicite tra float e int, non esiste tra float* e int*

smart pointers

shared pointers

to do…

Per tornare all'indice degli argomenti clicca qui linguaggio_c
  • appunti3s/gestione_della_memoria.txt
  • Last modified: 2019/07/28 09:41
  • by profpro