Главная страница

Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник


Скачать 1.57 Mb.
НазваниеВ., Фомин С. С. Курс программирования на языке Си Учебник
АнкорКурс на Си
Дата18.02.2023
Размер1.57 Mb.
Формат файлаdocx
Имя файлаПодбельский. Курс программирования на Си.docx
ТипУчебник
#943863
страница11 из 42
1   ...   7   8   9   10   11   12   13   14   ...   42
Глава 3

ПРЕПРОЦЕССОРНЫЕ СРЕДСТВА

В предыдущих главах мы познакомились с некоторыми базовы­ми понятиями и основными средствами (может быть, не самыми эффективными) программирования языка Си. С данной главы мы начнем подробное изучение тех особенностей и возможностей, кото­рыми Си отличается от других языков программирования и которые принесли ему заслуженную популярность и любовь профессиональ­ных программистов.

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

Итак, на входе препроцессора - текст с препроцессорными ди­рективами, на выходе препроцессора - текст без препроцессорных директив (см. рис. 1.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:

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

Директива #include позволяет включать в текст программы текст из указанного файла.

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

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

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

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

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

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

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

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

Директива #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 STRING "\n Game Over!

- \

Игра закончена!"

printf(STRING);




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




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





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

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

#define K 40 void main( ) 1

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

void main( )

1

{

int M[K][K];

float A[2*K+1], float B[K+3][K-3];

{

int M[40][40];

float A[2*40+1], float B[40+3][40-3];

При таком описании очень легко изменять предельные размеры сразу всех массивов, изменив только одну константу (строку заме­щения) в директиве #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' */

Замены в тексте можно отменять с помощью команды
1   ...   7   8   9   10   11   12   13   14   ...   42


написать администратору сайта