Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Программирование на языке Си

..pdf
Скачиваний:
15
Добавлен:
12.11.2023
Размер:
17.16 Mб
Скачать

132

Программирование на языке Си

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

Итак, на входе препроцессора - текст с препроцессорными директивами, на выходе препроцессора - текст без препроцессорных директив (см. рис. 1.1).

3.1. С тадии и ком анды препроцессорной

обработки

В интегрированную среду подготовки программ на Си или в компилятор языка как обязательный компонент входит препро­ цессор. Назначение препроцессора - обработка исходного тек­ ста программы до ее компиляции (см. рис. 2.1).

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

все системно-зависимые обозначения (например, системно­ зависимый индикатор конца строки) перекодируются в стандартные коды;

каждая пара из символов 'V и "конец строки" вместе с про­ белами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в кото­ рой находилась эта пара символов;

в тексте (точнее, в тексте каждой отдельной строки) распо­ знаются директивы и лексемы препроцессора, а каждый комментарий заменяется одним символом пустого проме­ жутка;

выполняются директивы препроцессора и производятся макроподстановки;

эскейп-последовательности в символьных константах и символьных строках, например '\п' или '\xF2', заменяются на их эквиваленты (на соответствующие числовые коды);

Глава 3. Препроцессорные средства

133

смежные символьные строки (строковые константы) кон­ катенируются, т.е. соединяются в одну строку;

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

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

Знакомство с перечисленными стадиями препроцессорной обработки объясняет, как реализуются некоторые правила син­ таксиса языка. Например, становится понятным смысл утвер­ ждений: "каждая символьная строка может быть перенесена в файле на следующую строку, если использовать символ 'V", или "две символьные строки, записанные рядом, воспринимаются как одна строка". Отметим, что после "склеивания" строк в со­ ответствии с приведенными правилами каждая полученная строка обрабатывается препроцессором отдельно.

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

замена идентификаторов (обозначений) заранее подготов­ ленными последовательностями символов;

включение в программу текстов из указанных файлов;

исключение из программы отдельных частей ее текста (ус­ ловная компиляция);

макроподстановка, т.е. замена обозначения параметризо­ ванным текстом, формируемым препроцессором с учетом конкретных параметров (аргументов).

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

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

134

 

 

Программирование на языке Си

#имя директивы лексемы

препроцессора

Перед

символом

и после него в директиве разрешены

пробелы.

Пробелы

также

разрешены перед лексема-

