È uscito il Corso Java Completo
Torna al blog

Cos'è C++ e quando si usa ancora

Cos'è il C++, come funziona la gestione manuale della memoria, cosa sono RAII e smart pointer, dove si usa oggi davvero e come si confronta con Rust.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

10 min di lettura

Il C++ ha una reputazione strana: tutti sanno che è difficile, pochi sanno dire dove venga effettivamente usato oggi, e quasi tutti se lo ricordano dal corso universitario in una forma che nessun professionista usa più. In questo articolo trovi cos'è davvero, cosa comporta gestire la memoria a mano, cosa sono RAII e smart pointer, e quando ha ancora senso sceglierlo.

Cos'è

C++ è un linguaggio compilato in codice macchina nativo che ti dà controllo diretto sulla memoria e sull'hardware, senza garbage collector e senza runtime che ti protegga dagli errori.

Nasce come "C con le classi" all'inizio degli anni Ottanta, ma da allora ha accumulato generici, eccezioni, una libreria standard enorme e strumenti che rendono il codice moderno quasi irriconoscibile rispetto a quello di vent'anni fa.

Il rapporto con il C è di parentela, non di identità: il C è un linguaggio piccolo che si impara per capire come funziona la macchina — il ragionamento è in perché imparare il C — mentre il C++ è un linguaggio grande che si usa per costruire sistemi complessi mantenendo quel controllo.

Potenza massima, nessuna rete di sicurezza

Il punto centrale, da cui dipende tutto il resto: in C++ la memoria la gestisci tu.

In Java, C# o Python, quando crei un oggetto qualcuno tiene il conto di chi lo usa e lo elimina quando non serve più: costa memoria e pause imprevedibili quando entra in azione. In C++ non c'è nessuno. Chiedi memoria, la usi, la restituisci.

int* dati = new int[1000];   // chiedo memoria
// ... uso dati ...
delete[] dati;               // la restituisco

Questo ti permette cose impossibili altrove: decidere dove stanno i dati perché finiscano nella cache del processore, riusare un blocco già allocato, garantire che una certa operazione non allochi mai e quindi non abbia mai latenze impreviste.

E ti permette anche di rompere tutto in modi difficili da trovare. Le tre categorie classiche:

  • Perdita di memoria: dimentichi la delete e il programma cresce finché non viene ucciso dal sistema.
  • Uso dopo il rilascio: liberi la memoria e poi ci accedi ancora. Spesso funziona, perché quei byte sono ancora lì. Finché qualcun altro non li riusa, e allora leggi dati altrui o corrompi i suoi.
  • Sforamento del buffer: scrivi oltre la fine di un array. Sovrascrivi qualcos'altro. Il programma continua e fallisce molto dopo, in un punto senza relazione con la causa.

Il tratto comune è il peggiore: il programma spesso non si ferma dove sta l'errore. Va avanti in uno stato corrotto e si schianta altrove, magari solo in produzione e sotto carico. È per questo che il C++ ha la fama che ha, e per questo che strumenti come AddressSanitizer e Valgrind non sono opzionali.

RAII: l'idea che cambia tutto

Il C++ moderno non risolve questo problema chiedendoti di essere più attento. Lo risolve con un meccanismo, e si chiama RAII: Resource Acquisition Is Initialization.

L'idea in una frase: una risorsa viene acquisita quando un oggetto nasce e rilasciata automaticamente quando quell'oggetto esce di scena.

Questo funziona perché il C++ garantisce una cosa precisa: quando l'esecuzione esce da un blocco, il distruttore di ogni oggetto locale viene chiamato. Sempre. Anche se esci con un return anticipato, anche se viene lanciata un'eccezione.

Il confronto rende l'idea concreta. Con la gestione manuale:

void elabora() {
    std::FILE* f = std::fopen("dati.txt", "r");
    if (!f) return;

    if (!leggi(f)) {
        std::fclose(f);   // devo ricordarmi qui
        return;
    }
    if (!valida(f)) {
        return;           // DIMENTICATO: il file resta aperto
    }
    std::fclose(f);
}

