Препроцессор для избежания переопределения c. Директивы препроцессора в си. Директивы #if #else #endif

Последнее обновление: 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 #include "main.c" int main(void) { printf("%d", number); // 5 return 0; }

При подключении своих файлов их имя указывается в кавычках. И несмотря на то, что в программе не определена переменная number, она будет браться из подключенного файла main.c. Но опять же отмечу, важно, что в данном случае файл main.c располагается в одной папке с главным файлов программы.

В то же время данный способ прекрасно работает в GCC. Но для разных сред программирования способ подключения файлов может отличаться. Например, в Visual Studio мы получим ошибку. И более правильный подход будет состоять в том, что определить объявление объекта (переменной/константы) или функции в дополнительном заголовочном файле, а определение объекта или функции поместить в стандартный файл с расширением .c .

Например, в нашем в файле main.c уже есть определение переменной number. Теперь в ту же папку добавим новый файл main.h - файл с тем же названием, но другим расширением. И определим в main.h следующий код:

Extern int number;

Ключевое слово extern указывает, что данный объект является внешним. И в этом случае мы могули бы его подключить в файл исходного кода:

#include #include "main.h" // объявление или описание объекта #include "main.c" // определение объекта int main(void) { printf("%d", number); return 0; }

Этот пример также будет работать в GCC, однако как уже было рассмотрено выше, подключение файла main.h для GCC необязательно.

Если разработка ведется в Visual Studio , то определение объекта не надо подключать исходный файл:

#include #include "main.h" // объявление или описание объекта int main(void) { printf("%d", number); return 0; }

Несмотря на то, что здесь явным образом не подключается файл main.c, но при трансляции Visual Studio через заголовочный файл main.h сможет подключить находящийся в той же папке файл main.c.

Препроцессор лучше всего рассматривать как отдельную программу, которая выполняется перед компиляцией. При запуске программы препроцессор просматривает код сверху вниз, файл за файлом, в поиске директив. Директивы — это специальные команды, которые начинаются с символа # и НЕ заканчиваются точкой с запятой. Есть несколько типов директив, которые мы рассмотрим ниже.

Директива #include

Вы уже видели директиву #include в действии. Когда вы #include файл, препроцессор копирует содержимое подключаемого файла в текущий файл сразу после строчки #include. Это очень полезно при использовании определённых данных (например, функций) сразу в нескольких местах.

Директива #include имеет две формы:

#include , которая сообщает препроцессору искать файл в системных путях. Чаще всего вы будете использовать эту форму при подключении из стандартных библиотек C++.

#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 void boo() { #define MY_NAME "Alex" } int main() { std::cout << "My name is: " << MY_NAME; return 0; }

#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 void doSomething() { #ifdef PRINT std::cout << "Printing!"; #endif #ifndef PRINT std::cout << "Not printing!"; #endif }

#include

void doSomething ()

#ifdef PRINT

std :: cout << "Printing!" ;

#endif

#ifndef PRINT

std :: cout << "Not printing!" ;

препроцессор . Назначение препроцессора - обработка исходного текста программы до ее компиляции. Препроцессорная обработка включает несколько стадий, выполняемых последовательно. Конкретная реализация может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись в следующем порядке:
  1. Все системно-зависимые обозначения перекодируются в стандартные коды.
  2. Каждая пара из символов " \ " и "конец строки" вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов.
  3. В тексте распознаются директивы и лексемы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка.
  4. Выполняются директивы препроцессора и производятся макроподстановки.
  5. Эскейп-последовательности в символьных константах и символьных строках заменяются на их эквиваленты.
  6. Смежные символьные строки конкатенируются, то есть соединяются в одну строку.
  7. Каждая препроцессорная лексема преобразуется в текст на языке Си.

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

Стадия обработки директив препроцессора. При ее выполнении возможны следующие действия:

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

Символические константы: #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; }

В результате выполнения нашего примера будем иметь.

Введение

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

Директивы препроцессора обычно используются для того, чтобы облегчить модификацию исходных программ и сделать их более независимыми от особенностей различных реализаций компилятора языка Си, разных компьютеров и операционных сред. Директивы препроцессора позволяют заменить лексемы в тексте программы некоторыми значениями, вставить в исходный файл содержимое другого исходного файла, запретить компиляцию некоторой части исходного файла и т.д. Препроцессор Си распознает следующие директивы:

