← Programmazione
C++ · Strutture Dati

Strutture Dati Dinamiche

Puntatori, nodi e allocazione in heap — lista concatenata con step-by-step, stack e coda senza capacità fissa.

🎓 4° anno ITIS/SIA 🔗 Allocazione dinamica ⏰ ~60 min 🔧 3 visualizzatori
▼ testa 15 8 23 → nullptr Nodo* testa

Puntatori

Un puntatore è una variabile che contiene l'indirizzo di memoria di un'altra variabile. Mentre una variabile normale memorizza un valore (int x = 42), un puntatore memorizza dove quel valore si trova in memoria. Sono la base di tutte le strutture dati dinamiche.

Operatori e comandi — riferimento rapido

Operatore / ComandoSignificatoEsempio
* (dichiarazione) Dichiara che la variabile è un puntatore del tipo indicato: contiene un indirizzo di memoria, non un valore diretto int* p;
* (espressione) Leggi o scrivi il valore all'indirizzo contenuto nel puntatore (dereferenziazione). *p = 42 scrive 42 nella cella di memoria puntata da p *p = 42;
& Restituisce l'indirizzo di memoria di una variabile esistente — serve per inizializzare un puntatore con l'indirizzo di una variabile già in memoria int* p = &x;
-> Accede a un campo di una struct tramite puntatore. Equivale a (*p).campo ma più leggibile nodo->valore
new Alloca memoria in heap per un oggetto e restituisce il suo indirizzo. La memoria rimane occupata finché non si chiama delete — il programmatore è responsabile Nodo* n = new Nodo;
delete Libera la memoria di un singolo oggetto allocato con new. Usare sempre p = nullptr dopo per evitare puntatori invalidi delete p; p = nullptr;
delete[] Libera la memoria di un array allocato con new[]. Non intercambiabile con delete delete[] arr;
nullptr Puntatore nullo — nessun indirizzo valido. Usato per indicare “non punta a nulla” e come valore di terminazione nelle catene di nodi p = nullptr;
Un occhio alla sintassi classica: perché non usiamo malloc, calloc, realloc, free?

Queste funzioni vengono dal linguaggio C e funzionano anche in C++, ma sono superate. malloc alloca byte grezzi senza chiamare costruttori, restituisce void* che richiede un cast esplicito e va accoppiato solo con free — mai con delete.

C (evitare)C++ moderno (usare)Differenza chiave
malloc(size)new Tiponew chiama il costruttore; malloc no
calloc(n, size)new Tipo[n]new[] inizializza gli oggetti
realloc(ptr, size)non ha equivalente sicuro in C++
free(ptr)delete ptrdelete chiama il distruttore; free no

In questo corso usiamo C++ — new e delete sono la scelta corretta.

Dichiarazione e operatori fondamentali

💡 Solo dimostrativo Lo snippet seguente serve esclusivamente a mostrare cosa restituisce ogni operazione sui puntatori. Nella pratica non si usa un puntatore per modificare una variabile locale già disponibile direttamente: si scriverebbe semplicemente x = 100. L'utilità reale dei puntatori emerge con new, le struct e le funzioni.
C++ · puntatori_base.cpp
#include <iostream>
using namespace std;
int main() {
    int x = 42;
    int* p = &x;  // p ora contiene l'indirizzo di memoria di x

    cout << x    << endl;  // 42       — valore di x letto direttamente
    cout << &x   << endl;  // 0x...    — indirizzo di x in memoria (esadecimale)
    cout << p    << endl;  // 0x...    — contenuto di p: lo stesso indirizzo di &x
    cout << *p   << endl;  // 42       — *p: vai all'indirizzo in p e leggi il valore

    *p = 100;  // vai all'indirizzo in p (= &x) e SCRIVI 100 in quella cella.
              // Equivale a "x = 100" ma fatto tramite puntatore.
              // Questo dimostra la dereferenziazione in scrittura.
    cout << x    << endl;  // 100      — x è cambiato perché p puntava a lui
    return 0;
}

