Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
Глава 3 ПРЕПРОЦЕССОРНЫЕ СРЕДСТВА В предыдущих главах мы познакомились с некоторыми базовыми понятиями и основными средствами (может быть, не самыми эффективными) программирования языка Си. С данной главы мы начнем подробное изучение тех особенностей и возможностей, которыми Си отличается от других языков программирования и которые принесли ему заслуженную популярность и любовь профессиональных программистов. Несколько нетрадиционно для пособий по языку Си начнем дальнейшее изложение материала с возможностей препроцессора. Это позволит в следующих главах продемонстрировать эффективность препроцессорных средств и их применимость при решении разнородных задач. Откладывать, как часто принято, изучение препроцессора на конец курса по языку Си, по нашему мнению, не совсем удачно. Отметим, что препроцессор обрабатывает почти любые тексты, а не только тексты программ на языке Си. Обработка программ - это основная задача препроцессора, однако он может преобразовывать произвольные тексты, и этой возможностью программисту не следует пренебрегать в своей работе. Итак, на входе препроцессора - текст с препроцессорными директивами, на выходе препроцессора - текст без препроцессорных директив (см. рис. 1.1). Стадии и директивы препроцессорной обработки В интегрированную среду подготовки программ на Си или в компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора - обработка исходного текста программы до ее компиляции (см. рис. 2.1). Стадии препроцессорной обработки. Препроцессорная обработка включает несколько стадий, выполняемых последовательно. Конкретная реализация может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись в следующем порядке: все системно-зависимые обозначения (например, системнозависимый индикатор конца строки) перекодируются в стандартные коды; каждая пара из символов '\' и «конец строки» вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов; в тексте (точнее, в тексте каждой отдельной строки) распознаются директивы и лексемы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка; выполняются директивы препроцессора и производятся макроподстановки; эскейп-последовательности в символьных константах и символьных строках, например '\n' или '\xF2', заменяются на их эквиваленты (на соответствующие числовые коды); смежные символьные строки (строковые константы) конкатенируются, то есть соединяются в одну строку; каждая препроцессорная лексема преобразуется в лексему языка Си. Поясним, что понимается под препроцессорными лексемами или лексемами препроцессора (preprocessing token). К ним относятся символьные константы, имена включаемых файлов, идентификаторы, знаки операций, препроцессорные числа, знаки препинания, строковые константы (строки) и любые символы, отличные от пробела. Можно сказать, что к лексемам препроцессора относятся лексемы языка Си, имена файлов и символы, не определенные иным способом. Знакомство с перечисленными стадиями препроцессорной обработки объясняет, как реализуются некоторые правила синтаксиса языка. Например, становится понятным смысл утверждений: «каждая символьная строка может быть перенесена в файле на следующую строку, если использовать символ '\'» или «две символьные строки, записанные рядом, воспринимаются как одна строка». Отметим, что после «склеивания» строк в соответствии с приведенными 115 HUB Стадии и директивы препроцессорной обработки правилами каждая полученная строка обрабатывается препроцессором отдельно. Рассмотрим подробно стадию обработки директив препроцессора. При ее выполнении возможны следующие действия: замена идентификаторов (обозначений) заранее подготовленными последовательностями символов; включение в программу текстов из указанных файлов; исключение из программы отдельных частей ее текста (условная компиляция); макроподстановка, то есть замена обозначения параметризованным текстом, формируемым препроцессором с учетом конкретных параметров (аргументов). Директивы препроцессора. Для управления препроцессором, то есть для задания нужных действий, используются команды (директивы) препроцессора, каждая из которых помещается на отдельной строке и начинается с символа #. Обобщенный формат директивы препроцессора: имя_директивы лексемы_препроцессора Перед символом '#' и после него в директиве разрешены пробелы. Пробелы также разрешены перед лексемами_препроцессора, между ними и после них. Окончанием препроцессорной директивы служит конец текстовой строки (при наличии символа '\', обозначающего перенос строки, окончанием препроцессорной директивы будет признак конца следующей строки текста). Определены следующие препроцессорные директивы: #define - определение макроса или препроцессорного идентификатора; #include - включение текста из файла; #undef - отмена определения макроса или идентификатора (препроцессорного); #if - проверка условия-выражения; #ifdef - проверка определенности идентификатора; #ifndef - проверка неопределенности идентификатора; #else - начало альтернативной ветви для #if; #endif - окончание условной директивы #if; #elif - составная директива #else/#if; #line - смена номера следующей ниже строки; #error - формирование текста сообщения об ошибке трансляции; #pragma - действия, предусмотренные реализацией; # - пустая директива. Кроме препроцессорных директив, имеются три препроцессорные операции, которые будут подробно рассмотрены вместе с командой #define: defined - проверка истинности операнда; ## - конкатенация препроцессорных лексем; # - преобразование операнда в строку символов. Директива #define имеет несколько модификаций. Они предусматривают определение макросов и препроцессорных идентификаторов, каждому из которых ставится в соответствие некоторая символьная последовательность. В последующем тексте программы препроцессорные идентификаторы заменяются на заранее запланированные последовательности символов. Примеры определения констант с помощью #define приведены в главе 1. Директива #include позволяет включать в текст программы текст из указанного файла. Директива #undef отменяет действие директивы #define, которая определила до этого имя препроцессорного идентификатора. Директива #if и ее модификации #ifdef, #ifndef совместно с директивами #else, #endif, #elif позволяют организовать условную обработку текста программы. При использовании этих средств компилируется не весь текст, а только те его части, которые выделены с помощью перечисленных директив. Директива #line позволяет управлять нумерацией строк в файле с программой. Имя файла и желаемый начальный номер строки указываются непосредственно в директиве #line (подробнее см. §3.6). Директива #error позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок. Директива #pragma вызывает действия, зависящие от реализации, то есть запланированные авторами компилятора. Директива # ничего не вызывает, так как является пустой директивой, то есть не дает никакого эффекта и всегда игнорируется. Рассмотрим возможности перечисленных директив и препроцес- сорных операций при решении типичных задач, поручаемых препроцессору. Одновременно на примерах поясним, что понимается под препроцессорными лексемами в обобщенном формате препро- цессорных директив. Замены в тексте Директива #define. Как уже иллюстрировалось на примере именованных констант (§1.3 и 2.1), для замены выбранного программистом идентификатора заранее подготовленной последовательностью символов используется директива (обратите внимание на пробелы): #define идентификатор строка_замещения Директива может размещаться в любом месте обрабатываемого текста, а ее действие в обычном случае распространяется от точки размещения до конца текста. Директива, во-первых, определяет идентификатор как препроцессорный. В результате работы препроцессора вхождения идентификатора, определенного командой #define, в тексте программы заменяются строкой замещения, окончанием которой обычно служит признак конца той «физической» строки, где размещена команда #define. Символы пробелов, помещенные в начале и в конце строки замещения, в подстановке не используются. Например: Исходный текст #define begin { #define end } void main( ) begin операторы end Результат препроцессорной обработки void main( ) { операторы } В данном случае программист решил использовать в качестве операторных скобок идентификаторы begin, end. До компиляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствующие указания программист дал препроцессору с помощью директив #define. Цепочка подстановок. Если в строке_замещения команды #define в качестве отдельной лексемы встречается препроцессорный идентификатор, ранее определенный другой директивой #define, то выполняется цепочка последовательных подстановок. В качестве примера рассмотрим, как можно определить диапазон (RANGE) возможных значений любой целой переменной типа int в следующей программе: #include #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_T = RANGE/8; Подстановки изменили строку замещения препроцессорного идентификатора RANGE в директиве #define, размещенной ниже, чем текст, включенный из файла limits.h. «Продвигаясь» по тексту программы, препроцессор встречает препроцессорный идентификатор RANGE и выполняет подстановку. Текст программы приобретает следующий вид: /*RANGE - диапазон значений для int*/ ... int RANGE_T = ((32767)-(-32768)+1)/8; Теперь все директивы #define удалены из текста. Получен текст, пригодный для компиляции, то есть создана «единица трансляции». Подстановка строки замещения вместо идентификатора RANGE выполнена в выражении RANGE/8, однако внутри комментария идентификатор RANGE остался без изменений и не изменился идентификатор RANGE_T. Этот пример иллюстрирует выполнение «цепочки» подстановок и ограничения на замены: замены не выполняются внутри комментариев, внутри строковых констант, внутри символьных констант и внутри идентификаторов (не может измениться часть идентификатора). Например, RANGE_T остался без изменений. Для еще одной иллюстрации перечисленных ограничений рассмотрим такой фрагмент программы: #define n 24 ... char c = '\n'; /* Символьная константа*/ /* \n - эскейп-последовательность:*/ . . . "\n Строковая константа". . . c='n'>'\n'?'n':'\n'; int k=n; В ходе препроцессорной обработки этого текста замена n на 24 будет выполнена только один раз в последнем определении, которое примет вид: int k=24; Все остальные вхождения символа n в текст программы препроцессор просто «не заметит». Вернемся к формату директивы #define. Если строка_замещения оказывается слишком длинной, то, как уже говорилось, ее можно продолжить в следующей строке текста. Для этого в конце продолжаемой строки помещается символ '\'. В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки будет удален из текста программы. Пример:
С помощью команды #define удобно выполнять настройку программы. Например, если в программе требуется работать с массивами, то их размеры можно явно определять на этапе препроцессорной обработки:
При таком описании очень легко изменять предельные размеры сразу всех массивов, изменив только одну константу (строку замещения) в директиве #define. Предусмотренные директивой #define препроцессорные замены не выполняются внутри строк, символьных констант и комментариев, то есть не распространяются на тексты, ограниченные кавычками («), апострофами (') и разделителями (/*, */). В то же время строка замещения может содержать перечисленные ограничители, например как это было в замене препроцессорного идентификатора STRING. Если в программе нужно часто печатать или выводить на экран дисплея значение какой-либо переменной и, кроме того, снабжать эту печать одним и тем же пояснительным текстом, то удобно ввести сокращенное обозначение оператора печати, например: #define PK printf("\n Номер элемента=%б.", N); После этой директивы использование в программе оператора PK; будет эквивалентно (по результату) оператору из строки замещения. Например, последовательность операторов int N = 4; PK; приведет к выводу такого текста: Номер элемента=4. Если в строку замещения входит идентификатор, определенный в другой команде #define, то в строке замещения выполняется следующая замена (цепочка подстановок). Например, программа, содержащая команды: #define K 50 #define PE printf (“\n Число элементов K=%d”,K); ... PE; выведет на экран такой текст: Число элементов К=50 Обратите внимание, что идентификатор К внутри строки замещения, обрамленной кавычками ("), не заменен на 50. Строку замещения, связанную с конкретным препроцессорным идентификатором, можно сменить, приписав уже определенному идентификатору новое значение другой командой #define: #define M 16 /* Идентификатор М определен как 16 */ #define M 'C' /* M определен как символьная константа 'C' */ #define M "C" /* M определен как символьная строка */ /* с двумя элементами: 'C' и '\0' */ Замены в тексте можно отменять с помощью команды |