#define #else #if #ifndef #line
#elif #endif #ifdef #include #undef

Символ # должен быть первым в строке, содержащей директиву в СП MSC версии 4. В СП MSC версии 5 ив СП ТС ему могут предшествовать пробельные символы. Как в СП MSC, так и в СП ТС пробельные символы допускаются между символом # и первой буквой директивы.

Некоторые директивы могут содержать аргументы. Директивы могут быть записаны в любом месте исходного файла, но их действие распространяется только от точки программы, в которой они записаны, до конца исходного файла.

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

В рассматриваемых системах программирования есть возможность получить промежуточный текст программы после работы препроцессора, до начала собственно компиляции. В этом файле уже выполнены макроподстановки, а все строки, содержащие директивы #define и #undef, заменены на пустые строки. На место строк #include подставлено содержимое соответствующих включаемых файлов. Выполнена обработка директив условной компиляции #if, #elif, #else, #ifdef, #ifndef, #endif, а строки, содержащие их, заменены пустыми строками. Пустыми строками заменены и исключенные в процессе условной компиляции фрагменты исходного текста. Кроме того, в этом файле есть строки следующего вида:

#["имя файла"]

которые соответствуют точкам изменения номера текущей строки и/или номера файла по директивам #line или #include.

Именованные константы и макроопределения

Директива #define обычно используется для замены часто используемых в программе констант, ключевых слов, операторов и выражений осмысленными идентификаторами. Идентификаторы, которые заменяют числовые или текстовые константы либо произвольную последовательность символов, называются именованными константами. Идентификаторы, которые представляют некоторую последовательность действий, заданную операторами или выражениями языка Си, называются макроопределениями. Макроопределения могут иметь аргументы. Обращение к макроопределению в программе называется макровызовом.

В языке Си принято записывать идентификаторы именованных констант и макроопределений символами верхнего регистра, чтобы отличать их от имен переменных и функций. Это, однако, не является требованием языка Си.

Директива #undef отменяет текущее определение именованной константы. Только когда определение отменено, именованной константе может быть сопоставлено другое значение. Однако многократное повторение определения с одним и тем же значением не считается ошибкой.

Макроопределение напоминает по синтаксису определение функции. Однако замена вызова функции макровызовом может повысить скорость выполнения программы, поскольку для макроопределения не требуется генерировать вызывающую последовательность, которая занимает относительно большое время (засылка аргументов в стек, передача управления и т.п.). С другой стороны, многократное употребление макроопределения в программе может потребовать значительно большей памяти, чем вызовы функции (код для функции генерируется один раз, а для макроопределения - столько раз, сколько имеется макровызовов в программе).

Имеется возможность задавать определения именованных констант не только в исходном тексте, но и в командной строке компиляции.

Имеется ряд предопределенных идентификаторов, которые нельзя использовать в директивах #define и #undef в качестве идентификаторов. Они рассмотрены в разделе 7.9 "Псевдопеременные".

Директива #define

Синтаксис:

Директива #define заменяет все вхождения в исходном файле на, следующий в директиве за. Этот процесс называется макроподстановкой, заменяется лишь в том случае, если он представляет собой отдельную лексему. Например, если является частью строки или более длинного идентификатора, он не заменяется. Если за следует, то директива определяет макроопределение с аргументами.

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

может быть опущен. В этом случае все экземпляры будут удалены из исходного текста программы. Тем не менее, сам рассматривается как определенный и при проверке директивой #if дает значение 1 (смотри раздел 7.4.1).

Если он задан, содержит один или более идентификаторов, разделенных запятыми. Идентификаторы в списке должны отличаться друг от друга. Их область действия ограничена макроопределением, в котором они заданы. Список должен быть заключен в круглые скобки. Имена формальных параметров в отмечают позиции, в которые должны быть подставлены фактические аргументы макровызова. Каждое имя формального параметра может появиться в произвольное число раз.

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

Примечание. Не следует путать подстановку аргументов в макроопределение с передачей аргументов функции. Подстановка в препроцессоре носит чисто текстовый характер. Никаких вычислений или преобразований типа при этом не производится.