Allocazione dinamica — new e delete

C++ · new e delete
int* p = new int;       // riserva spazio per un int in heap, restituisce l'indirizzo
*p = 42;                 // scrivi 42 nella cella appena allocata
cout << *p << endl;      // 42
delete p;                 // OBBLIGATORIO: libera la memoria, altrimenti memory leak
p = nullptr;             // buona prassi: evita uso accidentale dopo delete

int* q = new int(99);   // alloca e inizializza in un solo passo
delete q; q = nullptr;
⚠ Memory leak — la trappola più comune Se si perde il puntatore a una zona di heap senza aver chiamato delete, quella memoria rimane occupata per tutta la durata del programma. Ogni new deve avere il suo delete.

Il puntatore nullptr

C++ · nullptr
int* p = nullptr;   // puntatore che non punta a nessun indirizzo valido
if (p != nullptr) {
    cout << *p;     // sicuro: accediamo solo se p è valido
}
// dereferenziare nullptr = segmentation fault a runtime

Il nodo

Un nodo è la struttura elementare da cui si costruiscono tutte le strutture dinamiche. È una struct con due campi: il dato da memorizzare e un puntatore al nodo successivo. La struct si riferisce a sé stessa — struttura ricorsiva.

Definizione e creazione

C++ · nodo.cpp
struct Nodo {
    int   valore;   // campo dati: il valore intero memorizzato nel nodo
    Nodo* next;     // puntatore al nodo successivo (nullptr = fine catena)
};                  // la struct si riferisce a sé stessa: struttura ricorsiva

Nodo* n1 = new Nodo;   // alloca un nodo in heap, restituisce l'indirizzo → n1
n1->valore = 15;       // tramite n1, scrivi 15 nel campo valore di quel nodo
n1->next   = nullptr;  // tramite n1, nessun successore per ora

Nodo* n2 = new Nodo;   // alloca un secondo nodo in heap → n2
n2->valore = 8;        // scrivi 8 nel campo valore di n2
n2->next   = nullptr;  // n2 è l'ultimo nodo: nessun successore

n1->next = n2;          // collega i nodi: n1→next ora punta a n2 → catena n1→n2→nullptr

cout << n1->valore        << endl;  // legge n1.valore        → 15
cout << n1->next->valore  << endl;  // n1->next è n2, quindi n2.valore → 8
📌 Il punto . e la freccia -> Con variabili struct normali si usa il punto: s.valore. Con puntatori si usa la freccia: p->valore, equivalente a (*p).valore. Nella pratica si usa sempre -> con i puntatori.

Iterazione su una catena

C++ · percorrere una catena
Nodo* curr = testa;   // curr inizia dalla testa della lista
while (curr != nullptr) {   // continua finché non si raggiunge la fine
    cout << curr->valore << " ";
    curr = curr->next;        // avanza: curr ora punta al nodo successivo
}
// il ciclo termina quando curr è nullptr: nessun successore

Lista concatenata

Una lista concatenata è una sequenza di nodi in cui ogni nodo punta al successivo. Non occupa celle contigue in memoria: ogni nodo vive in un punto qualsiasi dell'heap. Il punto di accesso è il puntatore testa. Se testa == nullptr la lista è vuota.

💡 Vantaggio chiave rispetto all'array Inserire in testa a una lista è O(1): basta far puntare il nuovo nodo al vecchio primo e aggiornare testa. In un array la stessa operazione è O(n) perché richiede di spostare tutti gli elementi. Di contro, accedere all'i-esimo elemento della lista è O(n): non esiste l'indice diretto.

Implementazione completa

C++ · lista_concatenata.cpp
#include <iostream>
using namespace std;
struct Nodo { int valore; Nodo* next; };

// inserimento in testa — O(1)
void inserisciTesta(Nodo*& testa, int val) {
    Nodo* nuovo  = new Nodo;
    nuovo->valore = val;
    nuovo->next   = testa;  // nuovo → vecchio primo
    testa         = nuovo;
}

// inserimento in coda — O(n)
void inserisciCoda(Nodo*& testa, int val) {
    Nodo* nuovo = new Nodo;
    nuovo->valore = val; nuovo->next = nullptr;
    if (testa == nullptr) { testa = nuovo; return; }
    Nodo* curr = testa;
    while (curr->next != nullptr) curr = curr->next;
    curr->next = nuovo;
}

// inserimento ordinato (crescente) — O(n)
void inserisciOrdinato(Nodo*& testa, int val) {
    Nodo* nuovo = new Nodo;
    nuovo->valore = val; nuovo->next = nullptr;
    if (testa == nullptr || val <= testa->valore) {
        nuovo->next = testa; testa = nuovo; return;
    }
    Nodo* curr = testa;
    while (curr->next != nullptr && curr->next->valore < val)
        curr = curr->next;  // confronto su curr->NEXT->valore, non curr->valore
    nuovo->next = curr->next;  // PRIMA: nuovo punta al resto (due puntatori allo stesso nodo)
    curr->next  = nuovo;       // DOPO: curr punta a nuovo (se invertissi perderei il resto!)
}

// cancellazione per valore — O(n)
void cancella(Nodo*& testa, int val) {
    if (testa == nullptr) return;
    if (testa->valore == val) {
        Nodo* tmp = testa; testa = testa->next; delete tmp; return;
    }
    Nodo* curr = testa;
    while (curr->next != nullptr && curr->next->valore != val)
        curr = curr->next;
    if (curr->next != nullptr) {
        Nodo* tmp = curr->next;
        curr->next = tmp->next;
        delete tmp;
    }
}

void libera(Nodo*& testa) {
    while (testa != nullptr) {
        Nodo* tmp = testa; testa = testa->next; delete tmp;
    }
}
💡 L'ordine di esecuzione nell'inserimento ordinato è critico Si esegue nuovo→next = curr→next prima di curr→next = nuovo. Perché? Subito dopo il primo passo, sia curr→next che nuovo→next puntano allo stesso nodo successivo — il riferimento al resto della lista è al sicuro. Solo allora possiamo sovrascrivere curr→next. Invertire i due passi causa la perdita permanente del resto della lista — bug classico da esame.

🔧 Lista Interattiva — passo per passo

Il nodo nuovo viene mostrato staccato prima di essere collegato. Il puntatore ▼ testa è sempre visibile. Per l'inserimento ordinato, il passo 2 mostra esplicitamente i due puntatori che raggiungono lo stesso nodo.

🔧 Lista Concatenata testa → [val|→] → [val|→] → nullptr
Lista vuota — testa = nullptr
Inserisci un valore per iniziare.

Stack con nodi

Lo stack con allocazione dinamica usa la stessa logica LIFO della versione su array, ma ogni elemento è un nodo allocato con new. push aggiunge un nodo davanti a top, pop lo rimuove e chiama delete. Non esistono MAXisFull().

Confronto con la versione su array

CaratteristicaStack su arrayStack con nodi
CapacitàFissa (MAX compilata)Limitata solo dalla RAM
isFull()NecessariaNon esiste
MemoriaMAX × sizeof(int) sempreSolo gli elementi inseriti
Overhead push/popO(1) puroO(1) + new/delete

Implementazione in C++

C++ · stack_nodi.cpp
struct Nodo { int valore; Nodo* next; };
struct Stack { Nodo* top; };

void init(Stack& s)    { s.top = nullptr; }
bool isEmpty(Stack& s) { return s.top == nullptr; }

void push(Stack& s, int val) {
    Nodo* nuovo   = new Nodo;
    nuovo->valore = val;
    nuovo->next   = s.top;  // nuovo punta al vecchio top
    s.top         = nuovo;
}

