Препроцессор является обязательным компонентом компилятора языка Си. Вообще весь процесс компиляции программы на языке Си разбивается на три этапа:
Препроцессор
Компиляция
Линковка
Препроцессор обрабатывает исходный текст программы до ее непосредственной компиляции. Результатом работы препроцессора является полный текст программы, который передается на компиляцию в исполняемый файл.
Затем компилятор компилирует обработанный препроцессором исходный код в объектные файлы.
И на последнем этапе линкер (линковщик) объединяет (линкует) объектные файлы в один исполняемый файл или файл динамической библиотеки.
Для управления препроцессором применяются директивы, каждая из которых начинается с символа решетки # и располагается на отдельной строке. Препроцессор просматривает текст программы, находит эти директивы и должным образом обрабатывает их.
Мы можем использовать следующие директивы:
#define: определяет макрос или препроцессорный идентификатор
#undef: отменяет определение макроса или идентификатора
#ifdef: проверяет, определен ли идентификатор
#ifndef: проверяет неопределенности идентификатор
#include: включает текст из файла
#if: проверяет условие выражение (как условная конструкция if)
#else: задает альтернативное условие для #if
#endif: окончание условной директивы #if
#elif: задает альтернативное условие для #if
#line: меняет номер следующей ниже строки
#error: формирует текст сообщения об ошибке трансляции
#pragma: определяет действия, которые зависят от конкретной реализации компилятора
#: пустая директива, по сути ничего не делает
Рассмотрим основные из этих директив.
Ранее уже использовалась директива #include. Эта директива подключает в исходный текст файлы. Она имеет следующие формы применения:
#include <имя_файла> // имя файла в угловых скобках #include "имя_файла" // имя файла в кавычках
Например, если нам надо задействовать в приложении консольный ввод-вывод с помощью функций printf()
или scanf()
,
то нам надо подключить файл "stdio.h", который содержит определение этих функций:
#include <stdio.h>
При выполнении этой директивы препроцессор вставляет текст файла stdio.h
. Данный файл еще называется заголовочным. Заголовочные файлы содержат прототипы функций, определения и описания
типов и констант и имеют расширение .h.
Поиск файла производится стандартных системных каталогах. Вообще есть стандартный набор встроенных заголовочных файлов, который определяется стандартом языка и которые мы можем использовать:
assert.h: отвечает за диагностику программ
complex.h: для работы с комплексными числами
ctype.h: отвечает за преобразование и проверку символов
errno.h: отвечает за проверку ошибок
fenv.h: для доступа к окружению, которое управляет операциями с числами с плавающей точкой
float.h: отвечает за работу с числами с плавающей точкой
inttypes.h: для работы с большими целыми числами
iso646.h: содержит ряд определений, которые расширяют ряд логических операций
limits.h: содержит предельные значения целочисленных типов
locale.h: отвечает за работу с локальной культурой
math.h: для работы с математическими выражениями
setjmp.h: определяет возможности нелокальных переходов
signal.h: для обработки исключительных ситуаций
stdalign.h: для выравнивания типов
stdarg.h: обеспечивает поддержку переменного числа параметров
stdatomic.h: для выполнения атомарных операций по разделяемым данным между потоками
stdbool.h: для работы с типом _Bool
stddef.h: содержит ряд вспомогательных определений
stdint.h: для работы с целыми числами
stdio.h: для работы со средствами ввода-вывода
stdlib.h: содержит определения и прототипы функций общего пользования
stdnoreturn.h: содержит макрос noreturn
string.h: для работы со строками
tgmath.h: подключает math.h и complex.h плюс добавляет дополнительные возможности по работе с математическими вычислениями
threads.h: для работы с потоками
time.h: для работы с датами и временем
uchar.h: для работы с символами в кодировке Unicode
wchar.h: для работы с символами
wctype.h: содержит дополнительные возможности для работы с символами
Однако стоит отметить, что в различных средах к этому набору могут добавляться дополнительные встроенные заголовочные файлы для тех или иных целей, например, для работы с графикой.
Кроме стандартных заголовочных файлов мы можем подключать и свои файлы. Например, в той же папке, где находиться главный файл программы, определим еще один файл, который назовем numbers.c.
Определим в нем следующий код:
int number = 5;
Здесь просто определена одна переменная. Теперь подключим этот файл в главный файл программы, который, допустим, называется app.c:
#include <stdio.h> #include "numbers.c" int main(void) { printf("Number: %d", number); // Number: 5 return 0; }
При подключении своих файлов их имя указывается в кавычках. И несмотря на то, что в программе не определена переменная number, она будет браться из подключенного файла numbers.c. Но опять же отмечу, важно, что в данном случае файл numbers.c располагается в одной папке с главным файлов программы.
В то же время данный способ прекрасно работает в GCC. Но для разных сред программирования способ подключения файлов может отличаться. Например, в Visual Studio мы получим ошибку. И более правильный подход будет состоять в том, что определить объявление объекта (переменной/константы) или функции в дополнительном заголовочном файле, а определение объекта или функции поместить в стандартный файл с расширением .c.
Например, в нашем в файле numbers.c уже есть определение переменной number. Теперь в ту же папку добавим новый файл numbers.h - файл с тем же названием, но другим расширением.
И определим в numbers.h следующий код:
extern int number;
Ключевое слово extern указывает, что данный объект является внешним. И в этом случае мы могли бы его подключить в файл исходного кода:
#include <stdio.h> #include "numbers.h" // объявление или описание объекта int main(void) { printf("%d", number); return 0; }
Далее чтобы скомпилировать эту программу, передадим компилятору GCC оба файла с исходным кодом - app.c и numbers.c:
gcc app.c numbers.c -o app.exe && app
Компилятору передаются файлы через пробел и компилируются в один исполняемый файл.
Если разработка ведется в Visual Studio, то не надо подключать файл с исходным кодом (numbers.c
), однако чтобы Visual Studio видела функционал файла
в процессе разработки, может потребоваться подключить заголовочный файл (numbers.h
). При этом заголовочный файл numbers.h
помещается в папку Headers Files.