Cos'è il linguaggio C e perché è ancora ovunque
Cos'è il C: la gestione manuale della memoria, i puntatori, la compilazione, e perché un linguaggio degli anni Settanta è ancora sotto quasi tutto.
Ogni volta che leggi la classifica dei linguaggi più usati trovi il C nelle prime posizioni, e la cosa non torna: nessuno intorno a te ci scrive applicazioni, non lo vedi nelle offerte di lavoro per il web, e ha più di cinquant'anni. In questo articolo trovi cos'è davvero, perché continua a stare sotto quasi tutto il software che usi, e cosa cambia nella testa di chi lo impara.
Cos'è il linguaggio C
Il C è un linguaggio compilato di basso livello creato nel 1972 ai Bell Laboratories da Dennis Ritchie, pensato per riscrivere il sistema operativo Unix in qualcosa di più leggibile dell'assembly senza però rinunciare al controllo diretto della memoria e dell'hardware.
Quella doppia natura è tutto il punto. Il C sta esattamente a metà: ha funzioni, variabili e strutture di controllo come un linguaggio moderno, ma non nasconde quasi niente di ciò che la macchina fa davvero. Non c'è un garbage collector che libera la memoria al posto tuo, non c'è una macchina virtuale in mezzo, non c'è un runtime che controlla se stai uscendo dai limiti di un array.
Viene spesso descritto come "assembly portabile", e la definizione è azzeccata: scrivi codice che resta leggibile, ma resti responsabile di ogni byte.
Perché è ancora ovunque
Non è nostalgia, è una conseguenza tecnica. Il C è finito nelle fondamenta e le fondamenta non si riscrivono per moda.
- I sistemi operativi. Il kernel Linux è in C, e con lui Android nella sua parte profonda. Anche i nuclei di Windows e macOS hanno una base C/C++ enorme.
- Gli interpreti degli altri linguaggi. CPython, l'implementazione di riferimento di Python, è scritta in C. Il motore V8 che fa girare JavaScript è C++, che del C è un discendente diretto.
- I database. PostgreSQL, MySQL, SQLite sono in C. Quando scrivi una query SQL, a eseguirla è codice C.
- L'embedded. Microcontrollori, centraline, elettrodomestici, dispositivi medici: lì il C è ancora la scelta principale, perché spesso hai pochi kilobyte di memoria e nessun sistema operativo sotto.
- Le librerie condivise. Compressione, crittografia, grafica, rete: le librerie fondamentali sono in C, e tutti gli altri linguaggi le richiamano.
C'è un motivo per cui succede proprio col C e non con un altro: quasi ogni linguaggio sa parlare con il C. L'interfaccia per chiamare funzioni esterne, in Python, in Ruby, in Java, in Go, è modellata sulle convenzioni del C. È diventato la lingua franca tra linguaggi diversi, e questo ruolo da solo gli garantisce altri decenni di vita.
Come funziona la compilazione
In Python o JavaScript scrivi il file e lo esegui. Qui no: tra il tuo file di testo e il programma che gira ci sono quattro passaggi distinti, e conoscerli serve perché gli errori arrivano da fasi diverse.
#include <stdio.h>
int main(void) {
printf("Ciao\n");
return 0;
}
gcc -Wall -Wextra -o ciao ciao.c
./ciao
Dentro quel singolo comando succedono quattro cose:
- Preprocessore — esegue le direttive che iniziano con
#.#includeincolla letteralmente il contenuto di un altro file,#definesostituisce testo. Lavora sul testo, non conosce il linguaggio. - Compilazione — traduce il C in assembly per la tua architettura.
- Assemblaggio — trasforma l'assembly in codice macchina, producendo file oggetto.
- Collegamento — unisce i file oggetto e le librerie in un eseguibile unico.
Da qui discende una cosa che confonde chi inizia: gli errori di compilazione e gli errori di collegamento sono animali diversi. undefined reference to 'foo' non significa che hai sbagliato la sintassi, significa che hai dichiarato una funzione e non l'hai mai definita, o che hai dimenticato di passare una libreria al linker.
Un consiglio pratico che vale più di molti tutorial: compila sempre con -Wall -Wextra attivi. Il C, per scelta storica, accetta in silenzio parecchie cose discutibili. Gli avvisi del compilatore sono la prima e più economica rete di sicurezza che hai, e ignorarli è il modo più comune per passare una serata a cercare un bug che il compilatore ti aveva già segnalato.
I puntatori: il concetto che divide in due
È lo scoglio, e conviene affrontarlo senza mistificazioni: un puntatore è semplicemente una variabile che contiene un indirizzo di memoria invece di un valore.
int x = 42;
int *p = &x; // p contiene l'indirizzo di x
printf("%d\n", x); // 42
printf("%d\n", *p); // 42, leggo il valore all'indirizzo
*p = 7; // scrivo lì
printf("%d\n", x); // 7, ho modificato x
L'idea è banale. Quello che la rende difficile è che il C usa i puntatori per fare cose che altrove sono funzionalità separate del linguaggio: passare un parametro modificabile, restituire più valori, scorrere un array, costruire strutture dinamiche, gestire le stringhe, chiamare funzioni in modo indiretto.
E soprattutto gli array sono puntatori mascherati. array[i] è letteralmente un'altra scrittura per *(array + i). Il C non memorizza da nessuna parte quanto è lungo un array: se scrivi oltre la fine, il linguaggio non se ne accorge e va tranquillamente a sovrascrivere la memoria che segue. Quel comportamento ha un nome, buffer overflow, ed è una delle categorie di vulnerabilità più sfruttate della storia dell'informatica — il tema è centrale in cos'è la cybersecurity.
La memoria la gestisci tu
L'altra metà del C. Esistono due zone di memoria con regole diverse:
Lo stack è automatico. Le variabili locali di una funzione nascono quando la funzione parte e spariscono quando finisce. Non devi fare niente, ma è limitato e la dimensione va decisa in fase di compilazione.
L'heap è manuale. Lo usi quando la dimensione la conosci solo a programma avviato, o quando un dato deve sopravvivere alla funzione che l'ha creato.
int *numeri = malloc(n * sizeof(int));
if (numeri == NULL) {
return 1; // controlla SEMPRE: malloc può fallire
}
// ... usa numeri ...
free(numeri);
numeri = NULL; // evita di riusare un puntatore morto
Ogni malloc deve avere il suo free, una volta sola, né zero né due. Sbagliare produce tre problemi con nomi precisi:
| Errore | Cosa succede |
|---|---|
| Memory leak | Non liberi: il programma consuma memoria finché non collassa |
| Use after free | Usi memoria già liberata: comportamento imprevedibile, spesso sfruttabile |
| Double free | Liberi due volte: corrompi le strutture interne dell'allocatore |
Qui sta la differenza culturale che conta. In un linguaggio con garbage collector questi errori non esistono: non sono difficili, sono impossibili. Il C ti dà in cambio prestazioni prevedibili e nessun momento in cui il programma si ferma per pulire — che su un dispositivo medico o in un kernel non è un dettaglio.
E c'è un punto che spiega bene perché Rust esiste: Rust affronta esattamente questi errori spostandoli in fase di compilazione, senza garbage collector. Chi ha capito i problemi del C capisce Rust in un pomeriggio; chi non li ha mai incontrati trova il borrow checker arbitrario e vessatorio.
Il comportamento indefinito
C'è una caratteristica del C che non ha equivalenti nei linguaggi moderni e che va spiegata, perché è la causa dei bug più frustranti.
La specifica del linguaggio dichiara certe operazioni comportamento indefinito: leggere una variabile non inizializzata, uscire dai limiti di un array, dereferenziare un puntatore nullo, far traboccare un intero con segno. In quei casi lo standard non dice "il programma va in errore". Dice che può succedere qualsiasi cosa.
La conseguenza pratica è sgradevole: il programma può funzionare benissimo per mesi, e poi rompersi quando cambi compilatore, alzi il livello di ottimizzazione o sposti una variabile. Non hai introdotto il bug, era già lì e si vedeva solo con un certo allineamento degli astri.
L'unica difesa seria sono gli strumenti. Compila con -fsanitize=address,undefined durante lo sviluppo e usa Valgrind: trovano in pochi secondi cose che a occhio non vedi in una settimana.
C e C++ non sono la stessa cosa
Malinteso diffuso, vale la pena chiuderlo. C++ è nato come estensione del C ma è diventato un linguaggio enorme e con una filosofia diversa: classi, template, eccezioni, RAII, libreria standard ricchissima.
Il C è rimasto piccolo di proposito. Un programmatore C esperto tiene tutto il linguaggio in testa — cosa che in C++ non fa nessuno. Non sono due livelli dello stesso percorso: sono due strumenti con obiettivi diversi. Se ti serve controllo assoluto in poco spazio, C. Se ti serve costruire astrazioni complesse senza pagarle a runtime, C++.
Errori comuni di chi inizia
Confrontare stringhe con ==. Confronta gli indirizzi, non il contenuto. Serve strcmp. In C la stringa non è un tipo: è un array di caratteri che finisce con \0, e quasi tutti i problemi con le stringhe nascono da quel terminatore mancante.
Usare scanf e gets senza precauzioni. gets è stato rimosso dallo standard perché non c'è modo sicuro di usarlo. Con scanf("%s") chiunque può scrivere oltre il tuo buffer. Usa fgets con una dimensione esplicita.
Restituire l'indirizzo di una variabile locale. Quella memoria non esiste più quando la funzione termina. Il compilatore con -Wall te lo dice, ed è un'altra ragione per tenerlo acceso.
Confondere sizeof su array e puntatori. Su un array ti dà la dimensione totale; passa quell'array a una funzione e diventa un puntatore, quindi sizeof ti dà la dimensione del puntatore. La lunghezza va passata a parte, sempre.
Dimenticare break in uno switch. L'esecuzione prosegue nel caso successivo. A volte lo vuoi, molto più spesso è un bug.
Fidarsi dell'assenza di errori a runtime. Se il programma non segnala niente non significa che sia corretto: significa che il C non controlla. Gli strumenti citati sopra esistono per questo.
Vale la pena impararlo oggi?
Dipende da cosa vuoi fare, e la risposta onesta ha due parti.
Come strumento di lavoro quotidiano, per la maggior parte delle persone no. Se costruisci siti, applicazioni gestionali, prodotti web o app mobile, i linguaggi giusti sono altri — la mappa completa è in tutti i linguaggi di programmazione. Scrivere in C qualcosa che potresti scrivere in Python significa pagare settimane di lavoro e una superficie di bug molto più ampia per un vantaggio che quasi sempre non ti serve.
Come primo linguaggio in assoluto, quasi mai. Ti chiede di gestire memoria e puntatori prima ancora di aver capito cos'è un ciclo. Per iniziare da zero il percorso sensato è quello descritto in imparare a programmare da zero.
Come secondo o terzo linguaggio, invece, ha un valore formativo che pochi altri hanno: dopo il C sai perché un accesso sequenziale a un array è più veloce di uno casuale, cosa succede davvero quando passi un oggetto a una funzione, perché uno stack overflow si chiama così. Quel discorso è approfondito in perché imparare il linguaggio C.
Come strumento di lavoro specifico, è tutt'altro che opzionale: embedded, firmware, kernel, sistemi in tempo reale, reverse engineering e sicurezza. Lì non è una scelta tra alternative, è il territorio del C.
In sintesi
Il C è un linguaggio piccolo e senza rete di sicurezza, nato per scrivere sistemi operativi: ti dà controllo diretto su memoria e hardware e in cambio non ti protegge da niente, nemmeno dai tuoi errori più banali.
È ancora ovunque per una ragione strutturale, non affettiva: sta nei kernel, negli interpreti degli altri linguaggi, nei database e nell'embedded, ed è la lingua franca con cui i linguaggi diversi si parlano. Non lo vedi perché sta sotto, non perché sia scomparso.
I tre concetti che devi davvero possedere sono i puntatori come indirizzi, la distinzione tra stack e heap con la gestione manuale della memoria, e il comportamento indefinito — quest'ultimo è ciò che rende il C diverso da qualsiasi linguaggio moderno tu abbia usato.
Il consiglio pratico: se non lavori su sistemi, imparalo per capire, non per produrre. Un mese speso a scrivere qualche struttura dati in C cambia il modo in cui leggi il codice in ogni altro linguaggio, e quel guadagno non scade mai.
Se stai ancora decidendo da dove partire, la panoramica completa è in tutti i linguaggi di programmazione, mentre quale linguaggio imparare nel 2026 entra nel merito della scelta in base all'obiettivo che hai.