Il C è un linguaggio compilato: non viene eseguito nulla finché un compilatore non ha tradotto il tuo sorgente in codice macchina per la tua CPU e il tuo sistema operativo. Questo passaggio è un solo comando, ma sapere cosa fa trasforma la maggior parte dei messaggi di errore da misteriosi in ovvi.
I due comandi
Salva questo come hello.c:
#include <stdio.h>
int main(void) {
printf("Ciao, C!\n");
return 0;
}
Poi, nella stessa cartella:
gcc hello.c -o hello
./hello
Il primo comando compila. Il secondo esegue il risultato. Su Windows il comando per eseguire è hello nel Prompt dei comandi o .\hello in PowerShell: non c'è il prefisso ./, perché Windows cerca nella directory corrente per impostazione predefinita e le shell Unix volutamente no.
Se ometti -o hello, gcc chiama l'output a.out (o a.exe), ed è per questo che i tutorial più vecchi finiscono con ./a.out. Dai sempre un nome all'output: costa quattro caratteri e risparmia confusione.
Ecco lo stesso programma nell'editor del browser, che fa entrambi i passaggi per te:
Cosa fa davvero la "compilazione"
gcc hello.c -o hello sembra un solo passaggio. In realtà sono quattro, e ognuno può fallire con un suo tipo di errore.
1. Preprocessing. Prima che venga compilato qualsiasi codice C, il preprocessore gestisce ogni riga che inizia con #. #include <stdio.h> viene letteralmente sostituito dal contenuto di quel file header; le macro #define vengono espanse; i blocchi #ifdef vengono mantenuti o eliminati. L'output è un unico grande file C senza più righe #. Puoi vederlo:
gcc -E hello.c
Stampa centinaia di righe: quasi tutte sono stdio.h incollato dentro.
2. Compilazione. Il C preprocessato viene analizzato, controllato nei tipi e tradotto in assembly per la tua CPU. È da qui che arrivano errori di sintassi, errori di tipo e avvisi.
3. Assemblaggio. L'assembly diventa un file oggetto: codice macchina con dei segnaposto dove vanno le chiamate alle funzioni di altri file.
gcc -c hello.c # produce hello.o, si ferma prima del linking
4. Linking. I file oggetto vengono cuciti insieme alla libreria standard del C, ogni segnaposto viene riempito con un indirizzo reale e il risultato è un eseguibile. È da qui che arrivano gli errori "undefined reference".
La lezione pratica: un errore di compilazione indica una riga del tuo sorgente, un errore di linking no, perché il linking avviene dopo che ogni riga è già stata accettata.
Flag da usare fin dal primo giorno
Il semplice gcc file.c -o prog accetta in silenzio molto codice pericoloso. Quattro flag cambiano le cose.
gcc -std=c17 -Wall -Wextra -g hello.c -o hello
-Wallattiva gli avvisi comuni. Nonostante il nome non sono "tutti" gli avvisi: è l'insieme ragionevole.-Wextrane aggiunge altri, tra cui i parametri inutilizzati e alcuni errori nei confronti.-std=c17fissa lo standard del linguaggio, così il tuo codice significa la stessa cosa su ogni macchina. Usa-std=c99se segui materiale più vecchio.-gconserva le informazioni di debug, cosìgdbolldbpossono mostrare le righe reali del sorgente quando qualcosa va in crash.
Altri due che prima o poi vorrai:
-O2attiva l'ottimizzazione per le build di release. Lascialo spento mentre impari: il codice ottimizzato è più difficile da debuggare e gli avvisi a volte cambiano.-fsanitize=address,undefined(gcc e clang) fa terminare il programma con un messaggio chiaro nel momento in cui legge fuori dai limiti o incontra un comportamento indefinito. È il flag più utile in assoluto per imparare il C.
Prova tu stesso gli avvisi. Questo programma compila e gira, ma ha due bug reali:
A count non viene mai dato un valore, quindi il programma stampa i byte che si trovavano lì per caso. Con -Wall il compilatore lo dice: 'count' is used uninitialized. In C gli avvisi non sono quasi mai rumore: trattali come errori che non hai ancora incontrato.
Compilare più di un file
I programmi reali sono divisi in più file. Passali tutti a gcc:
gcc -std=c17 -Wall main.c utils.c -o myprog
Oppure compila ciascuno separatamente e collega alla fine, che è quello che fanno i sistemi di build perché modificare un file non ricompili tutto:
gcc -c main.c # -> main.o
gcc -c utils.c # -> utils.o
gcc main.o utils.o -o myprog
Alcune librerie richiedono un flag di linking esplicito. La libreria matematica è quella che ogni principiante incontra:
gcc calc.c -o calc -lm
Senza -lm, usare sqrt da math.h compila senza problemi e poi fallisce al linking con undefined reference to sqrt: la dichiarazione era nell'header, ma il codice stava in una libreria che nessuno ha richiesto.
Leggere gli errori
Gli errori del C sono stringati ma coerenti. Tre esempi coprono quasi tutto quello che vedrai all'inizio.
Un punto e virgola mancante viene segnalato sulla riga successiva, perché il compilatore ha continuato a leggere:
hello.c:5:5: error: expected ';' before 'return'
La correzione va sulla riga 4, non sulla 5. Ogni volta che un errore indica una riga che sembra corretta, controlla quella precedente.
Un header mancante sembra un mistero su una funzione che hai chiaramente scritto bene:
hello.c:4:5: warning: implicit declaration of function 'printf'
Significa che il compilatore non ha mai visto una dichiarazione di printf, quindi hai dimenticato #include <stdio.h>. Da C99 in poi è un errore, non solo un avviso.
Un fallimento del linking non ha alcun numero di riga:
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x1a): undefined reference to `helper'
Il compilatore ti ha creduto sul fatto che helper esistesse da qualche parte; il linker ha cercato e non c'è. O non hai mai scritto la definizione, o l'hai scritta in modo diverso, o hai dimenticato di passare a gcc il file che la contiene.
Correggi sempre il primo errore. Gli errori del C si propagano a cascata: una riga sbagliata può generare venti messaggi, e diciannove spariscono quando correggi il primo.
Codici di uscita
return 0 in main non è decorazione. È lo stato di uscita del programma, e la shell può leggerlo:
Dopo l'esecuzione, echo $? su macOS/Linux (o echo %errorlevel% su Windows) stampa quel numero. Script e strumenti di build lo usano per decidere se proseguire. La convenzione è assoluta: 0 è successo, e un valore diverso da zero è un codice di errore scelto da te. stdlib.h definisce EXIT_SUCCESS e EXIT_FAILURE se preferisci i nomi.
Da C99, arrivare alla fine di main senza un return equivale a return 0, ma scriverlo è più chiaro ed è obbligatorio per tutte le altre funzioni.
Domande frequenti
Come si compila ed esegue un programma C?
Salva il codice come hello.c, poi esegui gcc hello.c -o hello per compilarlo e ./hello per avviarlo (solo hello su Windows). Se gcc non viene trovato, prima devi installare un compilatore.
Cosa fa gcc -o?
-o dà un nome al file di output. gcc hello.c -o hello produce un eseguibile chiamato hello. Senza -o, gcc scrive in a.out (o a.exe su Windows), ed è per questo che tanti vecchi tutorial eseguono ./a.out.
Quali flag di gcc dovrei usare sempre?
gcc -std=c17 -Wall -Wextra -g yourfile.c -o yourprog. -Wall -Wextra attivano gli avvisi che scovano bug reali, -std=c17 fissa la versione del linguaggio e -g conserva i simboli di debug così un debugger può mostrarti il sorgente. Aggiungi -O2 quando vuoi velocità in una build di release.
Cosa significa "undefined reference to" in C?
È un errore del linker: il compilatore ha accettato la chiamata a una funzione, ma non è stata trovata alcuna definizione di quella funzione. Le cause comuni sono un errore di battitura nel nome, aver dimenticato di compilare un secondo file .c o aver usato una funzione matematica senza collegare la libreria matematica (-lm).