2.10 — Wprowadzenie do preprocesor

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_YEN

Makra 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?.

#if 0

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
#endif

Zakres #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.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
615 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze