La compilazione di un programma C ++ prevede tre passaggi:
Preelaborazione: il preprocessore prende un file di codice sorgente C ++ e si occupa delle direttive #include
s, se #define
altre preprocessori. L'output di questo passaggio è un file C ++ "puro" senza direttive pre-processore.
Compilazione: il compilatore prende l'output del pre-processore e produce da esso un file oggetto.
Collegamento: il linker prende i file oggetto prodotti dal compilatore e produce una libreria o un file eseguibile.
Pre-elaborazione
Il preprocessore gestisce le direttive del preprocessore , come #include
e #define
. È agnostico della sintassi del C ++, motivo per cui deve essere usato con cura.
Funziona su un file C ++ fonte alla volta sostituendo #include
le direttive con il contenuto dei rispettivi file (che di solito è solo dichiarazioni), facendo la sostituzione di macro ( #define
), e la selezione di diverse porzioni di testo a seconda di #if
, #ifdef
e #ifndef
direttive.
Il preprocessore funziona su un flusso di token di preelaborazione. La sostituzione macro è definita come la sostituzione di token con altri token (l'operatore ##
consente di unire due token quando ha senso).
Dopotutto, il preprocessore produce un singolo output che è un flusso di token risultante dalle trasformazioni sopra descritte. Aggiunge inoltre alcuni marcatori speciali che indicano al compilatore da dove proviene ciascuna riga in modo che possa utilizzarli per produrre messaggi di errore sensibili.
Alcuni errori possono essere prodotti in questa fase con un uso intelligente delle direttive #if
e #error
.
Compilazione
La fase di compilazione viene eseguita su ciascun output del preprocessore. Il compilatore analizza il codice sorgente C ++ puro (ora senza direttive per il preprocessore) e lo converte in codice assembly. Quindi richiama il back-end sottostante (assemblatore in toolchain) che assembla quel codice nel codice macchina producendo il file binario effettivo in un formato (ELF, COFF, a.out, ...). Questo file oggetto contiene il codice compilato (in forma binaria) dei simboli definiti nell'input. I simboli nei file oggetto sono indicati per nome.
I file oggetto possono fare riferimento a simboli non definiti. Questo è il caso in cui usi una dichiarazione e non fornisci una definizione per essa. Al compilatore non importa, e produrrà felicemente il file oggetto fintanto che il codice sorgente è ben formato.
I compilatori di solito ti consentono di interrompere la compilazione a questo punto. Questo è molto utile perché con esso puoi compilare ogni file di codice sorgente separatamente. Il vantaggio offerto da questo è che non è necessario ricompilare tutto se si modifica solo un singolo file.
I file oggetto prodotti possono essere inseriti in archivi speciali chiamati librerie statiche, per riutilizzarli più facilmente in seguito.
È a questo punto che vengono segnalati errori "regolari" del compilatore, come errori di sintassi o errori di risoluzione del sovraccarico non riusciti.
Collegamento
Il linker è ciò che produce l'output della compilazione finale dai file oggetto generati dal compilatore. Questo output può essere una libreria condivisa (o dinamica) (e sebbene il nome sia simile, non hanno molto in comune con le librerie statiche menzionate in precedenza) o un eseguibile.
Collega tutti i file oggetto sostituendo i riferimenti a simboli indefiniti con gli indirizzi corretti. Ognuno di questi simboli può essere definito in altri file oggetto o in librerie. Se sono definiti in librerie diverse dalla libreria standard, è necessario informarne il linker.
In questa fase gli errori più comuni sono definizioni mancanti o definizioni duplicate. Il primo significa che le definizioni non esistono (cioè non sono scritte) o che i file oggetto o le librerie in cui risiedono non sono stati dati al linker. Quest'ultimo è ovvio: lo stesso simbolo è stato definito in due file o librerie di oggetti diversi.