Ogni file C che hai compilato iniziava con una riga come #include <stdio.h>, e quella riga non è C. È un'istruzione per un programma separato, il preprocessore, che viene eseguito per primo, riscrive il testo del tuo sorgente e passa il risultato al compilatore.
Capire questa divisione in due fasi spiega molto del comportamento del C: perché gli header vengono incollati invece che importati, perché una macro sbagliata produce un errore su una riga che sembra perfettamente a posto e perché lo stesso file sorgente può diventare programmi diversi su sistemi diversi.
Cosa succede davvero prima della compilazione
Compilare un file .c non è un unico passaggio. A grandi linee sono quattro:
- Preprocessing: esegue ogni riga che inizia con
#, producendo un unico grande testo sorgente espanso (una unità di traduzione). - Compilazione: trasforma quel testo in assembly e poi in un file oggetto.
- Assemblaggio: produce il codice macchina.
- Linking: unisce i file oggetto con le librerie in un eseguibile.
Il preprocessore lavora solo sul testo. Non sa cos'è una variabile, cos'è un tipo o se le tue parentesi graffe sono bilanciate. Vede caratteri e token, ne sostituisce alcuni con altri e va avanti. Tutto ciò che c'è di strano nelle macro deriva da questo unico fatto.
Quando il compilatore legge questo programma, GREETING non c'è più da nessuna parte. Il preprocessore l'ha già sostituito con il letterale "Ciao da una macro", esattamente come se l'avessi scritto tu.
La famiglia delle direttive
Ogni direttiva del preprocessore inizia con # come primo carattere non di spaziatura della riga. Non c'è punto e virgola e la direttiva finisce a fine riga, a meno che tu non la continui con una barra rovesciata.
| Direttiva | Cosa fa |
|---|---|
#include | Incolla il contenuto di un altro file |
#define | Definisce una macro (una sostituzione di testo) |
#undef | Rimuove la definizione di una macro |
#ifdef, #ifndef | Mantiene il codice seguente solo se una macro è (o non è) definita |
#if, #elif, #else, #endif | Mantiene codice in base a un'espressione costante |
#error | Interrompe la compilazione con un messaggio |
#pragma | Istruzione specifica del compilatore, come #pragma once |
#line | Cambia il numero di riga riportato (raro) |
Tre di queste hanno una pagina tutta loro: le macro approfondiscono #define, la compilazione condizionale tratta la famiglia #if e i file header spiegano come si usa #include per strutturare un programma su più file.
#include: parentesi angolari o virgolette
#include fa esattamente una cosa: sostituisce la propria riga con l'intero contenuto del file indicato. Anche il file incluso viene preprocessato, quindi si espandono pure le sue righe #include.
#include <stdio.h> /* cerca nelle directory di inclusione di sistema */
#include "config.h" /* cerca prima nella directory di questo file */
La differenza è l'ordine di ricerca:
<parentesi angolari>cercano nelle directory di inclusione standard del compilatore:/usr/include, le cartelle della toolchain e tutto ciò che aggiungi con-I. Servono per gli header delle librerie."virgolette"cercano prima nella directory del file che fa l'inclusione e poi ripiegano sullo stesso elenco delle parentesi angolari. Servono per gli header scritti da te.
Con la maggior parte dei compilatori entrambe le forme funzionano per qualsiasi header, ma la convenzione ha un significato: le parentesi angolari dicono "questo header è di qualcun altro", le virgolette dicono "questo è mio". Confonderle è il modo in cui un progetto finisce per includere un vecchio header di sistema invece del proprio.
Dato che l'inclusione è un incollaggio di testo, includere due volte lo stesso header lo incolla due volte, ed è per questo che gli header hanno bisogno delle include guard. È la prima cosa che sistema la pagina sui file header.
#define: sostituzione di testo e niente di più
#define NOME sostituzione dice al preprocessore: da qui in avanti, ovunque compaia il token NOME, metti sostituzione al suo posto.
Nota cosa non sta succedendo qui. MAX_USERS non ha un tipo. Non è memorizzato da nessuna parte. Non si può ispezionare in un debugger. È una regola di trova e sostituisci, e dopo il preprocessing il programma contiene letteralmente printf("%s ammette %d utenti\n", "Coddy", 100);.
Questo significa anche che la sostituzione è cieca. Questo codice compila e fa qualcosa di sorprendente:
#define SIZE 5 + 1
int arr[SIZE]; /* ok: int arr[5 + 1]; */
int total = SIZE * 2; /* 5 + 1 * 2 == 7, non 12 */
La soluzione, cioè le parentesi attorno a tutto, è la regola centrale della pagina sulle macro, insieme alle macro che accettano argomenti.
Un #define senza testo di sostituzione definisce il nome come "presente ma vuoto". È inutile come sostituzione e indispensabile come flag:
#define DEBUG /* definita, si espande nel nulla */
Il codice può poi chiedere se DEBUG esiste con #ifdef.
Vedere l'output del preprocessore
Il modo migliore per farsi un'intuizione è guardare cosa ha prodotto davvero il preprocessore. gcc -E si ferma dopo il preprocessing e stampa il risultato:
gcc -E hello.c
Per un file che include <stdio.h> sono da 700 a 30.000 righe a seconda del sistema, quasi tutte contenuto dell'header stesso. Per vedere solo la tua parte, prendi la coda:
gcc -E hello.c | tail -20
Provalo su un file come questo:
#define SQUARE(x) ((x) * (x))
#define LIMIT 10
int main(void) {
int n = SQUARE(LIMIT);
return n;
}
La coda dell'output mostra:
int main(void) {
int n = ((10) * (10));
return n;
}
Ogni macro è sparita: resta solo il testo sostituito. Quando una macro si comporta male, questo comando ti dice il perché in pochi secondi, ed è meglio che tirare a indovinare. Due compagni utili: gcc -dM -E - < /dev/null elenca tutte le macro che il tuo compilatore predefinisce e gcc -E -P file.c omette il rumore dei marcatori di riga.
Perché gli errori indicano la riga sbagliata
Dato che il compilatore vede il testo espanso, un errore dentro una macro viene segnalato dove la macro è stata usata, non dove è stata scritta:
#define HALF(x) (x / 2
int main(void) {
int y = HALF(8); /* errore segnalato qui */
return 0;
}
La parentesi mancante sta nella riga del #define, ma il compilatore si lamenta della riga che contiene HALF(8), spesso con un messaggio su un token inatteso che nel contesto non ha alcun senso. Quando un errore sembra impossibile, espandi il file con gcc -E e leggi la riga vera.
I compilatori moderni aiutano: GCC e clang stampano una nota "in expansion of macro" che rimanda alla definizione. Compila con -Wall -Wextra per vedere davvero quelle note.
Macro predefinite
Il preprocessore fornisce da sé alcune macro. Sono davvero utili per la diagnostica:
__FILE__ e __LINE__ si espandono nel nome del file corrente e nel numero di riga, ed è così che le macro di asserzione e di logging riportano dove qualcosa è andato storto. (__func__ è leggermente diverso: è un vero identificatore fornito dal compilatore, non una macro del preprocessore, ma si usa allo stesso modo.)
I compilatori predefiniscono anche macro di piattaforma come __linux__, _WIN32 e __APPLE__. Il codice che deve cambiare da un sistema all'altro le controlla con #ifdef, che è l'argomento della compilazione condizionale.
Cosa portarsi a casa
Il preprocessore è piccolo ed è stupido, ed entrambe le cose sono volute. Ti dà tre poteri, cioè includere un file, sostituire testo e attivare o disattivare codice, e nessun controllo dei tipi che li accompagni.
Questo compromesso è il motivo per cui il consiglio classico è preferire prima le funzionalità del linguaggio: usa const int o un enum invece di #define per le costanti quando puoi, e una vera funzione invece di una macro con argomenti. Dove il preprocessore è davvero lo strumento giusto, cioè header, switch di portabilità e configurazione a tempo di compilazione, è insostituibile.
Nella prossima pagina, le macro prendono sul serio #define: argomenti, le regole sulle parentesi e le trappole che arrivano quando sostituisci testo dentro codice che non hai scritto tu.
Domande frequenti
Cos'è il preprocessore in C?
Una fase di elaborazione del testo che viene eseguita prima della compilazione. Esegue le righe che iniziano con #: incolla il contenuto dei file header al posto di #include, sostituisce le macro definite con #define e cancella o mantiene codice in base a #if/#ifdef. Il compilatore vede solo il risultato, mai il tuo file originale.
Qual è la differenza tra #include <stdio.h> e #include "miofile.h"?
Le parentesi angolari cercano nelle directory di inclusione di sistema del compilatore, dove si trovano gli header della libreria standard. Le virgolette cercano prima nella directory del file corrente e poi ripiegano sui percorsi di sistema. Usa le parentesi angolari per gli header delle librerie e le virgolette per gli header scritti da te.
Come posso vedere cosa ha prodotto il preprocessore?
Esegui gcc -E file.c per fermarti dopo il preprocessing e stampare il sorgente espanso. Su un file che include <stdio.h> l'output è di migliaia di righe, quindi mandalo in una pipe: gcc -E file.c | tail -30 mostra solo il tuo codice con ogni macro già sostituita.
#include è un'istruzione C?
No. Le direttive non fanno parte del linguaggio C vero e proprio: hanno una propria sintassi basata sulle righe, non vogliono il punto e virgola e sono già sparite quando il compilatore analizza il tuo programma. Per questo un errore in una macro si presenta come un errore confuso su una riga che sembra corretta.