Выше уже говорилось, что макроопределение может содержать более одного вхождения данного формального параметра. Если формальный параметр представлен выражением с побочным эффектом, то это выражение будет вычисляться более одного раза, а вместе с ним каждый раз будет возникать и побочный эффект. Результат выполнения в этом случае может быть ошибочным.

Внутрь в директиве #define могут быть вложены имена других макроопределений или констант. Их расширение производится лишь при расширении этого, а не при его определении директивой #define. Это надо учитывать, в частности, при взаимодействии вложенных именованных констант и макроопределений с директивой #undef: к моменту расширения содержащего их текста они могут уже оказаться отменены директивой #undef.

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

не приведет к зацикливанию препроцессора.

/* пример 1 */

#define WIDTH 80

#define LENGTH (WIDTH + 10)

/* пример 2 */

#define FILEMESSAGE "Попытка создать файл\

не удалась из-за нехватки дискового пространства"

/* пример 3 */

#define REG1 register

#define REG2 register

/* пример 4 */

#define MAX(x, y)((x)>(у)) ? (x) : (у)

/* пример 5 */

#define MULT(a, b) ((a)*(b))

В первом примере идентификатор WIDTH определяется как целая константа со значением 80, а идентификатор LENGTH - как текст (WIDTH + 10). Каждое вхождение идентификатора LENGTH в исходный файл будет заменено на текст (WIDTH + 10), который после расширения идентификатора WIDTH превратится в выражение (80 + 10). Скобки, окружающие текст (WIDTH + 10), позволяют избежать ошибок в операторах, подобных следующему:

var = LENGTH * 20;

После обработки препроцессором оператор примет вид:

var = (80 + 10)* 20;

Значение, которое присваивается var, равно 1800. В отсутствие скобок в макроопределении оператор имел бы следующий вид:

var = 80 + 10*20;

Значение var равнялось бы 280, поскольку операция умножения имеет более высокий приоритет, чем операция сложения.

Во втором примере определяется идентификатор FILEMESSAGE. Его определение продолжается на вторую строку путем использования символа обратный слэш непосредственно перед нажатием клавиши ENTER.

В третьем примере определены три идентификатора, REG1, REG2, REG3. Идентификаторы REG1 и REG2 определены как ключевые слова register. Определение REG3 опущено и, таким образом, любое вхождение REG3 будет удалено из исходного файла. В разделе 7.4.1 приведен пример, показывающий, как эти директивы могут быть использованы для задания класса памяти register наиболее важным переменным программы.

В четвертом примере определяется макроопределение МАХ. Каждое вхождение идентификатора МАХ в исходном файле заменяется на выражение ((x)>(у))?(x):(у), в котором вместо формальных параметров х и у подставлены фактические. Например, макровызов

заменится на выражение

((1)>(2))?(1):(2)

а макровызов

заменится на выражение

((i)>(s(i]))?(i):(s(i])

Обратите внимание на то, что в этом макроопределении аргументы с побочными эффектами могут привести к неверным результатам. Например, макровызов

заменится на выражение

((i)>(s))?(i):(s)

Операнды операции > могут быть вычислены в любом порядке, а значение переменной i зависит от порядка вычисления. Поэтому результат выражения непредсказуем. Кроме того, возможна ситуация, когда переменная i будет инкрементирована дважды, что, вероятно, не требуется.

В пятом примере определяется макроопределение MULT. Макровызов MULT(3,5) в тексте программы заменяется на (3)*(5). Круглые скобки, в которые заключаются фактические аргументы, необходимы в тех случаях, когда аргументы макроопределения являются сложными выражениями. Например, макровызов

заменится на (3+4)*(5+6), что равняется 76. В отсутствие скобок результат подстановки 3+4*5+6 был бы равен 29.

Склейка лексем и преобразование аргументов макроопределений

СП ТС и версия 5.0 СП MSC реализуют две специальные препроцессорные операции: ## и #.

В директиве #define две лексемы могут быть "склеены" вместе. Для этого их нужно разделить знаками ## (слева и справа от ## допустимы пробельные символы). Препроцессор объединяет такие лексемы в одну; например, макроопределение

#define VAR (i, j) i##j