Ogni via d'uscita dalla funzione è un'occasione per dimenticare la pulizia. E se leggi() lancia un'eccezione, non c'è nemmeno una riga in cui avresti potuto metterla.

Con RAII, la responsabilità passa dal programmatore al tipo:

void elabora() {
    std::ifstream f("dati.txt");   // aperto alla costruzione
    if (!f) return;

    if (!leggi(f))  return;   // chiuso
    if (!valida(f)) return;   // chiuso
                              // chiuso anche con un'eccezione
}

std::ifstream chiude il file nel proprio distruttore. Non c'è modo di dimenticarlo, perché non c'è nulla da ricordare.

Gli smart pointer applicano RAII alla memoria. Sono gli stessi puntatori di prima, avvolti in un oggetto che sa quando liberare:

// proprietà esclusiva: un solo proprietario, liberato alla fine dello scope
auto config = std::make_unique<Configurazione>("app.ini");

// proprietà condivisa: si libera quando l'ultimo riferimento sparisce
auto texture = std::make_shared<Texture>("sfondo.png");

La regola pratica del C++ moderno è netta: new e delete non si scrivono quasi mai nel codice applicativo. Usi std::vector invece degli array grezzi, std::string invece di char*, smart pointer invece di puntatori proprietari. Il costo a runtime di unique_ptr rispetto a un puntatore grezzo è zero — il controllo lo fa il compilatore, non la macchina.

Questo non elimina tutti i problemi. I riferimenti a oggetti già distrutti restano possibili, e shared_ptr può creare cicli che non si liberano mai. Riduce drasticamente la superficie d'errore, non la azzera.

Dove si usa davvero oggi

AmbitoPerché il C++
Motori di videogiochiUnreal Engine, Unity nel suo nucleo, Godot: serve controllo totale sul tempo per fotogramma
Calcolo scientifico e AII nuclei di TensorFlow e PyTorch sono in C++; Python è l'interfaccia
Sistemi embeddedAutomotive, dispositivi medicali, industriale: risorse limitate, vincoli temporali stretti
Finanza ad alta frequenzaDove la latenza si misura in microsecondi e una pausa del garbage collector costa denaro
Software professionaleAdobe, AutoCAD, software audio e video, motori di rendering

E poi c'è la categoria che sorprende: strumenti che usi senza saperlo. Il motore JavaScript V8 che fa girare Chrome e Node.js è C++. MySQL e MongoDB sono C++. Gran parte di LLVM, il compilatore dietro Rust, Swift e Clang, è C++. Anche chi non scriverà mai una riga di C++ lavora ogni giorno sopra codice C++.

Il criterio per sceglierlo è netto: quando i millisecondi contano davvero, quando i vincoli di memoria sono stretti, o quando esiste già una base di codice C++ a cui agganciarti. Per un backend web o un gestionale non è la scelta giusta: altri linguaggi fanno lo stesso lavoro in un decimo del tempo.

La complessità, detta onestamente

È un linguaggio enorme e nessuno lo conosce tutto. Non è un modo di dire modesto: lo standard supera le mille pagine, e persone che ci lavorano da vent'anni continuano a incontrare angoli che non avevano mai visto. Bjarne Stroustrup, che l'ha inventato, ha detto che dentro il C++ c'è un linguaggio più piccolo e più pulito che cerca di uscire.

"C++ moderno" è un'altra lingua rispetto a quello insegnato all'università. Molti corsi si fermano ai puntatori grezzi, new/delete e array in stile C — cioè esattamente la parte che nel codice professionale si evita. Chi arriva al lavoro con quel bagaglio deve reimparare l'approccio: smart pointer, contenitori della libreria standard, auto, semantica di spostamento, algoritmi standard. È una discontinuità reale, e vale la pena saperlo in anticipo.

Il comportamento indefinito è una categoria a sé. In molte situazioni lo standard non dice cosa succede: il compilatore può fare qualsiasi cosa, incluso ottimizzare via codice che sembrava sensato. Il programma funziona in debug e si rompe in release.

