Kompilacja programu C ++ obejmuje trzy kroki:
Przetwarzanie wstępne: preprocesor pobiera plik kodu źródłowego C ++ i zajmuje się #include
s, #define
si innymi dyrektywami preprocesora. Wyjściem tego kroku jest „czysty” plik C ++ bez dyrektyw przedprocesowych.
Kompilacja: kompilator pobiera dane wyjściowe procesora i tworzy z niego plik obiektowy.
Łączenie: konsolidator pobiera pliki obiektów wygenerowane przez kompilator i tworzy bibliotekę lub plik wykonywalny.
Przetwarzanie wstępne
Preprocesor obsługuje dyrektywy preprocesora , takie jak #include
i #define
. Jest niezależny od składni języka C ++, dlatego należy go używać ostrożnie.
To działa na jednym pliku źródłowym C ++ w czasie, poprzez zastąpienie #include
dyrektyw z treścią odpowiednich plików (zwykle jest to tylko deklaracje), robi wymianę makr ( #define
), a następnie wybierając różne fragmenty tekstu w zależności od #if
, #ifdef
i #ifndef
wskazówki.
Preprocesor działa na strumieniu tokenów przetwarzania wstępnego. Makropodstawienie definiuje się jako zastępowanie tokenów innymi tokenami (operator ##
umożliwia scalenie dwóch tokenów, gdy ma to sens).
Po tym wszystkim preprocesor wytwarza pojedyncze wyjście, które jest strumieniem tokenów wynikających z transformacji opisanych powyżej. Dodaje także specjalne znaczniki, które informują kompilator, skąd pochodzi każda linia, aby mógł używać tych znaków do generowania rozsądnych komunikatów o błędach.
Na tym etapie można popełnić pewne błędy dzięki sprytnemu zastosowaniu dyrektyw #if
i #error
.
Kompilacja
Etap kompilacji wykonywany jest na każdym wyjściu preprocesora. Kompilator analizuje czysty kod źródłowy C ++ (teraz bez dyrektyw preprocesora) i konwertuje go na kod asemblera. Następnie wywołuje bazowe zaplecze (asembler w toolchain), które składa ten kod w kod maszynowy, tworząc rzeczywisty plik binarny w jakimś formacie (ELF, COFF, a.out, ...). Ten plik obiektowy zawiera skompilowany kod (w formie binarnej) symboli zdefiniowanych na wejściu. Symbole w plikach obiektowych są określane według nazwy.
Pliki obiektowe mogą odnosić się do symboli, które nie są zdefiniowane. Dzieje się tak, gdy używasz deklaracji i nie podajesz jej definicji. Kompilatorowi to nie przeszkadza i chętnie utworzy plik obiektowy, o ile kod źródłowy jest poprawnie sformułowany.
Kompilatory zwykle pozwalają zatrzymać kompilację w tym momencie. Jest to bardzo przydatne, ponieważ dzięki niemu możesz skompilować każdy plik kodu źródłowego osobno. Zaletą tego jest to, że nie trzeba ponownie kompilować wszystkiego, jeśli zmienisz tylko jeden plik.
Wytworzone pliki obiektowe można umieścić w specjalnych archiwach zwanych bibliotekami statycznymi, aby ułatwić ich późniejsze użycie.
Na tym etapie zgłaszane są „zwykłe” błędy kompilatora, takie jak błędy składniowe lub błędy rozwiązywania przeciążenia.
Łączenie
Linker jest tym, co wytwarza ostateczne dane wyjściowe kompilacji z plików obiektowych utworzonych przez kompilator. To wyjście może być biblioteką współdzieloną (lub dynamiczną) (i chociaż nazwa jest podobna, nie mają wiele wspólnego z bibliotekami statycznymi wspomnianymi wcześniej) ani plikiem wykonywalnym.
Łączy wszystkie pliki obiektowe, zastępując odwołania do niezdefiniowanych symboli poprawnymi adresami. Każdy z tych symboli można zdefiniować w innych plikach obiektowych lub w bibliotekach. Jeśli są zdefiniowane w bibliotekach innych niż biblioteka standardowa, musisz o nich powiedzieć linkerowi.
Na tym etapie najczęstszymi błędami są brakujące definicje lub duplikaty definicji. To pierwsze oznacza, że albo definicje nie istnieją (tzn. Nie są zapisane), albo że pliki obiektów lub biblioteki, w których się znajdują, nie zostały przekazane linkerowi. To ostatnie jest oczywiste: ten sam symbol został zdefiniowany w dwóch różnych plikach obiektowych lub bibliotekach.