Podczas kompilowania projektu możesz spodziewaj się, że kompilator kompiluje każdy plik kodu dokładnie tak, jak go napisałeś. W rzeczywistości tak nie jest.
Zamiast tego przed kompilacją każdy plik kodu (.cpp) przechodzi przez fazę wstępnego przetwarzania . W tej fazie program o nazwie preprocesor wprowadza różne zmiany w tekście pliku z kodem. Preprocesor tak naprawdę nie modyfikuje w żaden sposób oryginalnych plików kodu — raczej wszystkie zmiany dokonane przez preprocesor zachodzą albo tymczasowo w pamięci, albo przy użyciu plików tymczasowych.
Na marginesie…
Historycznie rzecz biorąc, preprocesor był programem odrębnym od kompilatora, ale we współczesnych kompilatorach preprocesor mógł być wbudowany bezpośrednio w sam kompilator.
Większość tego, co robi preprocesor, jest dość nieciekawa. Na przykład usuwa komentarze i zapewnia, że każdy plik kodu kończy się znakiem nowej linii. Jednakże preprocesor pełni jedną bardzo ważną rolę: przetwarza #include dyrektywy (o czym szerzej za chwilę).
Gdy preprocesor zakończy przetwarzanie pliku z kodem, wynik nazywany jest jednostką tłumaczeniową. Ta jednostka tłumaczeniowa jest następnie kompilowana przez kompilator.
Powiązana treść
Cały proces wstępnego przetwarzania, kompilacji i łączenia nazywa się translacją.
Jeśli jesteś ciekawy, oto lista faz tłumaczenia. W chwili pisania tego tekstu przetwarzanie wstępne obejmuje fazy od 1 do 4, a kompilacja fazy od 5 do 7.
Dyrektywy preprocesora
Gdy preprocesor działa, skanuje plik kodu (od góry do dołu), szukając dyrektyw preprocesora. Dyrektywy preprocesora (często nazywane po prostu dyrektywami) to instrukcje rozpoczynające się od a # i zakończ znakiem nowej linii (NIE średnikiem). Dyrektywy te nakazują preprocesorowi wykonanie określonych zadań manipulacji tekstem. Należy zauważyć, że preprocesor nie rozumie składni C++ - zamiast tego dyrektywy mają własną składnię (która w niektórych przypadkach przypomina składnię C++, a w innych nie za bardzo).
Kluczowa informacja
Ostateczne wyjście preprocesora nie zawiera żadnych dyrektyw - jedynie wyjście przetworzonej dyrektywy jest przekazywane do kompilatora.
Na marginesie…
Using directives (przedstawione w lekcji 2.9 -- Kolizje nazewnictwa i wprowadzenie do przestrzeni nazw) nie są dyrektywami preprocesora (i dlatego nie są przez niego przetwarzane). Zatem chociaż termin directive Zazwyczaj oznacza preprocessor directive, nie zawsze tak jest.
#Include
Widzieliście już dyrektywę #include w działaniu (zazwyczaj #include <iostream>). Kiedy #include plikujesz, preprocesor zastępuje dyrektywę #include zawartością dołączonego pliku. Załączona zawartość jest następnie wstępnie przetwarzana (co może skutkować rekursywnym przetwarzaniem dodatkowych #includes), a następnie przetwarzana jest reszta pliku.
Rozważ następujący program:
#include <iostream>
int main()
{
std::cout << "Hello, world!\n";
return 0;
}Gdy preprocesor działa w tym programie, preprocesor zastąpi #include <iostream> zawartością pliku o nazwie „iostream”, a następnie wstępnie przetworzy dołączoną zawartość i resztę pliku.
Ponieważ #include jest używany prawie wyłącznie do Dołącz pliki nagłówkowe, omówimy #include bardziej szczegółowo w następnej lekcji (kiedy będziemy omawiać pliki nagłówkowe).
Kluczowa informacja
Każda jednostka tłumaczeniowa zazwyczaj składa się z pojedynczego pliku kodu (.cpp) i wszystkich plików nagłówkowych, które #zawiera (stosowanych rekurencyjnie, ponieważ pliki nagłówkowe mogą #zawierać inne pliki nagłówkowe).
Makro definiuje
Klasa #define dyrektywę można wykorzystać do utworzenia makra. W języku C++ a Makro jest regułą definiującą sposób konwertowania tekstu wejściowego na zastępczy tekst wyjściowy.
Istnieją dwa podstawowe typy makr: makra obiektowe, I makra funkcyjne.
Makra funkcyjne działają jak funkcje i służą podobnemu celowi. Ich użycie jest ogólnie uważane za niebezpieczne i prawie wszystko, co mogą zrobić, można wykonać za pomocą normalnej funkcji.
Makra obiektowe można zdefiniować na jeden z dwóch sposobów:
#define IDENTIFIER #define IDENTIFIER substitution_text
Górna definicja nie zawiera tekstu zastępczego, podczas gdy dolna tak. Ponieważ są to dyrektywy preprocesora (nie instrukcje), należy pamiętać, że żaden z formularzy nie kończy się średnikiem.
Identyfikator makra wykorzystuje te same zasady nazewnictwa, co zwykłe identyfikatory: mogą używać liter, cyfr i podkreśleń, nie mogą zaczynać się od cyfry i nie powinny zaczynać się od podkreślenia. Zgodnie z konwencją wszystkie nazwy makr są zazwyczaj pisane wielkimi literami, oddzielone podkreśleniami.
Najlepsza praktyka
Nazwy makr powinny być pisane wielkimi literami, a słowa oddzielone podkreśleniami.
Makra obiektowe z tekstem zastępczym
Gdy preprocesor napotka tę dyrektywę, tworzone jest powiązanie pomiędzy identyfikatorem makra a tekst_podstawienia. Wszystkie dalsze wystąpienia identyfikatora makra (poza użyciem w innych poleceniach preprocesora) są zastępowane przez tekst_podstawienia.
Rozważ następujący program:
#include <iostream>
#define MY_NAME "Alex"
int main()
{
std::cout << "My name is: " << MY_NAME << '\n';
return 0;
}Preprocesor konwertuje powyższe na:
// The contents of iostream are inserted here
int main()
{
std::cout << "My name is: " << "Alex" << '\n';
return 0;
}Który po uruchomieniu wypisuje wynik My name is: Alex.
Makra obiektowe z tekstem podstawienia zostały użyte (w C) jako sposób na przypisanie nazw do literałów. Nie jest to już konieczne, ponieważ w C++ dostępne są lepsze metody (patrz 7.10 — Współdzielenie stałych globalnych w wielu plikach (przy użyciu zmiennych wbudowanych)). Makra obiektowe z tekstem podstawieniowym są obecnie najczęściej spotykane w starszym kodzie i zalecamy ich unikać, jeśli to możliwe.
Najlepsza praktyka
Unikaj makr z tekstem podstawieniowym, chyba że nie istnieją realne alternatywy.
Makra obiektowe bez tekstu podstawieniowego
Makra obiektowe można również definiować bez tekstu podstawieniowego.
Na przykład:
#define USE_YENMakra obiektów ten formularz działa tak, jak można się spodziewać: większość dalszych wystąpień identyfikatora jest usuwana i zastępowana niczym!
Może się to wydawać całkiem bezużyteczne i jest bezużyteczne do zastępowania tekstu. Jednak nie do tego powszechnie stosuje się tę formę dyrektywy. Za chwilę omówimy zastosowania tej formy.
W przeciwieństwie do makr obiektowych z tekstem podstawieniowym, makra w tej formie są ogólnie uważane za akceptowalne w użyciu.
Kompilacja warunkowa
Klasa kompilacja warunkowa dyrektywy preprocesora pozwalają określić, w jakich warunkach coś się skompiluje, a czego nie. Istnieje sporo różnych dyrektyw kompilacji warunkowej, ale omówimy tylko kilka najczęściej używanych: #ifdef, #ifndef, I #endif.
Klasa #ifdef dyrektywa preprocesora pozwala preprocesorowi sprawdzić, czy identyfikator został wcześniej zdefiniowany za pomocą #define. Jeśli tak, kompilowany jest kod pomiędzy #ifdef i pasującym #endif . Jeżeli nie, kod jest ignorowany.
Rozważ następujący program:
#include <iostream>
#define PRINT_JOE
int main()
{
#ifdef PRINT_JOE
std::cout << "Joe\n"; // will be compiled since PRINT_JOE is defined
#endif
#ifdef PRINT_BOB
std::cout << "Bob\n"; // will be excluded since PRINT_BOB is not defined
#endif
return 0;
}Ponieważ PRINT_JOE został #zdefiniowany, linia std::cout << "Joe\n" zostanie skompilowana. Ponieważ PRINT_BOB nie został #zdefiniowany, linia std::cout << "Bob\n" zostanie zignorowana.
#ifndef jest przeciwieństwem #ifdef, ponieważ pozwala sprawdzić, czy identyfikator nie NIE nie został #defined jeszcze.
#include <iostream>
int main()
{
#ifndef PRINT_BOB
std::cout << "Bob\n";
#endif
return 0;
}Ten program wypisuje „Bob”, ponieważ PRINT_BOB nigdy nie był #defined.
Na miejscu z #ifdef PRINT_BOB i #ifndef PRINT_BOB, zobaczysz także #if defined(PRINT_BOB) i #if !defined(PRINT_BOB). Robią to samo, ale używają składni nieco bardziej w stylu C++.
Praktyczne zastosowanie tej funkcji możesz zobaczyć na lekcji 0.13 — Jakiego standardu językowego używa mój kompilator?.
Jeszcze jedno typowe zastosowanie kompilacji warunkowej polega na użyciu #if 0 w celu wykluczenia bloku kodu z kompilacji (tak jakby znajdował się w komentarzu) block):
#include <iostream>
int main()
{
std::cout << "Joe\n";
#if 0 // Don't compile anything starting here
std::cout << "Bob\n";
std::cout << "Steve\n";
#endif // until this point
return 0;
}Powyższy kod wypisuje tylko „Joe”, ponieważ „Bob” i „Steve” są wykluczeni z kompilacji przez #if 0 dyrektywa preprocesora.
Zapewnia wygodny sposób „komentowania” kodu zawierającego komentarze wielowierszowe (których nie można skomentować przy użyciu innego komentarza wielowierszowego, ponieważ komentarze wielowierszowe nie są zagnieżdżane):
#include <iostream>
int main()
{
std::cout << "Joe\n";
#if 0 // Don't compile anything starting here
std::cout << "Bob\n";
/* Some
* multi-line
* comment here
*/
std::cout << "Steve\n";
#endif // until this point
return 0;
}Aby tymczasowo ponownie włączyć kod zawinięty w #if 0, możesz zmienić #if 0 Do #if 1:
#include <iostream>
int main()
{
std::cout << "Joe\n";
#if 1 // always true, so the following code will be compiled
std::cout << "Bob\n";
/* Some
* multi-line
* comment here
*/
std::cout << "Steve\n";
#endif
return 0;
}podstawienie makra w innym polecenia preprocesora
Teraz możesz się zastanawiać, biorąc pod uwagę następujący kod:
#define PRINT_JOE
int main()
{
#ifdef PRINT_JOE
std::cout << "Joe\n"; // will be compiled since PRINT_JOE is defined
#endif
return 0;
}Skoro zdefiniowaliśmy PRINT_JOE jako nic, dlaczego preprocesor nie zastąpił PRINT_JOE w #ifdef PRINT_JOE bez niczego i wykluczyć instrukcję wyjściową z kompilacji?
W większości przypadków podstawienie makra nie następuje, gdy identyfikator makra jest używany w innym poleceniu preprocesora.
Na marginesie…
Istnieje co najmniej jeden wyjątek od tej reguły: większość form #if i #elif wykonuje podstawienie makra w poleceniu preprocesora.
Jako inny przykład:
#define FOO 9 // Here's a macro substitution
#ifdef FOO // This FOO does not get replaced with 9 because it’s part of another preprocessor directive
std::cout << FOO << '\n'; // This FOO gets replaced with 9 because it's part of the normal code
#endifZakres #defines
Dyrektywy są ustalane przed kompilacją, od góry do dołu, plik po pliku.
Rozważ następujący program:
#include <iostream>
void foo()
{
#define MY_NAME "Alex"
}
int main()
{
std::cout << "My name is: " << MY_NAME << '\n';
return 0;
}Mimo że wygląda na to, że #define MOJA_NAZWA „Alex” jest zdefiniowane wewnątrz funkcja foo, preprocesor nie rozumie pojęć C++, takich jak funkcje. Dlatego ten program zachowuje się identycznie jak ten, w którym #define MOJA_NAZWA „Alex” została zdefiniowana przed lub bezpośrednio po funkcji foo. Aby uniknąć nieporozumień, zazwyczaj będziesz chciał #definiować identyfikatory poza funkcjami.
Ponieważ dyrektywa #include zastępuje dyrektywę #include zawartością dołączonego pliku, #include może skopiować dyrektywy z dołączonego pliku do bieżącego pliku. Dyrektywy te będą następnie przetwarzane w określonej kolejności.
Na przykład poniższe zachowują się identycznie jak w poprzednich przykładach:
Alex.h:
#define MY_NAME "Alex"main.cpp:
#include "Alex.h" // copies #define MY_NAME from Alex.h here
#include <iostream>
int main()
{
std::cout << "My name is: " << MY_NAME << '\n'; // preprocessor replaces MY_NAME with "Alex"
return 0;
}Po zakończeniu działania preprocesora wszystkie zdefiniowane identyfikatory z tego pliku są odrzucane. Oznacza to, że dyrektywy obowiązują tylko od punktu definicji do końca pliku, w którym są zdefiniowane. Dyrektywy zdefiniowane w jednym pliku nie mają żadnego wpływu na inne pliki (chyba, że zostaną #uwzględnione w innym pliku). Na przykład:
function.cpp:
#include <iostream>
void doSomething()
{
#ifdef PRINT
std::cout << "Printing!\n";
#endif
#ifndef PRINT
std::cout << "Not printing!\n";
#endif
}main.cpp:
void doSomething(); // forward declaration for function doSomething()
#define PRINT
int main()
{
doSomething();
return 0;
}Powyższy program wyświetli:
Not printing!
Mimo że PRINT został zdefiniowany w main.cpp, nie ma to żadnego wpływu na żaden kod w function.cpp (PRINT jest #definiowany tylko od punktu definicji do końca main.cpp). Będzie to miało znaczenie, gdy będziemy omawiać zabezpieczenia nagłówków w przyszłej lekcji.