ми препроцессора, между ними и после них. Окончанием препроцессорной директивы служит конец текстовой строки (при наличии символа 'V, обозначающего перенос строки, окончани­ ем препроцессорной директивы будет признак конца следую­ щей строки текста).

Определены следующие препроцессорные директивы:

#define

- определение

макроса

или

препроцессорного

 

 

идентификатора;

 

 

 

#include

-

включение текста из файла;

 

 

#undef

- отмена определения макроса

или

идентифика­

 

 

тора (препроцессорного);

 

 

 

 

проверка условия-выражения;

 

 

#ifdef

 

проверка определенности идентификатора;

#ifndef

-

проверка неопределенности идентификатора;

#else

-

начало альтернативной ветви для #if;

#endif

-

окончание условной директивы #if;

 

#elif

 

составная директива #else/#if;

 

 

#Iine

-

смена номера следу ющей ниже строки;

#error

- формирование

текста

сообщения

об ошибке

 

 

трансляции;

 

 

 

 

#pragma

-

действия, предусмотренные реализацией;

#пустая директива.

Кроме препроцессорных директив имеются три препроцес­ сорные операции, которые будут подробно рассмотрены вместе с командой ^define:

defined - проверка истинности операнда;

##- конкатенация препроцессорных лексем;

#

- преобразование операнда в строку символов.

Глава 3. Препроцессорные средства

135

Директива #define имеет несколько модификаций. Они пре­ дусматривают определение макросов и препроцессорных иден­ тификаторов, каждому из которых ставится в соответствие некоторая символьная последовательность. В последующем тексте программы препроцессорные идентификаторы заменя­ ются на заранее запланированные последовательности симво­ лов. Примеры определения констант с помощью ^define при­ ведены в главе I.

Директива ^include позволяет включать в текст программы

текст из указанного файла.

/

Директива #undef отменяет действие директивы ttdefine, которая определила до этого имя препроцессорного иденти­ фикатора.

Директива #if и ее модификации #ifdef, #ifndef совместно с директивами #else, #endif, #elif позволяют организовать услов­ ную обработку текста программы. При использовании этих средств компилируется не весь текст, а только те его части, ко­ торые выделены с помощью перечисленных директив.

'Директива #line позволяет управлять нумерацией строк в файле с программой. Имя файла и желаемый начальный номер строки указываются непосредственно в директиве #line (под­ робнее см. §3.6).

Директива #еггог позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок.

Директива #pragma вызывает действия, зависящие от реали­ зации, т.е. запланированные авторами компилятора.

Директива # ничего не вызывает, так как является пустой ди­ рективой, т.е. не дает никакого эффекта и всегда игнорируется.

Рассмотрим возможности перечисленных директив и пре­ процессорных операций при решении типичных задач, пору­ чаемых препроцессору. Одновременно на примерах поясним, что понимается под препроцессорными лексемами в обобщен­ ном формате препроцессорных директив.

136

Программирование на языке Си

3.2. Зам ены в тексте

Директива #define. Как уже иллюстрировалось на примере именованных констант (§1.3 и 2.1'), для замены выбранного про­ граммистом идентификатора заранее подготовленной последо­ вательностью символов используется директива (обратите внимание на пробелы):

#define идентификатор строка_замещения

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

Исходный текст

Результат препроцессорной обработки

#define begin {

 

#define end }'<

void main()

void main()

begin

{

операторы

операторы

end

}

В данном случае программист решил использовать в качест­ ве операторных скобок идентификаторы begin, end. До компи­ ляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствую­ щие указания программист дал препроцессору с помощью ди­ ректив #define.

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

Главд 3. Препроцессорные средства

137

#define, то выполняется цепочка последовательных подстано­ вок. В качестве примера рассмотрим, как можно определить диапазон (RANGE) возможных значений любой целой перемен­ ной типа int в следующей программе:

#include <limits.h>

#define RANGE ((INT_MAX) - (INT_MIN)+1)

/♦RANGE - диапазон значений для int */

int RANGE T = RANGE/8;

Препроцессор последовательно, строку за строкой, просмат­ ривает текст и, обнаружив директиву #include <limits.h>, встав­ ляет текст из файла limits.h. Там определены константы INT_MAX (предельное максимальное значение целых вели­ чин), INT_MIN (предельное минимальное значение целых ве­ личин). Тем самым программа принимает, например, такой вид:

#define INT_MAX 32767 #define INT_MIN -32768

#define RANGE

((INT_MAX)- (INT_MIN)+1).

/♦RANGE - диапазон значений для int*/

int RANGE_T = RANGE/8;

Обратите внимание, что директива ^include исчезла из про­ граммы, но ее заменил соответствующий текст.

Обнаружив в тексте (добытом из файла limits.h) директивы ^define..., препроцессор выполняет соответствующие подста­ новки, и программа принимает вид:

#define RANGE ((32767)-(-32768)+1) /♦RANGE - диапазон Значений для int*/

int RANGE Т = RANGE/8;

1 3 8

Программирование на языке Си

Подстановки изменили строку замещения препроцессорного идентификатора RANGE в директиве ^define, размещенной ни­ же, чем текст, включенный из файла limits.h. "Продвигаясь" по тексту программы, препроцессор встречает препроцессорный идентификатор RANGE и выполняет подстановку. Текст про­ граммы приобретает следующий вид:

/♦RANGE - диапазон значений для int*/

int RANGE_T = ((32767)-(-32768)+1)/8 ;

Теперь все директивы #define удалены из текста. Получен текст, пригодный для компиляции, т.е. создана "единица транс­ ляции". Подстановка строки замещения вместо идентификатора RANGE выполнена в выражении RANGE/8, однако внутри комментария идентификатор RANGE остался без изменений и не изменился идентификатор RANGE_T.

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

#def±ne п 24

char с = '\п'; /* Символьная константа*/

/* \п - эскейп-последовательность:*/

. . . "\п Строковая константа". . .

с='п'>,\п'?'п':'\п'; int k=n;

В ходе препроцессорной обработки этого текста замена п на 24 будет выполнена только один раз в последнем определении, которое примет вид:

int k=24;

Глава 3. Препроцессорные средства

139

Все остальные вхождения символа п в текст программы пре­ процессор просто "не заметит".

Вернемся к формату директивы #define.

Если строка замещения оказывается слишком длинной, то, как уже говорилось, ее можно продолжить в следующей строке текста. Для этого в конце продолжаемой строки помещается символ 'V. В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки бу­ дет удален из текста программы. Пример:

#def±ne STRING "\n Game Over! - \ Игра закончена!"

printf(STRING);

На экран будет выведено:

Game Over! - Игра закончена!

С помощью команды #define удобно выполнять настройку программы. Например, если в программе требуется работать с массивами, то их размеры можно явно определять на этапе пре­ процессорной обработки:

Исходный текст

Результат препроцессорной обработки

#define К

40

 

void main( )

void ma±n(

)

 

int М[К][К];

 

{

 

int М[40][40];

float А[2*К+1],

 

float А[2*40+1],

float В[К+3][К-3] ;

float В[40+3][40-3];

При таком описании очень легко изменять предельные раз­ меры сразу всех массивов, изменив только одну константу (строку замещения) в директиве #define.

Предусмотренные директивой #define препроцессорные. за­ мены не выполняются внутри строк, символьных констант и комментариев, т.е. не распространяются на тексты, ограничен­ ные кавычками ("), апострофами (') и разделителями (/*, */). В

140

Программирование на языке Си

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

Если в программе нужно часто печатать или выводить на эк­ ран дисплея значение какой-либо переменной и, кроме того, снабжать эту печать одним и тем же пояснительным текстом, то удобно ввести сокращенное обозначение оператора печати, на­ пример:

#define РК printf("\n Номер элемента=%с1.", N) ;

После этой директивы использование в программе оператора РК; будет эквивалентно (по результату) оператору из строки замещения. Например, последовательность операторов

int N = 4; РК;

приведет к выводу такого текста:

Номер элемента=4.

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

#def±ne К 50

#def±ne РЕ printf ("\п Число элементов K=%d",K);

РЕ;

выведет на экран такой текст:

Число элементов К=50

Обратите внимание, что идентификатор К внутри строки за­ мещения, обрамленной кавычками ("), не заменен на 50.

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

Глава 3. Препроцессорные средства

141

#def±ne М 16

/* Идентификатор М определен как 16 */ #define М 'С

/* М определен как символьная константа 'С '

* /

#define М "С"

/* М определен как символьная строка */ /* с двумя элементами: 'С и '\0' */

Однако при таких сменах значений препроцессорного иден­ тификатора компилятор Borland C++ выдает предупреждающее сообщение на каждую следующую директиву #define:

Warning ..: Redefinition of 'M' is not identical

Замены в тексте можно отменять с помощью команды

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

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

#define М 16 #undef М #define М 'С #undef М #define М "С"

Директиву #undef удобно использовать при разработке больших программ, когда они собираются из отдельных "кусков текста", написанных в разное время или разными программи­ стами. В этом случае могут встретиться одинаковые обозначе­ ния разных объектов. Чтобы не изменять исходных файлов, включаемый текст можно "обрамлять" подходящими директи­ вами #define, #undef и тем самым устранять возможные ошиб­ ки. Приведем пример:

А = 10; / Основной текст */

#define А X

Соседние файлы в папке книги