при макровызове VAR(х,6) образует идентификатор х6. Некоторые компиляторы позволяют в аналогичных целях употребить запись х/**/6, но этот метод менее переносим.

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

Пример: макроопределение TRACE позволяет печатать с помощью стандартной функции printf значения переменных типа int в формате = .

#define TRACE(flag) printf (#flag " = %d\n", flag)

Следующий фрагмент текста программы:

TRACE (highval);

примет после обработки препроцессором вид:

printf("highval" " = %d\n", highval);

Следующие друг за другом символьные строки рассматриваются компилятором языка Си в СП MSC версии 5 и в СП ТС как одна строка, поэтому полученная запись эквивалентна следующей:

printf("highval = %d\n", highval);

При макровызове сначала выполняется макроподстановка всех аргументов макровызова, а затем их подстановка в тело макроопределения. Поэтому следующая программа напечатает строку "отклонение от стандарта":

#define АВ "стандарт"

#define А "отклонение"

#define В "от стандарта"

#define CONCAT(P,Q) Р##Q

printf(CONCAT(A,В) "\n");

Директива #undef

Синтаксис:

Директива #undef отменяет действие текущего определения #define для. Чтобы отменить макроопределение посредством директивы #undef, достаточно задать его. Задание списка параметров не требуется.

Не является ошибкой применение директивы #undef к идентификатору, который ранее не был определен (или действие его определения уже отменено). Это может использоваться для гарантии того, что идентификатор не определен.

Директива #undef обычно используется в паре с директивой #define, чтобы создать область исходной программы, в которой некоторый идентификатор определен.

#define WIDTH 80

#define ADD(X, Y) (X)+(Y)

В этом примере директива #undef отменяет определение именованной константы WIDTH и макроопределения ADD. Обратите внимание на то, что для отмены макроопределения задается только его идентификатор.

Включение файлов

Синтаксис:

#include "имя пути"

Директива #include включает содержимое исходного файла, которого задано, в текущий компилируемый исходный файл. Например, общие для нескольких исходных файлов определения именованных констант и макроопределения могут быть собраны в одном включаемом файле и включены директивой #include во все исходные файлы. Включаемые файлы используются также для хранения объявлений внешних переменных и абстрактных типов данных, разделяемых несколькими исходными файлами.

Препроцессор обрабатывает включаемый файл таким же образом, как если бы этот файл целиком входил в состав исходного файла в точке, где записана директива #include. Включаемый текст также может содержать директивы препроцессора. Препроцессор выполняет обработку включаемого файла, а затем возвращается к обработке первоначального исходного файла.

Имя пути представляет собой имя файла, которому может предшествовать имя устройства и спецификация директории. Синтаксис имени пути определяется соглашениями операционной системы.

Препроцессор использует понятие стандартных директорий для поиска включаемых файлов. Стандартные директории задаются командой PATH операционной системы.

Препроцессор ведет поиск до тех пор, пока не обнаружит файл с заданным именем.

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

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

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

Затем препроцессор продолжает поиск в директориях, указанных в командной строке компиляции, и, наконец, ищет в стандартных директориях.

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

#include /* пример 1 */

#include "defs.h" /* пример 2 */

В первом примере в исходный файл включается файл с именем stdio.h. Угловые скобки сообщают препроцессору, что поиск файла нужно осуществлять в директории, указанной в командной строке компиляции, а затем в стандартных директориях.

Во втором примере в исходный файл включается файл с именем defs.h. Двойные кавычки означают, что при поиске файла сначала должна быть просмотрена директория, содержащая текущий исходный файл.

В СП ТС имеется возможность задавать имя пути в директиве #include с помощью именованной константы. Если за словом include следует идентификатор, препроцессор проверяет, не именует ли он константу или макроопределение. Если же за словом include следует строка, заключенная в кавычки или в угловые скобки, СП ТС не будет искать в ней имя константы.

#define myinclude "c:\tc\include\mystuff.h"

#include myinclude

#include "myinclude.h"

Первая директива #include заставит препроцессор просматривать директорию C:\TC\INCLUDE\MYSTUFF.H, а вторая заставит искать файл MYINCLUDE.H в текущей директории.

