Nickolay.info. Обучение. Лекции по Си. Глава 10

10. Директивы препроцессора

 

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

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

Можно выделить следующие основные виды директив:

·  определение макрокоманд;

·  вставка файлов;

·  условная компиляция программы.

Директивы препроцессор Си перечислены в табл. 10.1.

 

Таблица 10.1. Директивы препроцессора Си

#define

#else

#if

#ifndef

#line

#elif

#endif

#ifdef

#include

#undef

При указании любой директивы первым значащим символом в строке должен быть символ "#".

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

 

10.1. Определение макрокоманд

Используя конструкцию

#define идентификатор текст

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

#define х х

не приведет к зацикливанию препроцессора. Это же позволяет делать макроопределения "многоступенчатыми" и зависящими от ранее сделанных подстановок. Обычное назначение директивы #define - введение удобных символических имен для различного рода констант и ключевых слов:

#define  null 0

#define begin {

Существенно то, что макроопределениям можно передавать параметры:

#define max((a),(b)) ((a)>(b)?(a):(b))

Формальные параметры макроопределения должны отличаться друг от друга. Их область действия ограничена определением, в котором они заданы. Список должен быть заключен в круглые скобки. При обращении к директиве с любыми фактическими аргументами они будут подставлены на место формальных параметров a и b. При этом не происходит вызова какой‑либо функции, но выполняется код условной функции, подставленный препроцессором в каждое место вызова макроопределения. Эта подстановка носит чисто текстовый характер. Никаких вычислений или преобразований типа при этом не производится.

В примере с макроопределением max его формальные аргументы для надежности заключены в круглые скобки. Однако это не гарантирует отсутствия побочных эффектов. Так, при макровызове max(i, a[i++]) он преобразуется к виду ((i)>(a[i++])?(i):(a[i++])), что, очевидно, может привести к лишнему вычислению инкремента.

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

#define name(i, j) i##j

при вызове name(a,1) образует идентификатор a1.

Одиночный символ #, помещаемый перед аргументом макроопределения, указывает на то, что аргумент должен быть преобразован в символьную строку, то есть, конструкция вида #формальный_параметр будет заменена на конструкцию "фактический_параметр". Например, сделав "отладочное" макроопределение debug

#define debug(a) printf (#a "=%d\n",a)

мы можем печатать значения переменных в формате имя=значение. Фрагмент программы

int i1=10; debug (i1);

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

int i1=10; printf("i1" " = %d\n", i1);

Наконец, возможно определение вида

#define some

При этом все экземпляры идентификатора some будут удалены из текста программы. Сам идентификатор some считается определенным и дает значение 1 при проверке директивой #if.

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

 

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

Общий вид директивы записывается в одном из двух форматов:

#include "имя_пути"

#include <имя_пути>

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

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

Угловые скобки сообщают препроцессору, что файл ищется в каталоге, указанном в командной строке компиляции, а затем в стандартных каталогах (для их настройки во многих компиляторах служит опция Include directories):

#include <stdio.h>

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

#include "my.h"

 

10.3. Директива условной компиляции

Эта директива позволяет компилятору исключить из обработки какие‑либо части исходного файла посредством проверки условий (константных выражений). Общий синтаксис директивы следующий:

#if ОКВ текст

<#elif ОКВ текст>

<#elif ОКВ текст>

<:>

<#else текст>

#endif

Здесь ОКВ - обозначение ограниченного константного выражения, специфика его будет раскрыта далее. Треугольные скобки, как обычно, обозначают необязательные элементы спецификации.

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

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

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

Если все выражения, следующие за #if, #elif на данном уровне вложенности ложны (равны нулю), выбирается текст, следующий за #else. Если при этом ветвь #else отсутствует, никакой текст не выбирается.

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

#if defined(name1) || defined(name2)

Приведем пример:

#if defined (COLOR)

color();

#elif defined (MONO)

mono();

#else

error();

#endif

Здесь условная директива управляет компиляцией одного из трех вызовов функции. Вызов функции color компилируется, если определена именованная константа COLOR. Если определена константа MONO, компилируется вызов функции mono, если ни одна из двух констант не определена, компилируется вызов функции error.

 

10.4. Директивы условного определения

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

#ifdef идентификатор

#ifndef идентификатор

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

Директива #ifndef противоположна по действию: если идентификатор не был определен директивой #define или его определение отменено директивой #undef, то условие считается истинным. В противном случае условие ложно.

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

 

10.5. Управление нумерацией строк

Директива имеет следующий общий вид:

#line константа <"имя_файла">

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

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

#line 1000 "file.сpp"

Здесь устанавливается имя исходного файла file.сpp и текущий номер строки 1000.

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

 

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

Директива имеет формат

#error текст

Чаще всего ее используют для обнаружения некоторой недопустимой ситуации. По директиве #error препроцессор прерывает компиляцию и выдает сообщение вида Fatal: имя_файла номер_строки Error directive: текст. Здесь имя_файла - имя исходного файла,  номер_строки - текущий номер строки, текст -диагностическое сообщение. Пример:

#if (Boolean!= 0 && Boolean!=1)

#error Boolean имеет значение не 0 и не 1!

#endif

 

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

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

#pragma текст

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

 

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

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

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

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

__DATE__ - дата начала компиляции текущего файла, записанная как символьная строка. Каждое вхождение __DATE__ в заданный файл дает одно и то же значение, независимо от того, как долго уже продолжается компиляция. Дата хранится в формате mmm dd yyyy, где mmm - месяц (Jan, Feb и т. д.), dd - число месяца (от " 1" до "31"), yyyy - четырехзначный год (например, 2008).

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

 

 

Рейтинг@Mail.ru
вверх гостевая; E-mail