Препроцессорные директивы. Директивы препроцессора
Препроцессор
- это специальная программа, являющаяся частью компилятора языка Си. Она предназначена для предварительной обработки текста программы. Препроцессор позволяет включать в текст программы файлы и вводить макроопределения.
Работа препроцессора осуществляется с помощью специальных директив (указаний). Они отмечаются знаком решетка #. По окончании строк, обозначающих директивы в языке Си, точку с запятой можно не ставить.
Основные директивы препроцессора
#include
- вставляет текст из указанного файла
#define
- задаёт макроопределение (макрос) или символическую константу
#undef
- отменяет предыдущее определение
#if
- осуществляет условную компиляцию при истинности константного выражения
#ifdef
- осуществляет условную компиляцию при определённости символической константы
#ifndef
- осуществляет условную компиляцию при неопределённости символической константы
#else
- ветка условной компиляции при ложности выражения
#elif
- ветка условной компиляции, образуемая слиянием else и if
#endif
- конец ветки условной компиляции
#line
- препроцессор изменяет номер текущей строки и имя компилируемого файла
#error
- выдача диагностического сообщения
#pragma
- действие, зависящее от конкретной реализации компилятора.
Директива #include
Директива #include
позволяет включать в текст программы указанный файл. Если файл является стандартной библиотекой и находится в папке компилятора, он заключается в угловые скобки <>
.
Если файл находится в текущем каталоге проекта, он указывается в кавычках ""
. Для файла, находящегося в другом каталоге необходимо в кавычках указать полный путь.
#include
#include
"func.c"
Директива #define
Директива #define
позволяет вводить в текст программы константы и макроопределения.
Общая форма записи
#define
Идентификатор Замена
Поля Идентификатор и Замена разделяются одним или несколькими пробелами.
Директива #define указывает компилятору, что нужно подставить строку, определенную аргументом Замена , вместо каждого аргумента Идентификатор в исходном файле. Идентификатор не заменяется, если он находится в комментарии, в строке или как часть более длинного идентификатора.
1
2
3
4
5
6
7
8
#include
#define
A 3
int
main()
{
printf("%d + %d = %d"
, A, A, A+A); // 3 + 3 = 6
getchar();
return
0;
}
В зависимости от значения константы компилятор присваивает ей тот или иной тип. С помощью суффиксов можно переопределить тип константы:
- U или u представляет целую константу в беззнаковой форме (unsigned );
- F (или f ) позволяет описать вещественную константу типа float ;
- L (или l ) позволяет выделить целой константе 8 байт (long int );
- L (или l ) позволяет описать вещественную константу типа long double
#define
A 280U // unsigned int
#define
B 280LU // unsigned long int
#define
C 280 // int (long int)
#define
D 280L // long int
#define
K 28.0 // double
#define
L 28.0F // float
#define
M 28.0L // long double
Вторая форма синтаксиса определяет макрос, подобный функции, с параметрами. Эта форма допускает использование необязательного списка параметров, которые должны находиться в скобках. После определения макроса каждое последующее вхождение
идентификатор(аргумент1, ..., агрументn)
замещается версией аргумента замена , в которой вместо формальных аргументов подставлены фактические аргументы.
Пример на Си
: Вычисление синуса угла
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include
#include
#include
#define
PI 3.14159265
#define
SIN(x) sin(PI*x/180)
int
main()
{
int
c;
system("chcp 1251"
);
system("cls"
);
printf("Введите угол в градусах: "
);
scanf("%d"
, &c);
printf("sin(%d)=%lf"
, c, SIN(c));
getchar(); getchar();
return
0;
}
Результат выполнения
Отличием таких макроопределений от функций в языке Си является то, что на этапе компиляции каждое вхождение идентификатора замещается соответствующим кодом. Таким образом, программа может иметь несколько копий одного и того же кода, соответствующего идентификатору. В случае работы с функциями программа будет содержать 1 экземпляр кода, реализующий указанную функцию, и каждый раз при обращении к функции ей будет передано управление.
Отменить макроопределение можно с помощью директивы #undef
.
Однако при использовании таких макроопределений следует соблюдать осторожность, например
1
2
3
4
5
6
7
8
9
10
11
12
13
#include
#define
sum(A,B) A+B
int
main()
{
int
a, b, c, d;
a = 3; b = 5;
getchar();
return
0;
}
Результат выполнения:
По умолчанию текст макроопределения должен размещаться на одной строке. Если требуется перенести текст макроопределения на новую строку, то в конце текущей строки ставится символ "обратный слеш" — \ .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include
#define
sum(A,B) A + \
B
int
main()
{
int
a, b, c, d;
a = 3; b = 5;
c = (a + b) * 2; // c = (a + b)*2
d = sum(a, b) * 2; // d = a + b*2;
printf(" a = %d\n b = %d\n"
, a, b);
printf(" c = %d \n d = %d \n"
, c, d);
getchar();
return
0;
}
Кроме того, директива #define позволяет замещать часть идентификатора. Для указания замещаемой части используется ## .
1
2
3
4
5
6
7
8
9
#include
#define
SUM(x,y) (a##x + a##y)
int
main()
{
int
a1 = 5, a2 = 3;
printf("%d"
, SUM(1, 2)); // (a1 + a2)
getchar();
return
0;
}
Результат выполнения:
Директивы #if
или #ifdef/#ifndef
вместе с директивами #elif
, #else
и #endif
управляют компиляцией частей исходного файла.
Если указанное выражение после #if
имеет ненулевое значение, в записи преобразования сохраняется группа строк, следующая сразу за директивой #if
. Синтаксис условной директивы следующий:
1
2
3
4
5
6
7
#if
константное выражение
группа операций
#elif
константное выражение
группа операций
#else
группа операций
#endif
Отличие директив #ifdef/#ifndef заключается в том, что константное выражение может быть задано только с помощью #define .
У каждой директивы #if в исходном файле должна быть соответствующая закрывающая директива #endif . Между директивами #if и #endif может располагаться любое количество директив #elif , однако допускается не более одной директивы #else . Директива #else , если присутствует, должна быть последней перед директивой #endif .
Пример
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
#include
#define
P 2
int
main()
{
system("chcp 1251"
);
system("cls"
);
#if
P==1
printf("Выполняется ветка 1"
);
#elif
P==2
printf("Выполняется ветка 2, P=%d"
, P);
#else
printf("Выполняется другая ветка, P=%d"
, P);
#endif
getchar();
return
0;
}
Последнее обновление: 22.05.2017
Препроцессор является обязательным компонентом компилятора языка Си. Препроцессор обрабатывает исходный текст программы до ее непосредственной компиляции. Результатом работы препроцессора является полный текст программы, который передается на компиляцию в исполняемый файл.
Для управления препроцессором применяются директивы, каждая из которых начинается с символа решетки # и располагается на отдельной строке. Препроцессор просматривает текст программы, находит эти директивы и должным образом обрабатывает их.
Мы можем использовать следующие директивы:
#define : определяет макрос или препроцессорный идентификатор
#undef : отменяет определение макроса или идентификатора
#ifdef : проверяет, определен ли идентификатор
#ifndef : проверяет неопределенности идентификатор
#include : включает текст из файла
#if : проверяет условие выражение (как условная конструкция if)
#else : задает альтернативное условие для #if
#endif : окончание условной директивы #if
#elif : задает альтернативное условие для #if
#line : меняет номер следующей ниже строки
#error : формирует текст сообщения об ошибке трансляции
#pragma : определяет действия, которые зависят от конкретной реализации компилятора
# : пустая директива, по сути ничего не делает
Рассмотрим основные из этих директив.
Директива #include. Включение файлов
Ранее уже использовалась директива #include . Эта директива подключает в исходный текст файлы. Она имеет следующие формы применения:
#include <имя_файла> // имя файла в угловых скобках #include "имя_файла" // имя файла в кавычках
Например, если нам надо задействовать в приложении консольный ввод-вывод с помощью функций printf() или scanf() , то нам надо подключить файл "stdio.h", который содержит определение этих функций:
#include
При выполнении этой директивы препроцессор вставляет текст файла 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 : содержит дополнительные возможности для работы с символами
Однако стоит отметить, что в различных средах к этому набору могут добавляться дополнительные встроенные заголовочные файлы для тех или иных целей, например, для работы с графикой.
Кроме стандартных заголовочных файлов мы можем подключать и свои файлы. Например, в той же папке, где находиться главный файл программы, определим еще один файл, который назовем main.c . Определим в нем следующий код:
Int number = 5;
Здесь просто определена одна переменная. Теперь подключим этот файл в программу:
#include
При подключении своих файлов их имя указывается в кавычках. И несмотря на то, что в программе не определена переменная number, она будет браться из подключенного файла main.c. Но опять же отмечу, важно, что в данном случае файл main.c располагается в одной папке с главным файлов программы.
В то же время данный способ прекрасно работает в GCC. Но для разных сред программирования способ подключения файлов может отличаться. Например, в Visual Studio мы получим ошибку. И более правильный подход будет состоять в том, что определить объявление объекта (переменной/константы) или функции в дополнительном заголовочном файле, а определение объекта или функции поместить в стандартный файл с расширением .c .
Например, в нашем в файле main.c уже есть определение переменной number. Теперь в ту же папку добавим новый файл main.h - файл с тем же названием, но другим расширением. И определим в main.h следующий код:
Extern int number;
Ключевое слово extern указывает, что данный объект является внешним. И в этом случае мы могули бы его подключить в файл исходного кода:
#include
Этот пример также будет работать в GCC, однако как уже было рассмотрено выше, подключение файла main.h для GCC необязательно.
Если разработка ведется в Visual Studio , то определение объекта не надо подключать исходный файл:
#include
Несмотря на то, что здесь явным образом не подключается файл main.c, но при трансляции Visual Studio через заголовочный файл main.h сможет подключить находящийся в той же папке файл main.c.
В этой статье мы продолжим постигать искусство программирования на языке С++. На этом этапе обучения пора познакомится с такими вещами, как директивы препроцессора. Забегая наперед скажу, что в предыдущих уроках мы уже использовали директиву #include , которая служит для подключения заголовочных файлов.
Вначале дадим определение, что такое препроцессор. Компиляция любой программы происходит в несколько этапов, при чем один из первых — обработка препроцессором. Если говорить простыми словами, то препроцессор это такая программа, которая считывает исходный код программы и на основе директив изменяет его. Схематически весь процесс сборки программы можно представить следующим образом.
Как видно перед самой компиляцией исходный текст программы обрабатывает препроцессор, давайте познакомимся с его инструкциями поближе.
Начнем с директивы #include, которая заменяется препроцессором на содержимое следующего за ней файла. Пример использования #include:
#include
#include «header2.h»
Если имя файл заключено в угловые скобки, то препроцессор ищет файл в предопределенном месте. Использование двойных скобок предполагает подключение файла с того же каталога, где лежит исходный код компилируемой программы. Стоит также заметить, что подключаемые файлы также могут содержать в себе директивы препроцессора, в частности директиву #include, поэтому могут возникнуть проблемы с многократным подключением одного и того же файла. Для избежания подобного рода путаницы были введены условные директивы, давайте рассмотрим пример их использования:
#ifndef CUCUMBLER_H
#define CUCUMBLER_H
/* содержимое файла cucumbler.h */
Директива #ifndef выполняет проверку не была ли определена константа CUCUMBLER_H ранее, и если ответ отрицательный, то выполняется определение данной константы, и прочего кода, который следует до директивы #endif. Как не сложно догадаться директива #define определяет константу CUCUMBLER_H. В данном случае подобный кусок кода помогает избежать многократного включения одного и того же кода, так как после первого включения проинициализируется константа CUCUMBLER_H и последующие проверки #ifndef CUCUMBLER_H будут возвращать FALSE.
Директива #define широко применяется и при отладке программы.
#include
#include
#include
using namespace std;
cout << "Начало функции main()\n";
vector
text_array; while (cin >> text)
cout << "Прочитан текст: " << text << "\n";
text_array.push_back(text);
Если константа IN_DEBUG не задана, то препроцессор сгенерирует следующий исходник:
#include
#include
#include
using namespace std;
vector
text_array; while (cin >> text)
text_array.push_back(text);
Но если определить IN_DEBUG, то текст программы кардинальным образом поменяется
#include
#include
#include
using namespace std;
cout << "Начало функции main()\n";
vector
text_array; while (cin >> text)
cout << "Прочитан текст: " << text << "\n";
text_array.push_back(text);
Задать препроцессорную константу можно прямо из консоли. Например для компилятора g++ применяется следующий формат
Заголовочные файлы включаются в текст программы с помощью директивы препроцессора
#include. Директивы препроцессора начинаются со знака "диез" (#),
который должен быть самым первым символом строки. Программа, которая обрабатывает
эти директивы, называется препроцессором
(в современных компиляторах
препроцессор обычно является частью самого компилятора).
Директива #include включает в программу содержимое указанного файла. Имя файла
может быть указано двумя способами:
#include
Если имя файла заключено в угловые скобки (<>), считается, что нам нужен
некий стандартный заголовочный файл, и компилятор ищет этот файл в предопределенных
местах. (Способ определения этих мест сильно различается для разных платформ
и реализаций.) Двойные кавычки означают, что заголовочный файл - пользовательский,
и его поиск начинается с того каталога, где находится исходный текст программы.
Заголовочный файл также может содержать директивы #include. Поэтому иногда трудно
понять, какие же конкретно заголовочные файлы включены в данный исходный текст,
и некоторые заголовочные файлы могут оказаться включенными несколько раз. Избежать
этого позволяют условные директивы препроцессора
. Рассмотрим пример:
#ifndef BOOKSTORE_H #define BOOKSTORE_H /* содержимое файла bookstore.h */ #endif
Условная директива #ifndef проверяет, не было ли значение BOOKSTORE_H определено
ранее. (BOOKSTORE_H - это константа препроцессора; такие константы принято писать
заглавными буквами.) Препроцессор обрабатывает следующие строки вплоть до директивы
#endif. В противном случае он пропускает строки от #ifndef до # endif.
Директива
#define BOOKSTORE_H
определяет константу препроцессора BOOKSTORE_H. Поместив эту директиву непосредственно
после директивы #ifndef, мы можем гарантировать, что содержательная часть заголовочного
файла bookstore.h будет включена в исходный текст только один раз, сколько бы
раз ни включался в текст сам этот файл.
Другим распространенным примером применения условных директив препроцессора
является включение в текст программы отладочной информации. Например:
Int main()
{
#ifdef DEBUG
cout << "Начало выполнения main()\n";
#endif
string word;
vector
Если константа DEBUG не определена, результирующий текст программы будет выглядеть так:
Int main()
{
string word;
vector
В противном случае мы получим:
Int main()
{
cout << "Начало выполнения main()\n";
string word;
vector
Константа препроцессора может быть определена в командной строке при вызове компилятора с помощью опции -D (в различных реализациях эта опция может называться по-разному). Для UNIX-систем вызов компилятора с определением препроцессорной константы DEBUG выглядит следующим образом:
$ CC -DDEBUG main.C
Есть константы, которые автоматически определяются компилятором. Например, мы можем узнать, компилируем ли мы С++ или С программу. Для С++ программы автоматически определяется константа __cplusplus (два подчеркивания). Для стандартного С определяется __STDC__. Естественно, обе константы не могут быть определены одновременно. Пример:
#idfef __cplusplus // компиляция С++ программы extern "C"; // extern "C" объясняется в главе 7 #endif int main(int,int);
Другими полезными предопределенными константами (в данном случае лучше сказать переменными) препроцессора являются __LINE__ и __FILE__. Переменная __LINE__ содержит номер текущей компилируемой строки, а __FILE__ - имя компилируемого файла. Вот пример их использования:
If (element_count == 0) cerr << "Ошибка. Файл: " << __FILE__ << " Строка: " << __LINE__ << "element_count не может быть 0";
Две константы __DATE__ и __TIME__ содержат дату и время компиляции.
Стандартная библиотека С предоставляет полезный макрос assert(), который проверяет
некоторое условие и в случае, если оно не выполняется, выдает диагностическое
сообщение и аварийно завершает программу. Мы будем часто пользоваться этим полезным
макросом в последующих примерах программ. Для его применения следует включить
в программу директиву
#include
assert.h - это заголовочный файл стандартной библиотеки С. Программа на C++
может ссылаться на заголовочный файл как по его имени, принятому в C, так и
по имени, принятому в C++. В стандартной библиотеке С++ этот файл носит имя
cassert. Имя заголовочного файла в библиотеке С++ отличается от имени соответствующего
файла для С отсутствием расширения.h и подставленной спереди буквой c (выше
уже упоминалось, что в заголовочных файлах для C++ расширения не употребляются,
поскольку они могут зависеть от реализации).
Эффект от использования директивы препроцессора #include зависит от типа заголовочного
файла. Инструкция
#include
включает в текст программы содержимое файла cassert. Но поскольку все имена, используемые в стандартной библиотеке С++, определены в пространстве std, имя assert() будет невидимо до тех пор, пока мы явно не сделаем его видимым с помощью следующей using-директивы:
Using namespace std;
Если же мы включаем в программу заголовочный файл для библиотеки С
#include
то надобность в using-директиве отпадает: имя assert() будет видно и так. (Пространства имен используются разработчиками библиотек для предотвращения засорения глобального пространства имен. В разделе 8.5 эта тема рассматривается более подробно.)
Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа # и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.
Директива #include
Вы уже видели директиву #include в действии. Когда вы #include файл, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строчки #include. Это очень полезно при использовании определённых данных (например, функций) сразу в нескольких местах.
Директива #include имеет две формы:
#include
#include "filename"
, которая сообщает препроцессору искать файл в текущей директории проекта. Если его там не окажется, то препроцессор начнёт проверять системные пути и любые другие, которые вы указали в настройках вашей . Эта форма используется для подключения пользовательских заголовочных файлов.
Директива #define
Директиву #define можно использовать для создания макросов. Макрос — это правило, которое определяет конвертацию идентификатора в указанные данные.
Есть два основных типа макросов: макросы-функции и макросы-объекты.
Макросы-функции ведут себя как функции и используются в тех же целях. Мы не будем сейчас их обсуждать, так как их использование, как правило, считается опасным, и почти всё, что они могут сделать, можно осуществить с помощью простой (линейной) функции.
Макросы-объекты можно определить одним из двух следующих способов:
#define identifier
#define identifier substitution_text
Верхнее определение не имеет никакого substitution_text , в то время как нижнее — имеет. Поскольку это директивы препроцессора (а не простые ), то ни одна из форм не заканчивается точкой с запятой.
Макросы-объекты с substitution_text
Когда препроцессор встречает макросы-объекты с substitution_text , то любое дальнейшее появление identifier заменяется на substitution_text . Идентификатор обычно пишется заглавными буквами с символами подчёркивания вместо пробелов.
Рассмотрим следующий фрагмент кода:
#define MY_FAVORITE_NUMBER 9 std::cout << "My favorite number is: " << MY_FAVORITE_NUMBER << std::endl;
#define MY_FAVORITE_NUMBER 9 std :: cout << "My favorite number is: " << MY_FAVORITE_NUMBER << std :: endl ; |
Препроцессор преобразует код выше в:
std::cout << "My favorite number is: " << 9 << std::endl;
Любое дальнейшее появление идентификатора USE_YEN удаляется и заменяется «ничем» (пустым местом)!
Это может показаться довольно бесполезным, однако, это не основное предназначение подобных директив. В отличие от макросов-объектов с substitution_text , эта форма макросов считается приемлемой для использования.
Условная компиляция
Директивы препроцессора условной компиляции позволяют определить, при каких условиях код будет компилироваться, а при каких — нет. В этом уроке мы рассмотрим только три директивы условной компиляции:
#ifdef;
#ifndef;
#endif.
Директива #ifdef (англ. «if def ined» = «если определено») позволяет препроцессору проверить, было ли значение ранее #define. Если да, то код между #ifdef и #endif скомпилируется. Если нет, то код будет проигнорирован. Например:
#define PRINT_JOE #ifdef PRINT_JOE std::cout << "Joe" << std::endl; #endif #ifdef PRINT_BOB std::cout << "Bob" << std::endl; #endif
#define PRINT_JOE #ifdef PRINT_JOE std :: cout << "Joe" << std :: endl ; #endif #ifdef PRINT_BOB std :: cout << "Bob" << std :: endl ; #endif |
Поскольку PRINT_JOE уже был #define, то строчка std::cout << "Joe" << std::endl; скомпилируется и выполнится. А поскольку PRINT_BOB не был #define, то строчка std::cout << "Bob" << std::endl; не скомпилируется и, следовательно, не выполнится.
Директива #ifndef (англ. «if n ot def ined» = «если не определено») - это полная противоположность #ifdef, которая позволяет проверить, не было ли значение ранее определено. Например:
#ifndef PRINT_BOB std::cout << "Bob" << std::endl; #endif
#ifndef PRINT_BOB std :: cout << "Bob" << std :: endl ; #endif |
Результатом выполнения этого фрагмента кода будет Bob , так как PRINT_BOB ранее никогда не был #define. Условная компиляция очень часто используется в качестве header guards (о них мы поговорим в следующем уроке).
Область видимости директивы #define
Директивы выполняются перед компиляцией программы: сверху вниз, файл за файлом.
Рассмотрим следующую программу:
#include
#include void boo () #define MY_NAME "Alex" int main () std :: cout << "My name is: " << MY_NAME ; return 0 ; |
Несмотря на то, что директива #define MY_NAME "Alex" определена внутри функции boo, препроцессор этого не заметит, так как он не понимает такие понятия C++, как функции. Следовательно, выполнение этой программы будет идентично той, в которой бы #define MY_NAME "Alex" было определено ДО, либо сразу ПОСЛЕ функции boo. Для лучше читабельности кода определяйте идентификаторы (с помощью #define) вне функций.
После того, как препроцессор завершит своё выполнение, все идентификаторы (определённые с помощью #define) из этого файла отбрасываются. Это означает, что директивы действительны только от точки определения до конца файла, в котором они определены. Директивы, определённые в одном файле кода, не влияют на директивы, определённые внутри других файлов этого же проекта.
Рассмотрим следующий пример:
#include
#include void doSomething () #ifdef PRINT std :: cout << "Printing!" ; #endif #ifndef PRINT std :: cout << "Not printing!" ; |