Директивы препроцессора языка си
Препроцессор языка Си – это программа, выполняющая обработку входных данных для другой программы. Препроцессор языка Си просматривает программу до компилятора, заменяет аббревиатуры в тексте программы на соответствующие директивы, отыскивает и подключает необходимые файлы, может влиять на условия компиляции. Директивы препроцессора не являются в действительности частью языка Си. Препроцессор включает в себя следующие директивы:
Определение |
Назначение |
Определение макроса |
|
Отмена определения макроса |
|
Включение объекта-заголовка |
|
Компиляция, если выражение истинно |
|
Компиляция, если макрос определен |
|
Компиляция, если макрос не определен |
|
Компиляция, если выражение в ifложно |
|
Составная директива else/if |
|
Окончание группы компиляции по условию |
|
Замена новым именем строки или имени исходного файла |
|
Формирование ошибок трансляции |
|
Действие определяется реализацией |
|
Null- директива |
Директива # define
Директива # define вводит макроопределение или макрос. Общая форма директивы следующая:
# define ИМЯ_МАКРОСА последовательность_символов
Последовательность символов называют еще строкой замещения. Когда препроцессор находит в исходном файле имя_макроса (просто макрос), он заменяет его на последовательность_символов.
Можно отменить определение макроса директивой # undef:
# undef имя_макроса
Данная строка удаляет любую ранее введенную строку замещения. Определение макроса теряется и имя_макроса становится неопределенным.
К примеру, можно определить МАХ как величину 100:
Это значение будет подставляться каждый раз вместо макроса МАХ в исходном файле, Можно также использовать макрос вместо строковой константы:
#defineNAME“TurboC++”
Если последовательность символов в директиве не помещается в одной строке, то можно поставить в конце строки \ и продолжить последовательность на другой строке. Среди программистов принято соглашение, что для имен макросов используются прописные буквы, так как их легко находить в программе. Также все директивы #defineлучше помещать в начало программы.
Директива # define имеет еще одну важную особенность: макрос может иметь аргументы. Каждый раз, когда происходит замена, аргументы заменяются на те, которые встречаются в программе.
Пример : #define MIN(a, b) ((9a)<(b)) ? (a) : (b)
printf(“Минимум из x и y “ % d, MIN(x ,y));
printf(“Минимум из a и b “ % d, MIN(n ,m));
Когда программа будет компилироваться, в выражение, определенное MIN(a,b) будут подставлены соответственноxиyилиmиn. Аргументыaиbзаключены в круглые скобки, так как вместо них может подставляться некоторое выражение, а не просто идентификатор.
Например, printf(“Минимум “ %d,MIN(x*x,x));
Директива # error
Имеет вид: # error сообщение_об_ошибке
Эта команда прекращает компиляцию программы и выдает сообщение об ошибке.
Директивы условной компиляции
К данным директивам относятся: # if , # else , # elif , # endif .
Данные директивы производят выборочную компиляцию программы. Если выражение, следующее за #if, истинно, то коды, заключенные между #ifи #endif, будут компилироваться. В противном случае они при компиляции будут пропущены. Выражение, следующее за #if, проверяется во время компиляции, поэтому оно может содержать только константы и макросы, которые прежде определены. Переменные здесь не могут использоваться.
Директива # else используется так же, как иelseв языке Си.
Пример: Использование условной компиляции.
# include
# define MAX 100
printf(“ MAX равно %d \n”, MAX);
Директива # elif используется для организации вложенной условной компиляции. Форма использования ее следующая:
#if<выражение>
последовательность операторов
#elif<выражение 1>
последовательность операторов
#elif<выражение 2>
последовательность операторов
…………………………………..
Другой метод условной компиляции состоит в использовании директив # ifdef и# ifndef . Основная форма использования этих директив следующая:
# ifdef ИМЯ_МАКРОСА
# endif
и соответственно
# ifndef ИМЯ_МАКРОСА
последовательность операторов
# endif
Если макрос определен, то при использовании # ifdefкомпилируется соответствующая последовательность до операторов #endif. Если же макрос не определен или был отменен директивой #undef, то соответствующая последовательность операторов игнорируется компилятором. Директива #ifndefдействует противоположным образом.
Последнее обновление: 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
- вставляет текст из указанного файла
#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;
}
Заголовочные файлы включаются в текст программы с помощью директивы препроцессора
#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 эта тема рассматривается более подробно.)
препроцессор . Назначение препроцессора - обработка исходного текста программы до ее компиляции. Препроцессорная обработка включает несколько стадий, выполняемых последовательно. Конкретная реализация может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись в следующем порядке:- Все системно-зависимые обозначения перекодируются в стандартные коды.
- Каждая пара из символов " \ " и "конец строки" вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов.
- В тексте распознаются директивы и лексемы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка.
- Выполняются директивы препроцессора и производятся макроподстановки.
- Эскейп-последовательности в символьных константах и символьных строках заменяются на их эквиваленты.
- Смежные символьные строки конкатенируются, то есть соединяются в одну строку.
- Каждая препроцессорная лексема преобразуется в текст на языке Си.
Поясним, что понимается под препроцессорными лексемами или лексемами препроцессора. К ним относятся символьные константы , имена включаемых файлов, идентификаторы, знаки операций, препроцессорные числа, знаки препинания, строковые константы и любые символы, отличные от пробела.
Стадия обработки директив препроцессора. При ее выполнении возможны следующие действия:
- замена идентификаторов заранее подготовленными последовательностями символов;
- включение в программу текстов из указанных файлов;
- исключение из программы отдельных частей ее текста, условная компиляция;
- макроподстановка, то есть замена обозначения параметризованным текстом, формируемым препроцессором с учетом конкретных аргументов.
Символические константы: #define
Если в качестве первого символа в строке программы используется символ # , то эта строка является командной строкой препроцессора (макропроцессора). Командная строка препроцессора заканчивается символом перевода на новую строку. Если непосредственно перед концом строки поставить символ обратной косой черты " \ ", то командная строка будет продолжена на следующую строку программы.
Директива #define , подобно всем директивам препроцессора, начинается c символа # в самой левой позиции. Она может появиться в любом месте исходного файла, а даваемое определение имеет силу от места появления до конца файла. Мы активно используем эту директиву для определения символических констант в наших примерах программ, однако она имеет более широкое применение, что мы покажем дальше.
Замена идентификаторов
#define идентификатор строка
Заменяет каждое вхождение идентификатора ABC в тексте программы на 100 :
#undef идентификатор
Отменяет предыдущее определение для идентификатора ABC .
/* Простые примеры директивы препроцессора */ #define TWO 2 /* можно использовать комментарии*/ #define MSG "Текст 1.\ Продолжение текста 1" /* обратная косая черта продолжает определение на следующую строку */ #define FOUR TWO*TWO #define PX printf("X равен %d.\n", x) #define FMT "X равен %d.\n" int main() { int x = TWO; PX; x = FOUR; printf(FMT,x); printf("%s\n",MSG); printf("TWO: MSG\n"); return TWO; }
В результате выполнения нашего примера будем иметь.