Директивы препроцессора - это особые инструкции для компилятора, данные перед вызовом компиляции реального кода. Таких директив несколько. Несколько важных категорий таких директив включают (i) определения макросов (ii) включения условий (iii) управление строкой и (iv) директивы об ошибках.

Макроопределения

Определения макросов можно получить с помощью #define и можно не определять с помощью #undef

Когда препроцессор C ++ встречает директиву #define, он заменяет любое вхождение идентификатора в остальной части тела на замену. Эта замена может быть выражением, оператором, блоком или просто чем угодно. Препроцессор не понимает C ++, он просто заменяет любое вхождение идентификатора на замену.

#define ARRAY_SIZE 100
int ar1[ARRAY_SIZE];
int ar2[ARRAY_SIZE];

После замены препроцессором ARRAY_SIZE код становится эквивалентным

int ar1[100];
int ar1[100];

Такое использование #define в качестве средства определения константы является обычным, но #define также может работать с параметрами для определения макросов функций:

#define getmax(a,b) a>b?a:b

Вот пример его использования:

#include <iostream>
using namespace std;

#define getmax(a,b) ((a)>(b)?(a):(b))

int main()
{
  int x=5, y;
  y= getmax(x,2);
  cout << y << endl;
  cout << getmax(7,x) << endl;
  return 0;
}

Макрос #define не зависит от структуры блока. Макрос существует до тех пор, пока он не будет определен с помощью директивы препроцессора #undef:

#define TABLE_SIZE 1000
int table1[TABLE_SIZE];
#undef TABLE_SIZE
#define TABLE_SIZE 2000
int table2[TABLE_SIZE]

После обработки макроса будет получен следующий код:

int table1[1000];
int table2[2000];

Определения макросов функций допускают использование двух специальных операторов (# и ##) в последовательности замены. Если оператор # используется до использования параметра в последовательности замены, этот параметр заменяется строковым литералом (как если бы он был заключен в двойные кавычки)

#define str(x) #x
cout << str(test);

что переводится как

cout << "test";

Однако ## объединяет два аргумента, не оставляя между ними пробелов:

#define join(a,b) a ## b
join(c,out) << "test";

что переводится как

cout << "test";

Условные включения (#ifdef, #ifndef, #if, #endif, #else и #elif)

Эти директивы позволяют нам включать или исключать часть кода при соблюдении определенных условий.

#ifdef позволяет компилировать часть программы только в том случае, если макрос, указанный в качестве параметра, был определен, независимо от его значения. Например

#ifdef TABLE_SIZE
int table[TABLE_SIZE];
#endif

В этом случае строка кода int table[TABLE_SIZE]; компилируется только в том случае, если TABLE_SIZE был ранее определен с #define, независимо от его значения. Если он не был определен, эта строка не будет включена в компиляцию программы.

#ifndef служит прямо противоположной цели: код между директивами #ifndef и #endif компилируется только в том случае, если указанный идентификатор не был ранее определен. Например

#ifndef TABLE_SIZE
#define TABLE_SIZE 100
#endif
int table[TABLE_SIZE];

В этом случае, если элемент управления поступает в этот фрагмент кода, а макрос TABLE_SIZE еще не определен, ему будет присвоено значение 100. Если он уже существует, он сохранит свое предыдущее значение, поскольку директива #define не будет быть казненным.

Директивы #if, #else и #elif (т. Е. «Else if») служат для определения некоторых условий, которые должны быть выполнены для компиляции той части кода, которую они окружают. Условие, которое следует за #if или #elif, может оценивать только константные выражения, включая макровыражения. Например:

#if TABLE_SIZE>200
#undef TABLE_SIZE
#define TABLE_SIZE 200
 
#elif TABLE_SIZE<50
#undef TABLE_SIZE
#define TABLE_SIZE 50
 
#else
#undef TABLE_SIZE
#define TABLE_SIZE 100
#endif
 
int table[TABLE_SIZE];

Обратите внимание, как связанные директивы #if, #elif и #else заканчиваются на #endif.

Поведение #ifdef и #ifndef также может быть достигнуто с помощью специальных операторов defined и !defined соответственно в любой из директив #if или #elif:

#if !defined TABLE_SIZE
#define TABLE_SIZE 100
#elif defined ARRAY_SIZE
#define TABLE_SIZE ARRAY_SIZE
int table[TABLE_SIZE];
#endif

Элемент управления строкой (#line)

Когда мы компилируем программу и в процессе компиляции возникают ошибки, компилятор показывает сообщения об ошибках со ссылками на имя файла, в котором произошла ошибка, и номер строки, поэтому легче найти код, генерирующий ошибку.

Директива #line позволяет нам контролировать и то, и другое: номера строк в файлах кода, а также имя файла, которое мы хотим отображать при возникновении ошибки. Его формат:

#line number “filename”

где номер - это номер новой строки, который будет присвоен следующей строке кода. С этого момента номера следующих друг за другом строк будут увеличиваться одна за другой.

“filename” - необязательный параметр, позволяющий переопределить имя файла, который будет отображаться. Например:

#include<iostream>
int main()
{
#line 20 "assigning variable"
int a?;
}

Этот код сгенерирует ошибку, которая будет отображаться как ошибка

assigning variable: In function ‘int main()’:
assigning variable:20:6: error: expected initializer before ‘?’ token

Er директива ror (#error)

Эта директива прерывает процесс компиляции, когда он обнаруживается, генерируя ошибку компиляции, которую можно указать в качестве ее параметра:

#ifndef __cplusplus
#error A C++ compiler is required!
#endif

Поскольку замена препроцессора происходит перед любой проверкой синтаксиса C ++, определения макросов - сложная функция. Код, основанный на сложных макросах, может показаться непонятным другим программистам, поскольку ожидаемый ими синтаксис может отличаться от стандартного соглашения, к которому привыкли программисты.