C++
Preprocesor
Szukaj…
Wprowadzenie
Preprocesor C to prosty parser / zamiennik tekstu, który jest uruchamiany przed faktyczną kompilacją kodu. Służy do rozszerzenia i ułatwienia korzystania z języka C (a później C ++), można go używać do:
za. Dołączanie innych plików przy użyciu #include
b. Zdefiniuj makro zastępujące tekst za pomocą #define
do. Kompilacja warunkowa przy użyciu #if
#ifdef
re. Logika specyficzna dla platformy / kompilatora (jako rozszerzenie kompilacji warunkowej)
Uwagi
Instrukcje preprocesora są wykonywane przed przekazaniem plików źródłowych do kompilatora. Są zdolne do logiki warunkowej na bardzo niskim poziomie. Ponieważ konstrukcje preprocesora (np. Makra obiektowe) nie są typowane jak normalne funkcje (etap wstępnego przetwarzania odbywa się przed kompilacją), kompilator nie może wymusić kontroli typu, dlatego należy ich ostrożnie używać.
Uwzględnij strażników
Plik nagłówka może zawierać inne pliki nagłówka. Plik źródłowy (jednostka kompilacji), który zawiera wiele nagłówków, może zatem pośrednio zawierać niektóre nagłówki więcej niż jeden raz. Jeśli taki plik nagłówkowy, który jest zawarty więcej niż jeden raz, zawiera definicje, kompilator (po wstępnym przetwarzaniu) wykrywa naruszenie reguły One Definition (np. §3.2 standardu C ++ 2003) i dlatego wydaje komunikat diagnostyczny, a kompilacja kończy się niepowodzeniem.
Wielokrotnemu włączeniu zapobiega się za pomocą „włączników”, które czasami są również znane jako osłony nagłówków lub osłony makro. Są one realizowane za pomocą dyrektyw preprocesora #define
, #ifndef
, #endif
.
na przykład
// Foo.h
#ifndef FOO_H_INCLUDED
#define FOO_H_INCLUDED
class Foo // a class definition
{
};
#endif
Kluczową zaletą korzystania z osłon obejmuje to, że będą one działać ze wszystkimi zgodnymi ze standardami kompilatorami i procesorami wstępnymi.
Jednak dołączanie zabezpieczeń powoduje również pewne problemy dla programistów, ponieważ należy upewnić się, że makra są unikalne we wszystkich nagłówkach używanych w projekcie. W szczególności, jeśli dwa (lub więcej) nagłówki używają FOO_H_INCLUDED
jako swojej osłony włączającej, pierwszy z tych nagłówków zawartych w jednostce kompilacyjnej skutecznie uniemożliwi włączenie innych. Szczególne wyzwania pojawiają się, jeśli projekt korzysta z wielu bibliotek stron trzecich z plikami nagłówkowymi, które zdarzają się używać, zawierają zabezpieczenia wspólne.
Konieczne jest również upewnienie się, że makra użyte w osłonach dołączających nie powodują konfliktu z innymi makrami zdefiniowanymi w plikach nagłówkowych.
Większość implementacji C ++ obsługuje również dyrektywę #pragma once
która gwarantuje, że plik zostanie dołączony tylko raz w ramach jednej kompilacji. Jest to de facto standardowa dyrektywa, ale nie jest częścią żadnej normy ISO C ++. Na przykład:
// Foo.h
#pragma once
class Foo
{
};
Podczas gdy #pragma once
unika pewnych problemów związanych z #pragma
strażników, #pragma
- z definicji w standardach - jest z natury zaczepem specyficznym dla kompilatora i zostanie po cichu zignorowany przez kompilatory, które go nie obsługują. Projekty, które używają #pragma once
trudniej jest przenieść do kompilatorów, które go nie obsługują.
Szereg wytycznych dotyczących kodowania i standardów bezpieczeństwa dla C ++ szczególnie zniechęca do jakiegokolwiek użycia preprocesora innego niż #include
plików nagłówkowych lub do celów umieszczania osłon w nagłówkach.
Logika warunkowa i obsługa wielu platform
W skrócie, warunkowa logika przetwarzania wstępnego polega na udostępnianiu lub niedostępności logiki kodu do kompilacji przy użyciu definicji makr.
Trzy znaczące przypadki użycia to:
- różne profile aplikacji (np. debugowanie, wydanie, testowanie, zoptymalizowane), które mogą być kandydatami na tę samą aplikację (np. z dodatkowym logowaniem).
- kompilacje między platformami - pojedyncza baza kodu, wiele platform kompilacji.
- wykorzystując wspólną bazę kodu dla wielu wersji aplikacji (np. wersje Basic, Premium i Pro oprogramowania) - z nieco innymi funkcjami.
Przykład a: Wieloplatformowe podejście do usuwania plików (ilustracyjne):
#ifdef _WIN32
#include <windows.h> // and other windows system files
#endif
#include <cstdio>
bool remove_file(const std::string &path)
{
#ifdef _WIN32
return DeleteFile(path.c_str());
#elif defined(_POSIX_VERSION) || defined(__unix__)
return (0 == remove(path.c_str()));
#elif defined(__APPLE__)
//TODO: check if NSAPI has a more specific function with permission dialog
return (0 == remove(path.c_str()));
#else
#error "This platform is not supported"
#endif
}
Makra takie jak _WIN32
, __APPLE__
lub __unix__
są zwykle predefiniowane przez odpowiednie implementacje.
Przykład b: Włączanie dodatkowego rejestrowania dla kompilacji debugowania:
void s_PrintAppStateOnUserPrompt()
{
std::cout << "--------BEGIN-DUMP---------------\n"
<< AppState::Instance()->Settings().ToString() << "\n"
#if ( 1 == TESTING_MODE ) //privacy: we want user details only when testing
<< ListToString(AppState::UndoStack()->GetActionNames())
<< AppState::Instance()->CrntDocument().Name()
<< AppState::Instance()->CrntDocument().SignatureSHA() << "\n"
#endif
<< "--------END-DUMP---------------\n"
}
Przykład c: Włącz funkcję premium w osobnej kompilacji produktu (uwaga: jest to przykładowe. Często lepszym pomysłem jest zezwolenie na odblokowanie funkcji bez konieczności ponownej instalacji aplikacji)
void MainWindow::OnProcessButtonClick()
{
#ifndef _PREMIUM
CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website");
return;
#endif
//...actual feature logic here
}
Niektóre popularne sztuczki:
Definiowanie symboli w czasie wywołania:
Preprocesor można wywołać za pomocą wstępnie zdefiniowanych symboli (z opcjonalną inicjalizacją). Na przykład to polecenie ( gcc -E
uruchamia tylko preprocesor)
gcc -E -DOPTIMISE_FOR_OS_X -DTESTING_MODE=1 Sample.cpp
przetwarza Sample.cpp w taki sam sposób, jak gdyby #define OPTIMISE_FOR_OS_X
i #define TESTING_MODE 1
zostały dodane na górze Sample.cpp.
Zapewnienie zdefiniowania makra:
Jeśli makro nie jest zdefiniowane, a jego wartość jest porównywana lub sprawdzana, preprocesor prawie zawsze po cichu przyjmuje wartość 0
. Istnieje kilka sposobów pracy z tym. Jednym z podejść jest założenie, że ustawienia domyślne są reprezentowane jako 0, a wszelkie zmiany (np. W profilu kompilacji aplikacji) muszą być wyraźnie wykonane (np. ENABLE_EXTRA_DEBUGGING = 0 domyślnie, ustaw -DENABLE_EXTRA_DEBUGGING = 1, aby zastąpić). Innym podejściem jest jawne zdefiniowanie wszystkich definicji i wartości domyślnych. Można to osiągnąć za pomocą kombinacji dyrektyw #ifndef
i #error
:
#ifndef (ENABLE_EXTRA_DEBUGGING)
// please include DefaultDefines.h if not already included.
# error "ENABLE_EXTRA_DEBUGGING is not defined"
#else
# if ( 1 == ENABLE_EXTRA_DEBUGGING )
//code
# endif
#endif
Makra
Makra są podzielone na dwie główne grupy: makra obiektowe i makra funkcyjne. Makra są traktowane jako podstawienie tokena na początku procesu kompilacji. Oznacza to, że duże (lub powtarzające się) sekcje kodu można wyodrębnić do makra preprocesora.
// This is an object-like macro
#define PI 3.14159265358979
// This is a function-like macro.
// Note that we can use previously defined macros
// in other macro definitions (object-like or function-like)
// But watch out, its quite useful if you know what you're doing, but the
// Compiler doesnt know which type to handle, so using inline functions instead
// is quite recommended (But e.g. for Minimum/Maximum functions it is quite useful)
#define AREA(r) (PI*(r)*(r))
// They can be used like this:
double pi_macro = PI;
double area_macro = AREA(4.6);
Biblioteka Qt wykorzystuje tę technikę do tworzenia systemu metaobiektu, gdy użytkownik deklaruje makro Q_OBJECT na czele zdefiniowanej przez użytkownika klasy rozszerzającej QObject.
Nazwy makr są zwykle pisane wielkimi literami, aby ułatwić ich odróżnienie od normalnego kodu. Nie jest to wymagane, ale jest przez wielu programistów uważane za dobry styl.
Kiedy napotkamy makro podobne do obiektu, jest ono rozwijane jako prosta operacja kopiuj-wklej, a nazwa makra zostaje zastąpiona definicją. Kiedy napotkamy makro podobne do funkcji, zarówno jego nazwa, jak i parametry są rozszerzane.
double pi_squared = PI * PI;
// Compiler sees:
double pi_squared = 3.14159265358979 * 3.14159265358979;
double area = AREA(5);
// Compiler sees:
double area = (3.14159265358979*(5)*(5))
Z tego powodu makropolecenia podobne do funkcji są często zawarte w nawiasach, jak w AREA()
powyżej. Zapobiega to wszelkim błędom, które mogą wystąpić podczas rozwijania makr, w szczególności błędom spowodowanym tym, że pojedynczy parametr makra składa się z wielu rzeczywistych wartości.
#define BAD_AREA(r) PI * r * r
double bad_area = BAD_AREA(5 + 1.6);
// Compiler sees:
double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6;
double good_area = AREA(5 + 1.6);
// Compiler sees:
double good_area = (3.14159265358979*(5 + 1.6)*(5 + 1.6));
Należy również pamiętać, że z powodu tego prostego rozszerzenia należy zachować ostrożność przy przekazywaniu parametrów do makr, aby zapobiec nieoczekiwanym skutkom ubocznym. Jeśli parametr zostanie zmodyfikowany podczas oceny, będzie modyfikowany za każdym razem, gdy zostanie użyty w rozszerzonym makrze, co zwykle nie jest tym, czego chcemy. Dzieje się tak nawet wtedy, gdy makro zawiera parametry w nawiasach, aby zapobiec rozbiciu niczego przez ekspansję.
int oops = 5;
double incremental_damage = AREA(oops++);
// Compiler sees:
double incremental_damage = (3.14159265358979*(oops++)*(oops++));
Ponadto makra nie zapewniają bezpieczeństwa typu, co prowadzi do trudnych do zrozumienia błędów dotyczących niedopasowania typu.
Ponieważ programiści zwykle kończą linie średnikiem, makra, które mają być używane jako samodzielne linie, są często zaprojektowane do „połykania” średnika; Zapobiega to powstawaniu niezamierzonych błędów przez dodatkowy średnik.
#define IF_BREAKER(Func) Func();
if (some_condition)
// Oops.
IF_BREAKER(some_func);
else
std::cout << "I am accidentally an orphan." << std::endl;
W tym przykładzie niezamierzony podwójny średnik powoduje uszkodzenie bloku if...else
, uniemożliwiając kompilatorowi dopasowanie else
do if
. Aby temu zapobiec, średnik jest pomijany w definicji makra, co spowoduje „połknięcie” średnika natychmiast po każdym jego użyciu.
#define IF_FIXER(Func) Func()
if (some_condition)
IF_FIXER(some_func);
else
std::cout << "Hooray! I work again!" << std::endl;
Pozostawienie końcowego średnika pozwala również na użycie makra bez kończenia bieżącego wyrażenia, co może być korzystne.
#define DO_SOMETHING(Func, Param) Func(Param, 2)
// ...
some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42));
Zwykle definicja makra kończy się na końcu linii. Jeśli makro musi obejmować wiele linii, można jednak zastosować ukośnik odwrotny na końcu linii, aby to wskazać. Ten odwrotny ukośnik musi być ostatnim znakiem w linii, który wskazuje preprocesorowi, że następna linia powinna zostać połączona z bieżącą linią, traktując ją jako pojedynczą linię. Można tego użyć wiele razy z rzędu.
#define TEXT "I \
am \
many \
lines."
// ...
std::cout << TEXT << std::endl; // Output: I am many lines.
Jest to szczególnie przydatne w złożonych makrach funkcyjnych, które mogą wymagać pokrycia wielu linii.
#define CREATE_OUTPUT_AND_DELETE(Str) \
std::string* tmp = new std::string(Str); \
std::cout << *tmp << std::endl; \
delete tmp;
// ...
CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.")
W przypadku bardziej złożonych makr funkcyjnych, przydatne może być nadanie im własnego zakresu, aby zapobiec możliwym kolizjom nazw lub spowodować zniszczenie obiektów na końcu makra, podobnie jak w przypadku rzeczywistej funkcji. Typowym idiomem tego jest do while 0 , gdzie makro jest zamknięte w bloku do-while . Po tym bloku zwykle nie występuje średnik, co pozwala mu połknąć średnik.
#define DO_STUFF(Type, Param, ReturnVar) do { \
Type temp(some_setup_values); \
ReturnVar = temp.process(Param); \
} while (0)
int x;
DO_STUFF(MyClass, 41153.7, x);
// Compiler sees:
int x;
do {
MyClass temp(some_setup_values);
x = temp.process(41153.7);
} while (0);
Istnieją również różne makra; podobnie jak funkcje wariadyczne, pobierają one zmienną liczbę argumentów, a następnie rozwijają je wszystkie zamiast specjalnego parametru „Varargs” __VA_ARGS__
.
#define VARIADIC(Param, ...) Param(__VA_ARGS__)
VARIADIC(printf, "%d", 8);
// Compiler sees:
printf("%d", 8);
Zauważ, że podczas rozwijania __VA_ARGS__
można umieścić w dowolnym miejscu definicji i zostanie poprawnie rozwinięte.
#define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo)
VARIADIC2(some_func, 3, 8, 6, 9);
// Compiler sees:
some_func(8, 6, 9, 3);
W przypadku parametru variadic z zerowym argumentem różne kompilatory będą inaczej obsługiwały przecinek końcowy. Niektóre kompilatory, takie jak Visual Studio, po cichu połykają przecinek bez specjalnej składni. Inne kompilatory, takie jak GCC, wymagają umieszczenia ##
bezpośrednio przed __VA_ARGS__
. Z tego powodu dobrze jest warunkowo zdefiniować różne makra, gdy problemem jest przenośność.
// In this example, COMPILER is a user-defined macro specifying the compiler being used.
#if COMPILER == "VS"
#define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__)
#elif COMPILER == "GCC"
#define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__)
#endif /* COMPILER */
Komunikaty o błędach preprocesora
Błędy kompilacji można generować za pomocą preprocesora. Jest to przydatne z wielu powodów, z których niektóre obejmują powiadamianie użytkownika, jeśli znajduje się on na nieobsługiwanej platformie lub nieobsługiwanym kompilatorze.
np. Błąd powrotu, jeśli wersja gcc to 3.0.0 lub wcześniejsza.
#if __GNUC__ < 3
#error "This code requires gcc > 3.0.0"
#endif
np. Błąd powrotu, jeśli kompilujesz na komputerze Apple.
#ifdef __APPLE__
#error "Apple products are not supported in this release"
#endif
Predefiniowane makra
Predefiniowane makra to makra zdefiniowane przez kompilator (w przeciwieństwie do tych zdefiniowanych przez użytkownika w pliku źródłowym). Makra te nie mogą być ponownie definiowane ani definiowane przez użytkownika.
Następujące makra są predefiniowane przez standard C ++:
-
__LINE__
zawiera numer linii, w której używane jest to makro, i może być zmieniony przez dyrektywę#line
. -
__FILE__
zawiera nazwę pliku, w którym jest używane to makro, i może być zmieniony przez dyrektywę#line
. -
__DATE__
zawiera datę (w formacie"Mmm dd yyyy"
) kompilacji pliku, gdzie Mmm jest sformatowany tak, jakby był uzyskany przez wywołaniestd::asctime()
. -
__TIME__
zawiera czas (w formacie"hh:mm:ss"
) kompilacji pliku. -
__cplusplus
jest definiowany przez (zgodne) kompilatory C ++ podczas kompilacji plików C ++. Jego wartością jest wersja standardowa, z którą kompilator jest w pełni zgodny, tj.199711L
dla C ++ 98 i C ++ 03,201103L
dla C ++ 11 i201402L
dla standardu C ++ 14.
-
__STDC_HOSTED__
ma wartość1
jeśli implementacja jest hostowana , lub0
jeśli jest wolnostojąca .
-
__STDCPP_DEFAULT_NEW_ALIGNMENT__
zawiera literałsize_t
, który jest wyrównaniem używanym do wywołaniaoperator new
nieświadomego dopasowywania.
Ponadto następujące makra mogą być wstępnie zdefiniowane przez implementacje i mogą, ale nie muszą być obecne:
-
__STDC__
ma znaczenie zależne od implementacji i zwykle jest definiowane tylko podczas kompilacji pliku jako C, co oznacza pełną zgodność ze standardem C. (Lub nigdy, jeśli kompilator zdecyduje się nie obsługiwać tego makra.)
-
__STDC_VERSION__
ma znaczenie zależne od implementacji, a jego wartością jest zwykle wersja C, podobnie jak__cplusplus
jest wersją C ++. (Lub nie jest nawet zdefiniowany, jeśli kompilator zdecyduje się nie obsługiwać tego makra.) -
__STDC_MB_MIGHT_NEQ_WC__
jest zdefiniowany na1
, jeśli wartości wąskiego kodowania podstawowego zestawu znaków mogą nie być równe wartościom ich szerokich odpowiedników (np. Jeśli(uintmax_t)'x' != (uintmax_t)L'x'
) -
__STDC_ISO_10646__
jest definiowany, jeśliwchar_t
jest zakodowany jako Unicode i rozwija się do stałej liczb całkowitych w postaciyyyymmL
, wskazując najnowszą obsługiwaną wersję Unicode. -
__STDCPP_STRICT_POINTER_SAFETY__
jest zdefiniowany na1
, jeśli implementacja ma ścisłe bezpieczeństwo wskaźnika (w przeciwnym razie ma zrelaksowane bezpieczeństwo wskaźnika ) -
__STDCPP_THREADS__
ma wartość1
, jeśli program może mieć więcej niż jeden wątek wykonania (dotyczy implementacji wolnostojącej - implementacje hostowane mogą zawsze mieć więcej niż jeden wątek)
Warto również wspomnieć o __func__
, która nie jest makrem, ale predefiniowaną zmienną lokalną dla funkcji. Zawiera nazwę funkcji, w której jest używany, jako statyczna tablica znaków w formacie zdefiniowanym przez implementację.
Oprócz tych standardowych predefiniowanych makr, kompilatory mogą mieć swój własny zestaw predefiniowanych makr. Aby się tego nauczyć, należy zapoznać się z dokumentacją kompilatora. Na przykład:
Niektóre makra służą jedynie do sprawdzania obsługi niektórych funkcji:
#ifdef __cplusplus // if compiled by C++ compiler
extern "C"{ // C code has to be decorated
// C library header declarations here
}
#endif
Inne są bardzo przydatne do debugowania:
bool success = doSomething( /*some arguments*/ );
if( !success ){
std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2
<< " in function " << __func__ << "()"
<< " in file " << __FILE__
<< std::endl;
}
I inne do trywialnej kontroli wersji:
int main( int argc, char *argv[] ){
if( argc == 2 && std::string( argv[1] ) == "-v" ){
std::cout << "Hello World program\n"
<< "v 1.1\n" // I have to remember to update this manually
<< "compiled: " << __DATE__ << ' ' << __TIME__ // this updates automagically
<< std::endl;
}
else{
std::cout << "Hello World!\n";
}
}
Makra X.
Idiomatyczna technika generowania powtarzających się struktur kodu w czasie kompilacji.
Makro X składa się z dwóch części: listy i wykonania listy.
Przykład:
#define LIST \
X(dog) \
X(cat) \
X(racoon)
// class Animal {
// public:
// void say();
// };
#define X(name) Animal name;
LIST
#undef X
int main() {
#define X(name) name.say();
LIST
#undef X
return 0;
}
który jest rozszerzany przez preprocesora na:
Animal dog;
Animal cat;
Animal racoon;
int main() {
dog.say();
cat.say();
racoon.say();
return 0;
}
W miarę powiększania się list (powiedzmy ponad 100 elementów) technika ta pomaga uniknąć nadmiernego wklejania kopii.
Źródło: https://en.wikipedia.org/wiki/X_Macro
Zobacz także: makra X.
Jeśli zdefiniowanie X
nieistotnego pod względem wizualnym przed użyciem LIST
nie jest w twoim guście, możesz przekazać nazwę makra jako argument:
#define LIST(MACRO) \
MACRO(dog) \
MACRO(cat) \
MACRO(racoon)
Teraz wyraźnie określasz, którego makra należy użyć podczas rozwijania listy, np
#define FORWARD_DECLARE_ANIMAL(name) Animal name;
LIST(FORWARD_DECLARE_ANIMAL)
Jeśli każde wywołanie MACRO
powinno przyjmować dodatkowe parametry - stałe względem listy, można użyć makr variadic
//a walkaround for Visual studio
#define EXPAND(x) x
#define LIST(MACRO, ...) \
EXPAND(MACRO(dog, __VA_ARGS__)) \
EXPAND(MACRO(cat, __VA_ARGS__)) \
EXPAND(MACRO(racoon, __VA_ARGS__))
Pierwszy argument jest dostarczany przez LIST
, a reszta jest dostarczana przez użytkownika w wywołaniu LIST
. Na przykład:
#define FORWARD_DECLARE(name, type, prefix) type prefix##name;
LIST(FORWARD_DECLARE,Animal,anim_)
LIST(FORWARD_DECLARE,Object,obj_)
rozwinie się do
Animal anim_dog;
Animal anim_cat;
Animal anim_racoon;
Object obj_dog;
Object obj_cat;
Object obj_racoon;
#pragma raz
Większość, ale nie wszystkie, implementacje C ++ obsługują dyrektywę #pragma once
która gwarantuje, że plik zostanie dołączony tylko raz w ramach jednej kompilacji. Nie jest częścią żadnego standardu ISO C ++. Na przykład:
// Foo.h
#pragma once
class Foo
{
};
Podczas gdy #pragma once
unika pewnych problemów związanych z #pragma
ochrony , #pragma
- z definicji w standardach - jest z natury specyficznym dla kompilatora hakiem i zostanie po cichu zignorowana przez kompilatory, które go nie obsługują. Projekty, które używają #pragma once
muszą zostać zmodyfikowane, aby były zgodne ze standardami.
W przypadku niektórych kompilatorów - szczególnie tych, które wykorzystują prekompilowane nagłówki - #pragma once
może spowodować znaczne przyspieszenie procesu kompilacji. Podobnie, niektóre preprocesory osiągają przyspieszenie kompilacji poprzez śledzenie, które nagłówki zastosowały obejmują osłony. Korzyści netto, gdy #pragma once
zarówno #pragma once
i osłony, zależą od implementacji i mogą być albo wzrostem, albo spadkiem czasów kompilacji.
#pragma once
połączeniu z osłonami dołączania było zalecanym układem plików nagłówkowych podczas pisania aplikacji opartych na MFC w systemie Windows i zostało wygenerowane przez add class
add dialog
, add windows
add dialog
i kreatorów add windows
programie Visual Studio. Dlatego bardzo często można je znaleźć w aplikacjach Windows C ++.
Operatory preprocesora
#
operator lub operator łańcuchowy służy do konwersji parametru Makro na literał łańcuchowy. Można go używać tylko z makrami mającymi argumenty.
// preprocessor will convert the parameter x to the string literal x
#define PRINT(x) printf(#x "\n")
PRINT(This line will be converted to string by preprocessor);
// Compiler sees
printf("This line will be converted to string by preprocessor""\n");
Kompilator łączy dwa ciągi, a końcowy argument printf()
będzie literałem ciągu ze znakiem nowej linii na końcu.
Preprocesor zignoruje spacje przed argumentem makra lub po nim. Tak więc poniższy wydruk da nam ten sam wynik.
PRINT( This line will be converted to string by preprocessor );
Jeśli parametr literału ciągu wymaga sekwencji ucieczki, jak przed podwójnym cudzysłowem (), zostanie on automatycznie wstawiony przez preprocesor.
PRINT(This "line" will be converted to "string" by preprocessor);
// Compiler sees
printf("This \"line\" will be converted to \"string\" by preprocessor""\n");
Operator ##
lub operator wklejania tokenów służy do łączenia dwóch parametrów lub tokenów makra.
// preprocessor will combine the variable and the x
#define PRINT(x) printf("variable" #x " = %d", variable##x)
int variableY = 15;
PRINT(Y);
//compiler sees
printf("variable""Y"" = %d", variableY);
i końcowy wynik będzie
variableY = 15