Мы уже пользовались разными директивами препроцессора. Даже в самом первом примере была одна. Рассмотрим самое главное (не будем рассматривать диграфы и триграфы).
Директива #include <...> включает содержимое заголовочного файла в ваш файл. Угловые скобки нужны для включения стандартных заголовочных файлов, кавычки — ваших собственных и библиотечных.
#include <stdio.h> // Стандартные заголовочные файлы.
#include "my_lib.h" // Заголовочные файлы библиотек и проекта.
Также мы уже пользовались директивой #define для задания констант. Они называются макросами (макроопределениями).
#define SIZE 10u
У этой директивы есть и такая форма.
#define FOO
У макросов могут быть аргументы.
#define INC(x) (x) + 1
printf("%d", INC(6)); // 7
Внутри макросов можно делать переносы.
#define INC(x) \
(x) + 1
Определим макрос, умножающий два числа.
#define MUL(x, y) x * y
И получим неверный результат.
printf("%d", MUL(2 + 2, 2 + 1)); // 7, а не 12.
Это получается потому, что макрос — простая текстовая подстановка. Посмотрите: 2 + 2 * 2 + 1, результат действительно 7. Добавим скобки, тогда всё будет в порядке. Это не единственный минус макросов, ещё вызовы не могут быть рекурсивными, а их отладка усложнена.
#define MUL(x, y) (x) * (y)
Определение идентификатора можно и отменить.
#undef FOOBAR
Директива #error выводит сообщение (если оно указано) и останавливает компиляцию. Ниже, где объясняется условная компиляция, есть пример использования.
#error Error code 7
Директива #warning также выводит сообщение (если оно указано), но не останавливает компиляцию.
#warning Warning: possible loss of data
#pragma message ("Warning: possible loss of data") // Visual Studio.
Директива #pragma обозначает действия препроцессора или компилятора, которые зависят от конкретной реализации.
Существует ряд предопределённых макросов. С их помощью, к примеру, можно подставить в исходный код номер строки или путь к файлу с исходным кодом.
printf("%d\n", __LINE__); // Выводит номер строки в исходном файле.
printf("%s\n", __FILE__); // Выводит полный путь к исходному файлу.
С помощью специальных макросов можно осуществлять условную компиляцию для разных операционных систем.
Эти операторы используются при создании макросов. Оператор # перед параметром макроса добавляет кавычки.
#define STRING(bar) # bar
printf(STRING(42)); // printf("42");
Оператор ## в макросах объединяет две части (две лексемы) в одну.
#define POSITION(bar) int bar##_x, bar##_y
POSITION(foo); // int foo_x, foo_y;
Можно производить условную компиляцию (компиляцию одних частей программы и игнорирование других) с помощью директив #ifdef и #ifndef.
#ifdef FOO
// Если идентификатор FOO ранее был определён, то эта часть программы будет скомпилирована.
// Иначе этот код будет проигнорирован.
#endif
#ifndef FOO
// Если идентификатор FOO ранее не был определён, эта часть программы будет скомпилирована.
// Иначе этот код будет проигнорирован.
#endif
С помощью директив #if, #elif и #else можно организовывать сложные проверки.
#define FOO 7
#if FOO == 7
puts("Will work fine.");
#elif FOO < 7
puts("Possible loss of data.");
#else
#error Error code 7
#endif
Теперь подробнее остановимся на самом распространённом случае условной компиляции — защите от повторного включения (когда директивы включения приводят к этому). Все объявления размещаются в одном файле, определения — в другом.
// my_lib.h
#ifndef __MY_LIB__
#define __MY_LIB__
// Объявления переменных, структур, функций и т. д.
#endif
// my_lib.c
#include "my_lib.h"
// Определения...
Директива #ifndef позаботится обо всём. Если файл включается впервые, то есть идентификатор __MY_LIB__ ещё не был определён, то он будет определён. А при последующих включениях файла весь код до #endif будет проигнорирован. Просто и элегантно.
Предлагаю самостоятельно найти в вашем компиляторе способ посмотреть результат работы препроцессора (удобно для отладки).