int pop(Stack& s) {
    if (isEmpty(s)) { cout << "Errore!" << endl; return -1; }
    int   val = s.top->valore;
    Nodo* tmp = s.top;
    s.top     = s.top->next;
    delete tmp;
    return val;
}

🔧 Prova le operazioni (max 6 nodi)

🔧 Stack con nodiLIFO — top → [val|↓] → nullptr
top = nullptr
isEmpty() = vero
Pila inizializzata — top = nullptr.

Coda con nodi

La coda dinamica è FIFO: si inserisce in fondo e si estrae dalla testa. Due puntatori separati (testa e coda) tengono traccia degli estremi della catena di nodi — non una struct separata, solo due puntatori passati per riferimento alle funzioni.

Confronto con la versione su array

CaratteristicaCoda su arrayCoda con nodi
CapacitàFissa (MAX compilata)Limitata solo dalla RAM
isFull()NecessariaNon esiste
Riutilizzo spazioRichiede coda circolareOgni nodo si dealloca dopo dequeue

Implementazione in C++

C++ · coda_nodi.cpp
#include <iostream>
using namespace std;

/* Nodo: il mattone base, identico alla lista concatenata */
struct Nodo {
    int   valore;  // dato memorizzato
    Nodo* next;    // puntatore al successivo (nullptr = fine catena)
};

/* Non serve una struct Coda: bastano due puntatori separati.
   testa → nodo da cui si estrae (dequeue) — avanza verso destra
   coda  → nodo dove si inserisce (enqueue) — avanza verso destra
   Le funzioni li ricevono per riferimento (&) perché devono modificarli nel chiamante. */

bool isEmpty(Nodo* testa) {
    return testa == nullptr;   // coda vuota se testa non punta a nessun nodo
}

void enqueue(Nodo*& testa, Nodo*& coda, int val) {
    Nodo* nuovo   = new Nodo;  // alloca il nodo in heap
    nuovo->valore = val;         // inizializza il dato
    nuovo->next   = nullptr;    // è l'ultimo: nessun successore
    if (isEmpty(testa)) {
        testa = coda = nuovo;    // primo nodo: testa e coda coincidono
        return;
    }
    coda->next = nuovo;          // aggancia nuovo in fondo alla catena
    coda       = nuovo;          // coda avanza al nuovo ultimo nodo
}

int dequeue(Nodo*& testa, Nodo*& coda) {
    if (isEmpty(testa)) { cout << "Errore!" << endl; return -1; }
    int   val  = testa->valore;  // salva il valore da restituire
    Nodo* tmp  = testa;          // segnaposto: serve per fare delete dopo
    testa      = testa->next;    // testa avanza al nodo successivo
    if (testa == nullptr)
        coda = nullptr;         // coda svuotata: anche coda → nullptr (altrimenti punta a memoria liberata!)
    delete tmp;                  // libera il nodo rimosso — FONDAMENTALE
    return val;
}

int main() {
    Nodo* testa = nullptr;       // coda vuota all'inizio
    Nodo* coda  = nullptr;

    enqueue(testa, coda, 5);     // testa → [5|∅] ← coda
    enqueue(testa, coda, 3);     // testa → [5|→] → [3|∅] ← coda
    enqueue(testa, coda, 8);     // testa → [5|→] → [3|→] → [8|∅] ← coda
    cout << dequeue(testa, coda) << endl;  // estrae 5, delete, testa avanza a [3]
    cout << dequeue(testa, coda) << endl;  // estrae 3, delete, testa avanza a [8]
    return 0;
}

🔧 Prova le operazioni (max 7 nodi)

Ogni enqueue e dequeue è mostrato passo per passo: vedi il nodo nuovo staccato prima del collegamento, e il puntatore tmp isolato prima del delete.

🔧 Coda con nodi FIFO — testa → [val|→] → … → [val|∅] ← coda
Coda vuota — testa = nullptr, coda = nullptr
testa = nullptr  |  coda = nullptr
Coda inizializzata — testa = nullptr, coda = nullptr.