Объединение символьных строк и склейку лексем в именованной константе, которая используется в директиве #include, использовать нельзя. Результат расширения константы должен сразу читаться как корректная директива #include.

Условная компиляция

В этом разделе описываются директивы, которые управляют условной компиляцией. Эти директивы позволяют исключить из процесса компиляции какие-либо части исходного файла посредством проверки условий (константных выражений).

Директивы #if, #elif, #else, #endif

Синтаксис:

Директива #if совместно с директивами #elif, #else и #endif управляет компиляцией частей исходного файла. Каждой директиве #if в том же исходном файле должна соответствовать завершающая ее директива #endif. Между директивами #if и #endif допускается произвольное количество директив #elif (в том числе ни одной) и не более одной директивы #else. Если директива #else присутствует, то между ней и директивой #endif на данном уровне вложенности не должно быть других директив #elif.

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

Участок текста, не выбранный препроцессором, игнорируется на стадии препроцессорной обработки и не компилируется.

Препроцессор выбирает участок текста для обработки на основе вычисления, следующего за каждой директивой #if или #elif. Выбирается, следующий за со значением истина (не нуль), вплоть до ближайшей директивы #elif, #else, или #endif, ассоциированной с данной директивой #if.

Если ни одно ограниченное константное выражение не истинно, то препроцессор выбирает, следующий за директивой #else. Если же директива #else отсутствует, то никакой текст не выбирается.

Ограниченное константное выражение описано в разделе 4.2.9 "Константные выражения". Такое выражение не может содержать операцию sizeof (в СП ТС - может), операцию приведения типа, константы перечисления и плавающие константы, но может содержать препроцессорную операцию defined(). Эта операция дает истинное (не равное нулю) значение, если заданный в данный момент определен; в противном случае выражение ложно (равно нулю). Следует помнить, что идентификатор, определенный без значения, тем не менее рассматривается как определенный. Операция defined может использоваться в сложном выражении в директиве #if неоднократно:

#if defined(mysym) || defined(yoursym)

СП TC (в отличие от СП MSC) позволяет использовать операцию sizeof в ограниченном константном выражении для препроцессора. В следующем примере в зависимости от размера указателя определяется одна из констант - либо SDATA, либо LDATA:

#if (sizeof(void *) == 2)

Директивы #if могут быть вложенными. При этом каждая из директив #else, #elif, #endif ассоциируется с ближайшей предшествующей директивой #if.

/* пример 1 */

#if defined(CREDIT)

#elif defined (DEBIT)

/* пример 2 */

#define SIGNAL 1

#if STACKUSE == 1

#derine STACK 200

#define STACK 100

#define SIGNAL 0

#if STACKUSE == 1

#define STACK 100

#define STACK 50

/* пример 3 */

#elif DLEVEL == 1

#define STACK 100

#elif DLEVEL > 5

display(debugptr);

#define STACK 200

/* пример 4 */

#define REG 1 register

#define REG2 register

#if defined (M_86)

#ifdefined(M_68000)

#define REG4 register

В первом примере директивы #if, #elif, #else, #endif управляют компиляцией одного из трех вызовов функции. Вызов функции credit компилируется, если определена именованная константа CREDIT. Если определена именованная константа DEBIT, то компилируется вызов функции debit. Если ни одна из.именованных констант не определена, то компилируется вызов функции printerror. Следует учитывать, что CREDIT и credit являются различными идентификаторами в языке Си.

В следующих двух примерах предполагается, что константа DLEVEL предварительно определена директивой #define.

Во втором примере показаны два вложенных набора директив #if, #else, #endif. Первый набор директив обрабатывается, если значение DLEVEL больше 5. В противном случае обрабатывается второй набор.

В третьем примере директивы уловной компиляции используют для выбора текста значение константы DLEVEL. Константа STACK определяется со значением 0, 100 или 200, в зависимости от значения DLEVEL. Если DLEVEL больше 5, то компилируется вызов функции display, а константа STACK не определяется.

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

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

В примере показано, каким образом предоставить приоритет регистровой памяти наиболее важным переменным. Именованные константы REG1 и REG2 определяются как ключевые слова register. Они предназначены для объявления двух наиболее важных локальных переменных функции. Например, в следующем фрагменте программы такими переменными являются b и c.

func(REG3 int а)

