Глава 8 Препроцессор
Каждая программа на языке СИ есть последовательность препроцессорных директив, описаний и определений глобальных объектов и функций.
Препроцессорные директивы (#include, #define и т.д.) управляют преобразованием текста программы до ее компиляции.
Определения вводят функции и объекты. Объекты необходимы для представления в программе обрабатываемых данных.
Функции определяют принципиально возможные действия программы.
Описания уведомляют компилятор о свойствах и именах тех объектов и функций, которые определены в других местах программы (например, ниже по ее тексту или в другом файле).
Исходная программ, подготовленная на языке СИ в виде текстового файла, походит три последовательных этапа обработки:
· Препроцессорное преобразование текста.
· Компиляция.
· Компоновка (редактирование связей или сборка).
Только после успешного завершения всех перечисленных этапов формируется исполняемый машинный код программы.
Задача препроцессора – преобразование текста программы до ее компиляции. Правила препроцессорной обработки определяет программист с помощью директив препроцессора.
Препроцессорная обработка включает несколько стадий, выполняемых последовательно. Конкретная реализация может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись в следующем порядке:
· Все системно–зависимые обозначения (например, системно–зависимый индикатор конца строки) перекодируются в стандартные коды.
· Каждая пара из символов ‘\’ и “конец строки” вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов.
· В тексте (точнее в тексте каждой отдельной строки) располагаются директивы и лексемы препроцессора, а каждый комментарий заменяется символом пустого промежутка.
· Выполняются директивы препроцессора и производятся макроподстановки (в зависимости от конкретной программы). Препроцессор “сканирует” исходный текст программы в поиске строк, начинающихся с символа #. Такие строки воспринимаются препроцессором как команды (директивы), которые определяют действия по преобразованию текста:
· Замена идентификаторов (обозначений) заранее подготовленными последовательностями символов.
· Включение в программу текстов из указанных файлов.
· Исключение из программы отдельных частей ее текста (условная компиляция).
· Макроподстановка, т.е. замена значения параметризированным текстом, формируемым препроцессором с учетом конкретных параметров (аргументов).
· Esc-последовательности в символьных константах и символьных строках,
например: ‘\n’ или ‘\xF2’, заменяются на их эквиваленты (на соответствующие числовые коды).
· Смежные символьные строки (строковые константы) конкатенируются, т.е. соединяются в одну строку.
· Каждая препроцессорная лексема преобразуется в лексему языка СИ.
Формат директивы препроцессора:
# имя_директивы лексемы_препроцессора
Перед символом # и после него в директиве разрешены пробелы. Пробелы также разрешены перед лексемами_препроцессора, между ними и после их.
Окончанием препроцессорной директивы служит конец текстовой строки (при наличии символа ‘\’, обозначающего перенос строки, окончанием препроцессорной директивы будет признак конца следующей строки текста).
Поясним, что подразумевается под препроцессорными лексемами или лексемами препроцессора (preprocessing token). К ним относятся символьные константы, имена включаемых файлов, идентификаторы, знаки операций, препроцессорные числа, знак препинания, строковые константы (строки) и любые символы, не определенные другим способом.
Определены следующие препроцессорные директивы:
#define –определение макроса или препроцессорного идентификатора.
#include –включение текста из файла.
#undef –отмена определения макроса или идентификатора (препроцессорного)
#if –проверка условия–выражения.
#ifdef –поверка определенности идентификатора.
#ifndef –проверка неопределенности идентификатора.
#else –начало альтернативной ветви для #if.
#endif –окончание условной директивы #if.
#elif –составная директива #else/#if
#line –смена номера следующей ниже строки.
#error –формирование текста сообщения об ошибке трансляции.
#pragma –действия, предусмотренные реализацией.
# –пустая директива.
Директива #define имеет несколько модификаций. Они предусматривают определение макросов и препроцессорных идентификаторов, каждому из которых ставится в соответствие некоторая символьная последовательность.
Директива #include позволяет включать в текст программы текст из указанного файла.
Директива #undef отменяет действие директивы #define, которая определила до этого имя препроцессорного идентификатора.
Директива #if и ее модификации #ifdef, #ifndef свместно с директивами #else, #endif, #elif позволяют организовать условную обработку текста программы. При использовании этих средств компилируется не весь текст, а только те его части которые выделены с помощью перечисленных директив.
Директива #line позволяет управлять нумерацией строк в файле с программой. Имя файла и желаемый начальный номер строки указывается непосредственно в директиве #line.
Директива #error позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок.
Директива #pragma вызывает действия, зависящие от реализации, т.е. запланированные авторами программы.
Директива # ничего не вызывает, т.к. является пустой директивой, т.е. не дает никакого эффекта и всегда игнорируется.
8.1 Макроопределения
Начнем с самой простой – с макроопределений констант. Мы можем задать какую-либо константу как
const int a = 5;.
В таком случае a – это полноценная переменная, с собственным адресом и областью видимости. Более того, написав
*((int*) &a) = 6;,
мы даже сможем изменять содержимое этой константы. По сути дела, модификатор const не задает константу, а лишь указывает компилятору, что значение переменной по идее не должно меняться.
Но есть и другой способ. Он хорош, если требуется задать не переменную, а именно некоторую константу (как правило – параметр).Для замены выбранного программистом идентификатора заранее подготовленной последовательностью символов используется директива (обратите внимание на пробелы):
#define <идентификатор> <строка_замещения>
(обратите внимание на отсутствие точки с запятой). Символы пробелов, помещенные в начале и в конце строки замещения, в подстановке не используются.
Например: #define VAR 5
Директива может размещаться в любом месте обрабатываемого текста. , а ее действие в обычном случае распространяется от точки размещения до конца текста. Директива, во-первых, определяет идентификатор как препроцессорный. В результате работы препроцессора вхождения идентификатора, определенного командой #define, в тексте программы заменяются строкой замещения, окончанием которой обычно служит признак конца той “физической” строки.
Где действует макроопределение? Ввиду того, что замена производится до компиляции, областей видимости на этот этап времени еще нет. Поэтому макроопределение действительно с момента его объявления на протяжении всего текста программы до конца файла или явной отмены определения директивой #undef:
Например: #undef VAR
Исходный текст Результат препроцессорной обработки
#define BEGIN {
#define END {
void main() void main()
begin {
операторы операторы
end }
В данном случае программист решил использовать в качестве операторных скобок идентификаторы begin и end. До компиляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствующие указания программист дал препроцессору с помощью директив #define.
Данные ранее определения переопределять нельзя. Если это надо все же сделать, требуется сначала отменить определение:
Например: #define VAR 2 ... #undef VAR 2 #define VAR 3 .... #undef VAR 3 #define VAR 4И, наконец, определение не должно превышать одну строку. Если <строка_замещения> оказывается слишком длинной, то ее можно продолжить в следующей строке текста. Для этого в конце продолжаемой строки помещается символ ‘\’. В ходе одной из стадий препроцессорной обработки этот символ вместе с последующим символом конца строки будет удален из текста программы.
Пример:
#define STRING “\n Game Over! \
– \t Игра закончена!”
…
printf(STRING);
На экране будет выведено:
Game Over! – Игра закончена!
К склейке применимы два правила:
1) Между косой чертой и символом новой строки не должно быть никаких других символов (пробелов, табуляций, ... );
2) Склейка удаляет только символ новой строки и никак не затрагивает символы, идущие после нее (например, табуляции, помещенные с целью повышения читаемости программы). Это критично только для символьных строк, которые рекомендуется склеивать несколько иначе:
Пример:
#define LONG_STR "Это длинная строка..." "...очень длинная строка..." "ну просто очень длинная строка."
Для всего остального лишняя табуляция препятствием не является.
С помощью команды #define удобно выполнять настройку программы.
Например, если в программе требуется работать с массивами, то их размеры можно явно определить на этапе препроцессорной обработки:
Исходный текст Результат препроцессорной обработки
#define K 40
void main() void main()
{ {
int M[K][K]; int M[40][40];
float A[2*K+1]; float A[2*40+1];
float B[K+3][K–3]; float B[40+3][40–3];
· #define M 16 /* Идентификатор M определен как int 16*/
· #define M 'c' /* Идентификатор определен как симв. const*/
· #define M "c" /* Идентификатор определен как симв. строка
с двумя элементами 'c', '\0' */
· #define PK printf ( \n Номер элемента = %d", i )
…
int i = 4; На экране:
PK Номер элемента = 4
…
#define REAL long double
#define E (5+10) // Подстановка в программе 2*E приведет
#define E 5+10 // к разным результатам
#define PI 3.141592
Макрозамены не производятся в строковых константах:
#include <stdio.h>
#define str строка
void main (void) {
printf ("Это моя str.\n"); // Печатается "Это моя str."
}
#include <stdio.h>
#define str "строка"
void main (void) {
printf ("Это моя %s.\n", str); // Печатается "Это моя строка."
}
Макрозамена будет произведена только во втором случае.
8.2 Включение в программу заголовочных файлов
Директива #include <…> предназначена для включения в текст программы текста файла из каталога “заголовочных файлов”, поставляемых вместе со стандартными библиотеками компилятора.
Каждая библиотечная функция, определенная стандартом языка СИ, имеет соответствующее описание (прототип библиотечной функции плюс определения типов, переменных, макроопределений и констант) в одном из заголовочных файлов.
Список заголовочных файлов для стандартных библиотек определен стандартом языка.
#include <имя_заголовочного_файла>
не подключает к программе соответствующую стандартную библиотеку. Препроцессорная обработка выполняется на уровне исходного текста программы. Директива #include только позволяет вставить в текст программы описания из указанного заголовочного файла. Подключение к программе кодов библиотечных функций осуществляется только на этапе редактирования связей (этап компоновки), т.е. после компиляции, когда уже получен машинный код программы. Доступ к кодам библиотечных функций нужен только на этапе компоновки.
Именно поэтому компилировать программу и устранять синтаксические ошибки в ее тексте можно без стандартной библиотеки, но обязательно с заголовочными файлами.
Здесь следует отметить еще одну важную особенность. Хотя в заголовочных файлах содержатся описания всех стандартных функций, в код программы включаются только те функции, которые используются в программе. Выбор нужных функций выполняет компоновщик на этапе, называемом «редактирование связей».
Термин “заголовочный файл” (header file) в применении к файлам содержащим описания библиотечных функций стандартных библиотек, не случаен.
Он предполагает включение этих файлов именно в начало программы. Мы настоятельно рекомендуем, чтобы до обращения к любой функции она была определена или описана в том же файле, где помещен текст программы. Описание или определение функции должно быть “выше” по тексту, чем вызов функций. Именно поэтому заголовочные файлы нужно помещать в начале текста программы, т.е. заведомо раньше обращений к соответствующим библиотечным функциям.
Хотя заголовочный файл может быть включен в программу не в ее начале, а непосредственно перед обращением к нужной библиотечной функции, такое размещение директив #include <…> не рекомендуется.
Команда #include имеет три формата записи:
#include <имя_файла> /*имя в угловых скобках*/
#include “имя_файла” /*имя в кавычках*/
#include имя_макроса /*макрос, расширяемый до обозначения файла*/
где <имя_макроса> – это введенный директивой #define предпроцессорный идентификатор либо макрос, при замене которого после конечного числа подстановок будет получена последовательность символов <имя_файла> либо “имя_файла”.
Существует правило, что если имя_файла – в угловых скобках, то препроцессор разыскивает файл в стандартных системных каталогах. Если имя_файла заключено в кавычки, то вначале препроцессор просматривает текущий каталог пользователя и только затем обращается к просмотру стандартных системных каталогов.
Дата добавления: 2015-06-10; просмотров: 1161;