Każdy plik C, jaki kiedykolwiek kompilujesz, zaczyna się od linii w rodzaju #include <stdio.h>, a ta linia nie jest kodem C. To polecenie dla osobnego programu, czyli preprocesora, który uruchamia się jako pierwszy, przepisuje tekst źródłowy i przekazuje wynik kompilatorowi.
Zrozumienie tego podziału na dwa etapy wyjaśnia sporo zachowań C: dlaczego nagłówki są wklejane, a nie importowane, dlaczego błędne makro daje błąd w linii, która wygląda zupełnie dobrze, i dlaczego ten sam plik źródłowy może się skompilować do różnych programów na różnych systemach.
Co naprawdę dzieje się przed kompilacją
Kompilacja pliku .c to nie jeden krok. W uproszczeniu są cztery:
- Preprocesowanie: wykonanie każdej linii zaczynającej się od
#, co daje jeden duży, rozwinięty tekst źródłowy (jednostkę translacji). - Kompilacja: zamiana tego tekstu na asembler, a potem na plik obiektowy.
- Asemblacja: wytworzenie kodu maszynowego.
- Linkowanie: połączenie plików obiektowych z bibliotekami w plik wykonywalny.
Preprocesor pracuje wyłącznie na tekście. Nie wie, czym jest zmienna ani typ, ani czy nawiasy klamrowe się bilansują. Widzi znaki i tokeny, zamienia jedne na drugie i idzie dalej. Wszystkie dziwactwa makr wynikają z tego jednego faktu.
Gdy kompilator czyta ten program, nigdzie nie ma już GREETING. Preprocesor zastąpił je literałem "Witaj z makra", dokładnie tak, jakby był wpisany ręcznie.
Rodzina dyrektyw
Każda dyrektywa preprocesora zaczyna się od # jako pierwszego niebiałego znaku w linii. Nie ma średnika, a dyrektywa kończy się na końcu linii, chyba że przedłużysz ją ukośnikiem wstecznym.
| Dyrektywa | Co robi |
|---|---|
#include | Wkleja zawartość innego pliku |
#define | Definiuje makro (podstawienie tekstu) |
#undef | Usuwa definicję makra |
#ifdef, #ifndef | Zostawia następny kod tylko wtedy, gdy makro jest (lub nie jest) zdefiniowane |
#if, #elif, #else, #endif | Zostawia kod na podstawie wyrażenia stałego |
#error | Przerywa kompilację z komunikatem |
#pragma | Instrukcja specyficzna dla kompilatora, np. #pragma once |
#line | Zmienia zgłaszany numer linii (rzadko używana) |
Trzy z nich mają własne strony: makra omawiają #define dogłębnie, kompilacja warunkowa opisuje rodzinę #if, a pliki nagłówkowe pokazują, jak #include służy do budowy programu z wielu plików.
#include: nawiasy ostre czy cudzysłowy
#include robi dokładnie jedną rzecz: zastępuje własną linię całą zawartością wskazanego pliku. Dołączony plik sam również przechodzi preprocesowanie, więc jego linie #include też się rozwijają.
#include <stdio.h> /* szukaj w systemowych katalogach naglowkow */
#include "config.h" /* szukaj najpierw w katalogu tego pliku */
Różnica dotyczy kolejności wyszukiwania:
<angle brackets>szukają w standardowych katalogach nagłówków kompilatora:/usr/include, własnych folderach toolchaina oraz w tym, co dodasz przez-I. To wariant dla nagłówków bibliotek."quotes"szukają najpierw w katalogu pliku, który dołącza nagłówek, a potem w tych samych miejscach co nawiasy ostre. To wariant dla nagłówków napisanych przez ciebie.
W większości kompilatorów obie formy działają dla obu rodzajów nagłówków, ale konwencja ma znaczenie: nawiasy ostre mówią „to cudzy nagłówek”, cudzysłowy mówią „to mój”. Pomylenie ich to prosty sposób, by projekt dołączył przestarzały nagłówek systemowy zamiast własnego.
Ponieważ dołączanie to wklejanie tekstu, dwukrotne dołączenie tego samego nagłówka wkleja go dwa razy. Dlatego nagłówki potrzebują include guards. To pierwsza rzecz, którą naprawia strona o plikach nagłówkowych.
#define: podstawienie tekstu i nic więcej
#define NAME replacement mówi preprocesorowi: od tego miejsca, wszędzie gdzie pojawia się token NAME, wstaw replacement.
Zwróć uwagę, co się tutaj nie dzieje. MAX_USERS nie ma typu. Nie jest nigdzie przechowywane. Nie da się go podejrzeć w debuggerze. To reguła „znajdź i zamień”, a po preprocesowaniu program dosłownie zawiera printf("%s obsluguje %d uzytkownikow\n", "Coddy", 100);.
To oznacza też, że podstawienie jest ślepe. Poniższy kod się kompiluje i robi coś zaskakującego:
#define SIZE 5 + 1
int arr[SIZE]; /* dobrze: int arr[5 + 1]; */
int total = SIZE * 2; /* 5 + 1 * 2 == 7, a nie 12 */
Rozwiązanie, czyli nawiasy wokół wszystkiego, to główna zasada strony o makrach, obok makr przyjmujących argumenty.
#define bez tekstu zastępczego definiuje nazwę jako „obecną, ale pustą”. Jako podstawienie jest to bezużyteczne, ale jako flaga niezbędne:
#define DEBUG /* zdefiniowane, rozwija sie do niczego */
Kod może wtedy sprawdzić przez #ifdef, czy DEBUG istnieje.
Podgląd wyniku preprocesora
Najlepszy sposób na wyrobienie intuicji to zobaczyć, co preprocesor faktycznie wygenerował. gcc -E zatrzymuje się po preprocesowaniu i wypisuje wynik:
gcc -E hello.c
Dla pliku, który dołącza <stdio.h>, to od 700 do 30 000 linii zależnie od systemu, prawie w całości zawartość samego nagłówka. Aby zobaczyć tylko swoją część, weź koniec:
gcc -E hello.c | tail -20
Wypróbuj to na pliku takim jak ten:
#define SQUARE(x) ((x) * (x))
#define LIMIT 10
int main(void) {
int n = SQUARE(LIMIT);
return n;
}
Koniec wyniku pokazuje:
int main(void) {
int n = ((10) * (10));
return n;
}
Wszystkie makra zniknęły, został tylko podstawiony tekst. Gdy makro zachowuje się dziwnie, to polecenie w kilka sekund pokaże dlaczego, a to lepsze niż zgadywanie. Dwa przydatne dodatki: gcc -dM -E - < /dev/null wypisuje wszystkie makra predefiniowane przez kompilator, a gcc -E -P file.c pomija szum znaczników linii.
Dlaczego błędy wskazują złą linię
Kompilator widzi rozwinięty tekst, więc błąd wewnątrz makra jest zgłaszany tam, gdzie makra użyto, a nie tam, gdzie je napisano:
#define HALF(x) (x / 2
int main(void) {
int y = HALF(8); /* blad zglaszany tutaj */
return 0;
}
Brakujący nawias jest w linii z #define, ale kompilator narzeka na linię zawierającą HALF(8), często komunikatem o nieoczekiwanym tokenie, który w tym kontekście nie ma sensu. Gdy błąd wygląda na niemożliwy, rozwiń plik przez gcc -E i przeczytaj prawdziwą linię.
Nowoczesne kompilatory pomagają: GCC i clang wypisują notkę „in expansion of macro” wskazującą definicję. Kompiluj z -Wall -Wextra, żeby faktycznie widzieć te notki.
Makra predefiniowane
Część makr preprocesor dostarcza sam. Są naprawdę przydatne przy diagnostyce:
__FILE__ i __LINE__ rozwijają się do nazwy bieżącego pliku i numeru linii. W ten sposób makra asercji i logowania informują, gdzie coś poszło nie tak. (__func__ działa nieco inaczej: to prawdziwy identyfikator dostarczany przez kompilator, a nie makro preprocesora, ale używa się go tak samo.)
Kompilatory predefiniują też makra platform, takie jak __linux__, _WIN32 i __APPLE__. Kod, który musi się różnić między systemami, sprawdza je przez #ifdef, i tym zajmuje się kompilacja warunkowa.
Co warto zapamiętać
Preprocesor jest mały i głupi, i jedno, i drugie celowo. Daje trzy możliwości: wciągnąć plik, podstawić tekst oraz włączać i wyłączać kod, a do tego zero sprawdzania typów.
Właśnie dlatego standardowa rada brzmi: najpierw sięgaj po mechanizmy języka. Dla stałych używaj const int lub enum zamiast #define, gdy tylko się da, a zamiast makra funkcyjnego napisz prawdziwą funkcję. Tam, gdzie preprocesor naprawdę jest właściwym narzędziem, czyli przy nagłówkach, przełącznikach przenośności i konfiguracji w czasie kompilacji, nic go nie zastąpi.
Dalej strona o makrach traktuje #define poważnie: argumenty, zasady nawiasów i pułapki, które pojawiają się, gdy podstawiasz tekst w cudzym kodzie.
Najczęściej zadawane pytania
Czym jest preprocesor w C?
To etap przetwarzania tekstu, który działa przed kompilacją. Wykonuje linie zaczynające się od #: wkleja zawartość plików nagłówkowych w miejsce #include, podstawia makra zdefiniowane przez #define i usuwa lub zostawia kod zgodnie z #if/#ifdef. Kompilator widzi tylko wynik, nigdy twój oryginalny plik.
Czym różni się #include <stdio.h> od #include "myfile.h"?
Nawiasy ostre przeszukują systemowe katalogi nagłówków kompilatora, gdzie leżą nagłówki biblioteki standardowej. Cudzysłowy najpierw przeszukują katalog bieżącego pliku, a dopiero potem ścieżki systemowe. Nawiasów ostrych używaj dla nagłówków bibliotek, a cudzysłowów dla nagłówków napisanych przez siebie.
Jak zobaczyć, co wygenerował preprocesor?
Uruchom gcc -E file.c, aby zatrzymać się po preprocesowaniu i wypisać rozwinięty kod źródłowy. Dla pliku, który dołącza <stdio.h>, wynik ma tysiące linii, więc przekaż go dalej potokiem: gcc -E file.c | tail -30 pokazuje tylko twój kod z już podstawionymi makrami.
Czy #include to instrukcja języka C?
Nie. Dyrektywy nie należą do samego języka C: mają własną składnię opartą na liniach, nie kończą się średnikiem i znikają, zanim kompilator zacznie analizować program. Dlatego błąd w makrze objawia się mylącym komunikatem w linii, która wygląda poprawnie.