Если определена константа М_86, препроцессор удаляет идентификаторы REG3 и REG4 из файла путем замены их на пустой текст. Регистровую память в этом случае получат только переменные b и с. Если определен идентификатор М_68000, то все четыре переменные объявляются с классом памяти register.

Если не определена ни одна из констант - ни М_86, ни М_68000, - то регистровую память получат переменные а, b и с.

Директивы #ifdef и #ifndef

Синтаксис:

Аналогично директиве #if, за директивами #ifdef и #ifndef может следовать набор директив #elif и директива #else. Набор должен быть завершен директивой #endif.

Использование директив #ifdef и #ifndef эквивалентно применению директивы #if, использующей выражение с операцией defined(). Эти директивы поддерживаются исключительно для совместимости с предыдущими версиями компиляторов языка Си. Для новых программ рекомендуется использовать директиву #if с операцией defined().

Когда препроцессор обрабатывает директиву #ifdef, он проверяет, определен ли в данный момент директивой #define. Если да, условие считается истинным, если нет - ложным.

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

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

Текущий номер строки и имя исходного файла доступны в программе через псевдопеременные с именами __LINE__ и __FILE__. Эти псевдопеременные могут быть использованы для выдачи во время выполнения сообщений о точном местоположении ошибки.

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

/* пример 1 */

#line 151 "copy.с"

/* пример 2 */

#define ASSERT(cond) if (!cond)\

{printf ("ошибка в строке %d файла %s\n", \

LINE__, __FILE__);} else;

В первом примере устанавливается имя исходного файла сору.с и текущий номер строки 151.

Во втором примере в макроопределении ASSERT используются псевдопеременные __LINE__ и __FILE__ для печати сообщения об ошибке, содержащего координаты исходного файла, если некоторое условие, заданное макроаргументом cond, ложно.

Директива обработки ошибок

В СП ТС реализована директива #error. Ее формат:

Обычно эту директиву записывают среди директив условной компиляции для обнаружения некоторой недопустимой ситуации. По директиве #error препроцессор прерывает компиляцию и выдает следующее сообщение:

Fatal: Error directive:

Fatal - признак фатальной ошибки; - имя исходного файла; - текущий номер строки; Error directive - сообщение об ошибке в директиве; - собственно текст диагностического сообщения.

Указания компилятору языка Си

Синтаксис:

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

Набор прагм для каждого компилятора языка Си различен. Для получения подробной информации о прагмах смотрите системную документацию по используемому вами компилятору.

Псевдопеременные

Псевдопеременные представляют собой зарезервированные именованные константы, которые можно использовать в любом исходном файле. Каждый из них начинается и оканчивается двумя символами подчеркивания (__).

Номер текущей обрабатываемой строки исходного файла-десятичная константа. Первая строка исходного файла имеет номер 1.

Имя компилируемого исходного файла - символьная строка. Значение данной псевдопеременной изменяется каждый раз, когда компилятор обрабатывает директиву #include или директиву #line, а также по завершении включаемого файла.

Следующие две псевдопеременные поддерживаются только СП ТС.

Дата начала компиляции текущего исходного файла - символьная строка. Каждое вхождение __DATE__ в заданный файл дает одно и то же значение, независимо от того, как долго уже продолжается обработка. Дата имеет формат mmm dd УУУУ, где mmm - месяц (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec), dd - число текущего месяца (1…31; в 1-й позиции dd ставится пробел, если число меньше 10), уууу - год (например, 1990).

Время начала компиляции текущего исходного файла - символьная строка. Каждое вхождение __TIME__ в заданный файл дает одно и то же значение, независимо от того, как долго уже продолжается обработка. Время имеет формат hh:mm:ss, где hh - час (00…23), mm - минуты (00…59), ss - секунды (00…59).

Препроцессор - это специальная программа, являющаяся частью компилятора языка Си. Она предназначена для предварительной обработки текста программы. Препроцессор позволяет включать в текст программы файлы и вводить макроопределения.
Работа препроцессора осуществляется с помощью специальных директив (указаний). Они отмечаются знаком решетка #. По окончании строк, обозначающих директивы в языке Си, точку с запятой можно не ставить.

Основные директивы препроцессора

#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;
}



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: