Memoria dinamica.

Molto spesso, durante il lavoro, utilizziamo variabili locali e globali: eseguiamo calcoli intermedi in quelli locali, creiamo oggetti di classe di libreria a livello globale e li utilizziamo nel programma, e così via. Senza approfondire il programma, puoi capire dove è disponibile una particolare variabile, dove si trova il suo ambito. Se vediamo il nome di tale variabile nel codice, possiamo affermare con sicurezza che questa variabile esiste in un determinato punto del programma. Il suo valore è in memoria e possiamo lavorarci direttamente, leggerlo o modificarlo, e anche "misurare" il peso di questa variabile. Il fatto è che il ciclo di vita di tali variabili è noto in anticipo, non possono cambiare la loro dimensione durante il funzionamento del programma, non possono apparire o scomparire in un altro momento o in un altro luogo, lavorare con loro è semplice e comprensibile.
Ma cosa succede se, ad esempio, dobbiamo accettare dati di dimensioni sconosciute? Oppure è necessario utilizzare diverse "librerie" condizionali nell'ambito globale del programma, ma non c'è abbastanza RAM per tutti? La memoria dinamica viene in soccorso: possiamo creare variabili (allocare memoria) proprio nel processo di esecuzione del programma e cancellarle da lì. Sempre e ovunque, a tuo piacimento.

Distribuzione della memoria.
Prima di tutto, dobbiamo familiarizzare con l'distribuzione della memoria e capire come funziona e cosa faremo in generale. Ecco un diagramma di distribuzione della memoria in MCU da AVR, che si trova su Arduino. In molte altre architetture, la memoria ad accesso casuale (SRAM) è organizzata in modo simile:

La memoria è essenzialmente un grande array, ogni cella di cui ha il proprio indirizzo, l'indirizzo cresce da sinistra a destra (nell'immagine sopra). Il flash viene prima, è anche memoria di programma, memorizza il codice del programma. Mentre il programma è in esecuzione, questo codice non cambia (ci sono modi per farlo).

Siamo interessati alla memoria dinamica SRAM, che nel diagramma è rappresentata da una combinazione di aree blu, verdi, rosa e arancioni, nonché da un'area bianca con frecce tra il rosa e l'arancione. Diamo un'occhiata più da vicino:
  • Globals (blu e verde): le variabili globali e statiche risiedono in quest'area. La dimensione di quest'area è nota al momento dell'avvio del programma e non cambia durante la sua esecuzione, perché le variabili globali e statiche sono già dichiarate, la loro dimensione e numero sono noti.
  • Stack (arancione) - Le variabili locali e gli argomenti delle funzioni risiedono in quest'area. La dimensione di quest'area cambia durante l'esecuzione del programma, lo stack cresce dalla fine dell'area di memoria verso indirizzi decrescenti, verso Heap (vedi la freccia nel diagramma). Le variabili che risiedono qui sono chiamate automatiche: il programma stesso alloca memoria (quando si crea una variabile locale) e libera questa memoria stessa (la variabile locale viene cancellata quando la funzione esce). Importante: il processore non controlla la dimensione dello stack, ovvero, durante il funzionamento, lo stack può bloccarsi nel Heap e sovrascrivere i dati che si trovano lì.
  • Heap (rosa): da quest'area possiamo allocare in modo indipendente la memoria per le nostre esigenze. La dimensione di quest'area può cambiare durante l'esecuzione del programma, Heap "cresce" nella direzione di indirizzi crescenti, da sinistra a destra, come mostrato dalla freccia nel diagramma. Controlliamo noi stessi questa memoria: la assegniamo e la rilasciamo noi stessi. Importante: il processore non ti consentirà di allocare un'area se non c'è abbastanza memoria libera per essa, ad es. la scansione da Heap a Stack è improbabile.
Allocazione della memoria.
Abbiamo strumenti già pronti per allocare e liberare la memoria dinamica (Heap). Durante l'allocazione della memoria, otteniamo l'indirizzo del primo byte dell'area allocata e dovrà essere memorizzato in un puntatore.
  • malloc(quantità) - alloca la quantità di byte di memoria dinamica e restituisce l'indirizzo del primo byte dell'area allocata. Se non c'è abbastanza memoria libera da allocare, restituisce un "puntatore null" - NULL (nullptr).
  • free(ptr) - libera la memoria a cui punta il puntatore ptr. Solo la memoria allocata con le funzioni malloc(), realloc() o calloc() può essere liberata. L'area allocata memorizza la dimensione di quest'area (+2 byte) e, una volta liberata, la funzione free stessa sa quale dimensione liberare.
  • new ed delete: sono tecnicamente gli stessi malloc() e free(), differenza nell'utilizzo (vedi esempio sotto).
  • calloc(quantità, size) - alloca memoria per il quantità di elementi con una size ciascuno (in byte). Lo stesso malloc, ma un po' più comodo da usare: nell'esempio sopra, abbiamo moltiplicato per ottenere il numero di byte richiesto per memorizzare int malloc(20 * sizeof(int)), oppure puoi chiamare calloc(20, sizeof(int )); - sostituendo il segno di moltiplicazione con una virgola.
  • realloc(ptr, size) - Modifica la quantità di memoria allocata puntata da ptr in un nuovo valore specificato dal parametro size. Il valore della dimensione è espresso in byte e può essere maggiore o minore dell'originale. Viene restituito un puntatore a un blocco di memoria, poiché potrebbe essere necessario spostare il blocco all'aumentare delle sue dimensioni. In questo caso, il contenuto del vecchio blocco viene copiato nel nuovo blocco e nessuna informazione va persa.
Nota: le funzioni per lavorare con la memoria dinamica sono piuttosto pesanti, il loro utilizzo aggiunge circa 2 kB al peso del programma! Ecco perché la libreria per stringhe String occupa così tanto spazio.
Considera un esempio di allocazione e deallocazione della memoria utilizzando malloc/free e new/delete. Gli esempi sono esattamente gli stessi in termini di ciò che sta accadendo, differiscono solo per la sintassi:
Esempio: malloc/free.
void setup() 
{ 
     // allocare memoria per 10 variabili, tipo byte (10 byte).
     byte *a = malloc(10); 
     // allocare memoria per 20 variabili, tipo int (40 byte).
     int *b = malloc(20 * sizeof(int)); 
     // allocare memoria per 1 variabile di tipo uint32_t (4 byte).  
     uint32_t *c = malloc(4); 
     // lavorare con array
     a[0] = 50; 
     a[1] = 60; 
     a[2] = 90; 
     uart.println(a[0]); 
     uart.println(a[1]); 
     uart.println(a[2]); 
     // con una variabile ordinaria
     *c = 123456; 
     free(c); // deallocazione  
     free(b); // deallocazione  
     free(a); // deallocazione
     // qui *c è uguale a zero
void loop() 
{
}

Esempio: new/delete.
void setup() 
     // allocare memoria per 10 variabili, tipo byte (10 byte).  
     byte *a = new byte [10]; 
     // allocare memoria per 10 variabili, tipo byte (10 byte).
     int *b = new int [20]; 
     // allocare memoria per 1 variabile di tipo uint32_t (4 byte).  
     uint32_t *c = new uint32_t; 
    // lavorare con array
     a[0] = 50; 
     a[1] = 60; 
     a[2] = 90; 
     // con una variabile ordinaria
     *c = 123456; 
     delete c; // deallocazione  
     delete [] b; // deallocazione (indica che questo è un array). 
     delete [] a; // deallocazione (indica che questo è un array). 
     // qui *c e il resto sono uguale a zero! Li abbiamo "rimossi".
void loop() 
{


Oggetti dinamici.
A volte è necessario utilizzare una "libreria" condizionale su un ampio dominio di definizione, ma non farlo durante l'intero programma, ad es. a volte allocare un oggetto dalla memoria, utilizzarlo e quindi eliminarlo. Per libreria, in questo caso, intendo una classe o una struttura, perché Il 99,99% delle biblioteche sono classi. Dal punto di vista di un programma, un oggetto è anche una variabile, solo "complessa". Possiamo sempre creare un puntatore a un oggetto e usarlo dinamicamente (non dimenticare che l'operatore "punto" si trasforma in una "freccia" ->). Un esempio astratto con la libreria Servo standard, in cui un oggetto viene creato dinamicamente e quindi scaricato dalla memoria quando ruotato di un determinato angolo:
#include <Servo.h> 
Servo* srv; // puntatore al tipo Servo 
void setup() 
     srv = new Servo; // creato 
     srv->attach(10); // collegato 
int count = 0; 
void loop() 
     if (count < 100) 
     { 
          srv->write(count); 
          count += 10; 
          if (count >= 100) 
          delete srv; // rimosso 
          delay(100); 
     } 

Frammentazione.
Possiamo riservarci un'area della memoria, possiamo liberarla. Cosa può andare storto?
  1. Se si alloca memoria in sequenza più volte, le sezioni verranno posizionate una dopo l'altra.
  2. Se rilasci l'area assegnata prima delle precedenti, in memoria rimarrà un "buco": le aree occupate non si spostano nello spazio lasciato libero!
  3. Se si continua ad allocare memoria, il programma proverà prima su una nuova area sui "buchi" e poi sull'area libera nel Heap.
  4. Se la memoria dinamica viene gestita con noncuranza, questa stessa memoria può esaurirsi molto più velocemente del previsto!
Nota: e necessario liberare memoria nell'ordine inverso rispetto alla sua allocazione per evitare la formazione di spazi vuoti.

Perdere il puntatore.
Come puoi vedere dagli esempi sopra, il blocco di memoria viene rilasciato in base al puntatore, ovvero indichiamo da quale indirizzo deve essere liberata la memoria. Questo significa che non dobbiamo mai perdere il puntatore, altrimenti non potremo liberare la memoria! Un esempio astratto:
void foo() { 
      { 
          int* v = new int; 
          *v = 12345; 
      } 
     // qua e oltre non possiamo più rimuovere v!
}
Nota: non dimenticare di liberare memoria!
 
Se una variabile selezionata esiste.
Come determinare se una variabile selezionata esiste attualmente e se è possibile lavorarci? È molto semplice: abbiamo un puntatore ad esso. Se si passa un puntatore alla condizione, verrà visualizzato true se l'indirizzo è diverso da zero (la variabile esiste in memoria) e false se è null (la variabile non esiste in memoria, non è possibile accedere al puntatore). Tornando all'esempio Servo, puoi fare questo:
if (srv) srv->write(..);
Cioè, se l'oggetto esiste (per puntatore), allora lavoriamo con esso.
Ma ci sono un paio di cose da tenere a mente se un programma esegue questo controllo di esistenza:
  • Un puntatore generato localmente può essere non nullo. Se non si alloca memoria immediatamente durante la creazione di un puntatore in locale, è meglio impostarlo su zero, equiparandolo a NULL o nullptr: int* val = nullptr;
  • Quando si libera memoria, il puntatore non viene azzerato! Si fa manualmente: delete v; v = nullptr;
Peso variabile dinamico.
Se per qualche ragione era necessario scoprire il peso di una variabile dinamica, allora non è necessario confondere cosa misurare: stiamo lavorando con un puntatore e il puntatore ha un valore (tipo) a cui punta. Ad esempio, allochiamo un byte di memoria: byte* b = new byte;
  • Se misurato come sizeof(b) - il risultato sarà 2 byte (AVR) e 4 byte (esp8266 / esp32), perché questo è il peso del puntatore, che dipende dal bit dello spazio degli indirizzi del microcontrollore, cioè il risultato non dipende dal tipo e dal peso dei dati.
  • Se misurato come sizeof(*b) - otteniamo 1 byte, è quanto pesa un byte e abbiamo misurato la dimensione dei dati con un puntatore.
Gestione della memoria batch.
Quindi, abbiamo imparato come allocare e liberare memoria, ora considereremo diversi strumenti per lavorare comodamente con la memoria dinamica:
  • memset(ptr, valore, quantità) - riempie l'area di memoria puntata da ptr con byte di valore, nel quantità. Spesso utilizzato per impostare i valori iniziali dell'area di memoria allocata. Attenzione! Si riempie solo di byte, 0..255.
  • memcpy(ptr1, ptr2, quantità) - riscrive i byte dall'area ptr2 a ptr1 nel quantità. In parole povere, sovrascrive un array in un altro. Attenzione! Funziona con i byte.
// memset
 // allocati 50 byte
byte *buf = (byte*)malloc(50); 
// riempire con dati
memset(buf, 10, 50); 
// memcpy
// creare una array 
byte data1[] = {1, 2, 3, 4, 5}; 
// e un altro array 
//byte data2[5];  // può essere estratto dal Stack
byte *data2 = malloc(5); // può provenire dal Heap
// riscrivere i dati1 in dati2
memcpy(data2, data1, 5); 
// data2 adesso e uguale a 1 2 3 4 5

Per che cosa?
Un principiante non avrà bisogno di lavorare con la memoria dinamica, ma la conoscenza sarà utile quando studia il codice di qualcun altro. Incontriamo anche memoria dinamica ogni volta che utilizziamo String, che sono array dinamici.
Crea il tuo sito web gratis! Questo sito è stato creato con Webnode. Crea il tuo sito gratuito oggi stesso! Inizia