книги / Программирование на языке Си
..pdf132 |
Программирование на языке Си |
ная задача препроцессора, однако он может преобразовывать произвольные тексты, и этой возможностью программисту не следует пренебрегать в своей работе.
Итак, на входе препроцессора - текст с препроцессорными директивами, на выходе препроцессора - текст без препроцессорных директив (см. рис. 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