La compilazione è lenta e il sistema di build è un mestiere a parte. Tra CMake, gestione delle dipendenze e tempi che su progetti grandi si misurano in decine di minuti, una parte non banale del lavoro non riguarda il linguaggio. E un errore su una riga che usa la libreria standard può produrre centinaia di righe di diagnostica sui template.

Errori comuni che fanno perdere ore

Copiare grandi oggetti senza accorgersene. Passare un std::vector per valore lo duplica interamente. In un ciclo, questo trasforma un programma veloce in uno lento senza alcun segnale.

void elabora(std::vector<Punto> punti);          // copia tutto
void elabora(const std::vector<Punto>& punti);   // nessuna copia

Restituire un riferimento a una variabile locale. L'oggetto muore all'uscita dalla funzione e il chiamante lavora su memoria che non esiste più. Spesso funziona nei test e fallisce in produzione.

Usare shared_ptr per abitudine. Ha un costo (il conteggio dei riferimenti è atomico) e permette cicli che non si liberano. La scelta predefinita è unique_ptr; shared_ptr si usa quando la proprietà è davvero condivisa.

Invalidare gli iteratori. Modificare un contenitore mentre lo stai percorrendo. Un push_back su un vector può riallocare tutto e rendere invalidi tutti gli iteratori esistenti.

Non compilare con i sanitizer. -fsanitize=address,undefined in fase di sviluppo trova in pochi secondi bug che altrimenti costano giorni: è la singola abitudine che fa più differenza, insieme agli avvisi attivati al massimo con -Wall -Wextra.

C++ e Rust

È il confronto inevitabile, e conviene farlo senza tifo.

Rust risolve per progetto del linguaggio ciò che in C++ dipende dalla disciplina. Il suo compilatore verifica staticamente chi possiede ogni dato e per quanto tempo, e rifiuta di compilare il codice che potrebbe produrre uso dopo il rilascio o accessi concorrenti non sicuri. Non è "il programmatore attento non sbaglia": è "il programma che sbaglia non compila". Su una classe intera di errori, quella che causa la maggior parte delle vulnerabilità di sicurezza nel software di sistema, è una differenza strutturale.

Il rovescio è altrettanto concreto. Il C++ ha quarant'anni di librerie, strumenti, motori grafici e competenze accumulate, compilatori per architetture che Rust non raggiunge, e basi di codice da centinaia di milioni di righe che non verranno riscritte. La curva di apprendimento è diversa, non più bassa: in Rust combatti con il compilatore all'inizio, in C++ con il debugger dopo.

In pratica: per un progetto nuovo di sistema, Rust è una scelta difendibile e sempre più comune. Per lavorare su motori di gioco, software scientifico o qualunque cosa esista già, il C++ non è sostituibile — e non lo sarà per molto tempo.

In sintesi

Il C++ ti dà controllo totale sulla memoria e nessuna rete di sicurezza. È il motivo per cui è veloce e il motivo per cui è pericoloso: sono la stessa proprietà vista da due lati.

RAII è l'idea da capire davvero: legare la vita di una risorsa alla vita di un oggetto. Smart pointer, std::vector e std::string sono applicazioni della stessa regola, e nel codice moderno new e delete quasi non compaiono.

Si usa dove i millisecondi contano: motori di gioco, calcolo, embedded, finanza — e dentro parecchi strumenti che usi ogni giorno senza saperlo.

È enorme e nessuno lo conosce tutto, e il C++ moderno è molto diverso da quello insegnato nei corsi: aspettati di reimparare l'approccio.

Rust sposta nel compilatore ciò che qui è responsabilità tua. È un vantaggio reale sui progetti nuovi; non cancella quarant'anni di ecosistema.

Per collocarlo rispetto a tutti gli altri linguaggi c'è la mappa completa in tutti i linguaggi di programmazione. Se ti interessano i linguaggi con garbage collector della famiglia opposta, vedi cos'è Java e cos'è C#. E dato che qui le prestazioni sono il punto, vale la pena conoscere la notazione O grande e le strutture dati.