11. Препроцессор языка Си

ДИРЕКТИВЫ ПРЕПРОЦЕССОРА СИМВОЛЬНЫЕ КОНСТАНТЫ МАКРООПРЕДЕЛЕНИЯ И "МАКРОФУНКЦИИ" ПОБОЧНЫЕ ЭФФЕКТЫ МАКРООПРЕДЕЛЕНИИ ВКЛЮЧЕНИЕ ФАЙЛОВ УСЛОВНАЯ КОМПИЛЯЦИЯ

ДИРЕКТИВЫ ПРЕПРОЦЕССОРА #define, #include, #undef, #if, #ifdef, #ifndef, #else, #endif

Язык Си был разработан в помощь работающим программистам, а им нравится его препроцессор. Этот полезный помощник просматривает программу до компилятора (отсюда и термин "препроцессор") и заменяет символические аббревиатуры в программе на соответствующие директивы. Он отыскивает другие файлы, необходимые вам, и может также изменить условия компиляции. Однако эти слова не отражают истинную пользу и значение препроцессора, поэтому обратимся к примерам. Конечно, до сих пор мы снабжали все примеры директивами #define и #include, но теперь мы можем подытожить все, что изучили, и развить тему дальше.

СИМВОЛИЧЕСКИЕ КОНСТАНТЫ: #define

Директива #define, подобно всем директивам препроцессора, начинается с символа # в самой левой позиции. Она может появиться в любом месте исходного файла, а даваемое ею определение имеет силу от места появления до конца файла. Мы активно используем эту директиву для определения символических констант в наших программах, однако она имеет более широкое применение; что мы и покажем дальше. Вот пример, иллюстрирующий некоторые возможности и свойства директивы #define:

/* простые примеры директивы препроцессора */

#define TWO 2 /* по желанию можно использовать комментарии */

#define MSG "Старый серый кот поет веселую песню."

/* обратная косая черта продолжает определение на следующую строку */

#define FOUR TWO *TWO

#define PX printf("X равен %d.\n", x)

#define FMT "X равен %d.\n"

main( )

{

int x = TWO;

PX;

x = FOUR;

printf(FMT, x);

printf( "%s\n", MSG);

printf("FTWO: MSG\n");

}


Каждая строка состоит из трех частей. Первой стоит директива #define. Далее идет выбранная нами аббревиатура, известная у программистов как "макроопределение". Макроопределение не должно содержать внутри себя пробелы. И наконец, идет строка (называемая "строкой замещения"), которую представляет макроопределение. Когда препроцессор находит в программе одно из ваших макроопределений, он почти всегда заменяет его строкой замещения. (Есть одно исключение, которое мы вам сейчас покажем.) Этот процесс прохождения от макроопределения до заключительной строки замещения называется "макрорасширением". Заметим, что при стандартной форме записи на языке Си можно вставлять комментарии; они будут игнорироваться препроцессором. Кроме того, большинство систем разрешает использовать обратную косую черту ('\') для расширения определения более чем на одну строку.

"Запустим" наш пример, и посмотрим за его выполнением.

Х равен 2.

Х равен 4.

Старый серый кот поет веселую песню. TWO: MSG

Вот что произошло. Оператор int x = TWO; превращается в int x = 2;.

РИС. 11.1. Части макроопределения.

т. е. слово TWO заменилось цифрой 2. Затем оператор РХ; превращается в printf("X равно %d. \n", х); поскольку сделана полная замена. Это новшество, так как до сих пор мы использовали макроопределения только для представления констант. Теперь же мы видим, что макроопределение может представлять любую строку, даже целое выражение на языке Си. Заметим, однако, что это константная строка; РХ напечатает только переменную, названную x.

Следующая строка также представляет что-то новое. Вы можете подумать, что FOUR заменяется на 4, но на самом деле выполняется следующее:

х = FOUR;

превращается в х = TWO *TWO; превращается в х = 2*2; и на этом все заканчивается. Фактическое умножение имеет место не во время работы препроцессора и не при компиляции, а всегда без исключения при работе программы. Препроцессор не выполняет вычислений; он только очень точно делает предложенные подстановки.

Заметим, что макроопределение может включать другие определения. (Некоторые компиляторы не поддерживают это свойство "вложения".)

В следующей строке printf(FMT, х); превращается в printf(" Х равно %d.\n", х) когда FMT заменяется соответствующей строкой. Этот подход может оказаться очень удобным, если у вас есть длинная строка, которую вы используете несколько раз.

В следующей строке программы MSG заменяется соответствующей строкой. Кавычки делают замещающую строку константой символьной строки; поскольку программа получает ее содержимое, эта строка будет запоминаться в массиве, заканчивающемся нуль-символом. Так, #define HAL 'Z' определяет символьную константу, а #define НАР "Z" определяет символьную строку: Z\0.

Обычно препроцессор, встречая одно из ваших макроопределений в программе, очень точно заменяет их эквивалентной строкой замещения. Если эта строка также содержит макроопределения, они тоже замешаются. Единственным исключением при замене является макроопределение, находящееся внутри двойных кавычек. Поэтому printf("TWO: MSG"); печатает буквально TWO: MSG вместо печати следующей строки:

2: "Старый серый кот поет веселую песню."

Если вам нужно напечатать эту строку, можно использовать оператор

printf("%d: %s\n", TWO, MSG);

потому что здесь макроопределения находятся вне кавычек.

Когда следует использовать символические константы? Вероятно, вы должны применять их для большинства чисел. Если число является константой, используемой в вычислениях, то символическое имя делает яснее ее смысл. Если число - размер массива, то символическое имя упрощает изменение вашей программы при работе с большим массивом. Если число является системным кодом, скажем для символа EOF, то символическое представление делает вашу программу более переносимой; изменяется только определение EOF. Мнемоническое значение, легкость изменения, переносимость: все это делает символические константы заслуживающими внимания. Легко ли этого достичь? Рискнем и рассмотрим простую функцию, т. е. макроопределение с аргументами.

ИСПОЛЬЗОВАНИЕ АРГУМЕНТОВ С #define

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

/* макроопределение с аргументами */

#define SQUARE (х) х*х

#define PR(x) printf("x равен %d.\n" ,x)

main( )

{

int x = 4;

int z;

z = SQUARE(x);

PR(z);

z = SQUARE(2);

PR(z);

PR(SQUARE(x));

PR(SQUARE(x + 2));

PR(100/SQUARE(2));

PR(SQUAREC(++x));

}

Всюду, где в вашей программе появляется макроопределение SQUARE(x), оно заменяется на х*х. В отличие от наших прежних примеров при использовании этого макроопределения мы можем совершенно свободно применять символы, отличные от x. В макроопределении 'x' замещается символом, использованным в макровызове программы. Поэтому макроопределение SQUARE(2) замещается на 2*2. Таким образом, x на самом деле действует как аргумент. Однако, как вы вскоре увидите, аргумент макроопределения не "работает" точно так же, как аргумент функции. Вот результаты выполнения программы. Обратите внимание, что некоторые ответы отличаются от тех, которые вы могли бы ожидать.

z paвнo 16.

z paвнo 4.

SQUARE(x) равно 16.

SQUARE(x + 2) равно 14.

100/SQUARE(2) равно 100.

SQUAREC(++ x) равно 30.


Первые две строки предсказуемы. Заметим, однако, что даже внутри двойных кавычек в определении PR переменная замещается соответствующим аргументом. Все аргументы в этом определении замещаются.

Третья строка представляет интерес:

PR(SQUARE(x));

она становится следующей строкой:

printf("SQUARE(x) равно %d.\n", SQUARE(x));

после первого этапа макрорасширения. Второе SQUARE(x) расширяется, превращаясь в х*х, а первое остается без изменения, потому что теперь оно находится внутри двойных кавычек в операторе программы, и таким образом защищено от дальнейшего расширения. Окончательно строка программы содержит

printf(" SQUARE(x) равно %d.\n", x*x);

и выводит на печать

SQUARE(x) равно 16.

при работе программы.

Давайте еще раз проверим то, что заключено в двойные кавычки. Если ваше макроопределение включает аргумент с двойными кавычками, то аргумент будет замещаться строкой из макровызова. Но после этого он в дальнейшем не расширяется, даже если строка является еще одним макроопределением. В нашем примере переменная х стала макроопределением SQUARE(x) и осталась им.

Теперь мы добрались до несколько специфических результатов. Вспомним, что х имеет значение b. Это позволяет предположить, что SQUARE(x + 2) будет равно 6*6 или 36. Но напечатанный результат говорит, что получается число 14, которое, несомненно, никак не похоже на квадрат целого числа! Причина такого вводящего в заблуждение результата проста, и мы уже об этом говорили: препроцессор не делает вычислений, он только замещает строку. Всюду, где наше определение указывает на х, препроцессор подставит строку х + 2. Таким образом, х*х становится х + 2*х + 2.

Единственное умножение здесь 2*x. Если x равно 4, то получается следующее значение этого выражения:

4+2*4+2=4+8+2= 14.

Этот пример точно показывает очень важное отличие между вызовом функции и макровызовом. Вызов функции передает значение аргумента в функцию во время выполнения программы. Макровызов передает строку аргументов в программу до ее компиляции; это другой процесс, происходящий в другое время.

Можно ли ваше определение переделать так, чтобы SQUARE(x + 2) было равно 36? Конечно. Нам просто нужно больше скобок:

#define SQUARE(x) (x)*(x)

Тогда SQUARE(x + 2) становится (х + 2)*(х + 2), и мы получаем наше желанное умножение, так как перенесли скобки в строку замещения.

Однако это не решает всех наших проблем. Рассмотрим случаи, которые приводят к следующей строке на выходе: 100/SQUARE(2) превращается в 100/2*2 .

Вычисления следует вести слева направо, т. е. (100/2)*2 или 50*2 или 100.


Эту путаницу можно исправить, определив SQUARE(x) следующим образом:

#define SQUARE(x) (x*x)

Это даст 100/(2 *2), что в конечном счете эквивалентно 100/4 или 25.

Чтобы выполнить два последних примера, нам необходимо определение

#define SQUARE(x) ((x)*(x))

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


Даже эти предосторожности не спасают последний пример от беды: SQUARE(++ х) превращается в ++х * ++х и x увеличивается дважды - один раз до умножения и один раз после: ++х * ++х = 5*6 = 30.

(Так как порядок выполнения операций не установлен, то некоторые компиляторы превратят это в 6*5, но конечный результат будет тем же самым.)

Единственное лекарство в этом случае - не использовать ++х в качестве аргумента для макроопределения. Заметим, что ++х обычно работает как аргумент функции, так как ему присваивается значение 5, и затем это значение 5 передается функции.

МАКРООПРЕДЕЛЕНИЕ ИЛИ ФУНКЦИЯ?

Многие задачи можно решать, используя макроопределение с аргументами или функцию. Что из них следует применять нам? На этот счет нет строгих правил, но есть некоторые соображения.

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

Выбор макроопределения приводит к увеличению объема памяти, а выбор функции - к увеличению времени работы программы. Так что думайте, что выбрать! Макроопределение создает "строчный" код, т. е. вы получаете оператор в программе. Если макроопределение применить 20 раз, то в вашу программу вставится 20 строк кода. Если вы используете функцию 20 раз, у вас будет только одна копия операторов функции, поэтому получается меньший объем памяти. Однако управление программой следует передать туда, где находится функция, а затем вернуться в вызывающую программу, а на это потребуется больше времени, чем при работе со "строчными" кодами.

Преимущество макроопределений заключается в том, что при их использовании вам не нужно беспокоиться о типах переменных (макроопределения имеют дело с символьными строками, а не с фактическими значениями). Так наше макроопределение SQUARE(x) можно использовать одинаково хорошо с переменными типа int или float.

Обычно программисты используют макроопределения для простых действии, подобных следующим:

#define MAX(X,Y) ( (X) > (Y) ? (X) : (Y))

#define ABS(X) ( (X) < 0 ? -(X) : (X))

#define ISSIGN(X) ( (X) == '+' || (X) == '-' ? 1 : 0)


(Последнее макроопределение имеет значение 1 (истинно), если X является символом алгебраического знака.) Отметим следующие моменты:


1. В макроопределении нет пробелов, но они могут появиться в замещающей строке. Препроцессор "полагает", что макроопределение заканчивается на первом пробеле, поэтому все, что стоит после пробела, остается в замещающей строке.

РИС. 11.2. Ошибочный пробел и макроопределении.

2. Используйте круглые скобки для каждого аргумента и всего определения. Это является гарантией того, что элементы будут сгруппированы надлежащим образом в выражении, подобном forks = 2*MAX(guests + 3, last);


3. Для имен макрофункций следует использовать прописные буквы. Это соглашение не распространяется так широко, как соглашение об использовании прописных букв для макроконстант. Применение их предостережет вас от возможных побочных эффектов макроопределений.

Предположим, что вы разработали несколько макрофункций по своему усмотрению. Должны ли вы их переопределять каждый раз, когда пишете новую программу? Нет, если вы вспомните о директиве #include. Теперь рассмотрим ее.

ВКЛЮЧЕНИЕ ФАЙЛА: #include

Когда препроцессор "распознает" директиву #include, он ищет следующее за ней имя файла и включает его в текущий файл. Директива выдается в двух видах:

#include имя файла в угловых скобках

#include "mystuff.h" имя файла в двойных кавычках


В операционной системе UNIX угловые скобки сообщают препроцессору, что файл следует искать в одном или нескольких стандартных системных каталогах. Кавычки говорят ему, что сначала нужно смотреть в вашем каталоге (или в каком-то другом, если вы определяете его именем файла), а затем искать в "стандартных" местах.

#include ищет в системном каталоге

#include "hot.h" ищет в вашем текущем рабочем каталоге

#include "/usr/biif/p.h" ищет в каталоге /usr/biff

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

#include "stdio.h" ищет на стандартном диске

#include ищет на стандартном диске

#include "a : stdio.h" ищет на диске а

Зачем включают файлы? Потому что они несут нужную вам информацию. Файл stdio.h, например, обычно содержит определения EOF, getchar( ) и putchar( ). Две последние Функции определены как макрофункции.

По соглашению суффикс .h используется для "заголовочных" файлов, т. е. файлов с информацией, которая располагается в начале вашей программы. Заголовочные файлы обычно состоят из операторов препроцессора. Некоторые файлы, подобно stdio.h, включены в систему, но вы можете создать и свой собственный.

Заголовочные файлы:

Пример:

Предположим, например, что вам правится использовать булевы переменные, т. с. вместо того чтобы иметь 1 как "истину" и 0 как "ложь", хотели бы использовать слова TRUE и FALSE. Можно создать файл, названный, скажем, bool.h, который содержал бы эти определения:

/* файл bool.h */

#define BOOL int;

#define TRUE 1

#define FALSE 0

Вот пример программы, использующей этот заголовок:

/* считает пустые символы */

#include

#include "bool.h"

main( )

{

int ch;

int count = 0;

BOOL whitesp( );

while((ch = getchar( )) != EOF)

if(whitesp(ch)) count++;

printf(" Имеется %d пустых символов. \n", count);

}

BOOL whitesp(c)

char c;

if(c == ' ' || с == '\n' || с == '\t')

return(TRUE);

else

return(FALSE);

}

Замечания по программе

1. Если две функции в этой программе ('main( )' и 'whitesp( )') следовало скомпилировать раздельно, то нужно было бы использовать директиву #include "bool.h" с каждой из них.


2. Выражение if(whitesp(ch)) аналогично if(whitesp(ch)) == TRUE, так как сама функция whitesp(ch) имеет значение TRUE или FALSE.


3. Мы не создали новый тип BOOL, так как BOOL уже имеет тип int. Цель наименования функции BOOL - напомнить пользователю, что функция используется для логических вычислений (в противоположность арифметическим).


4. Использование функции в предусматриваемых логических сравнениях может сделать программу яснее. Она может также сэкономить наш труд, если сравнение встречается в программе более одного раза.


5. Мы могли бы использовать макроопределение вместо функции для задания whitesp( ).

Многие программисты разрабатывают свои стандартные заголовочные файлы, чтобы использовать их в программах. Некоторые файлы могут создаваться для специальных целей, другие можно использовать почти в каждой программе. Так как включенные файлы могут содержать директивы #include, можно, если хотите, создать сжатый, хорошо огранизованный заголовочный файл. Рассмотрим этот пример:

/*заголовочный файл mystuff.h*/

#include

#include "bool.h"

#include "funct.h"

#define YES 1

#define NO 0


Во-первых, мы хотим напомнить вам, что препроцессор языка Си распознает комментарии, помеченные /* и */, поэтому мы можем включать комментарии в эти файлы.

Во-вторых, мы включили три файла. По-видимому, третий содержит некоторые макрофункции, которые используются часто.

В-третьих, мы определили YES как 1, тогда как в файле bool.h мы определили TRUE как 1. Но здесь нет конфликта, и мы можем использовать слова YES и TRUE в одной и той же программе. Каждое из них будет заменяться на 1. Может возникнуть конфликт, если мы добавим в файл строку

#define TRUE 2

Второе определение вытеснило бы первое, и некоторые препроцессоры предупреждали бы вас, что TRUE переопределено.

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

#include "sort.с"

чтобы скомпилировать его совместно с вашей текущей программой.

Директивы #include и #define являются наиболее активно используемыми средствами препроцессора языка Си. Рассмотрим более сжато другие его директивы.

ДРУГИЕ ДИРЕКТИВЫ: #undef, #if, #ifdef, #ifndef, #else И #endif

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

Директива #undef отменяет самое последнее определение поименованного макроопределения.

#define BIG 3

#define HUGE 5

#undef BIG /* BIG теперь не определен */

#define HUGE 10 /* HUGE переопределен как 10 */

#undef HUGE /* HUGE снова равен 5*/

#undef HUGE /* HUGE теперь не определен */


Очевидно (мы надеемся), вы не будете компилировать файл, как в этом примере. Предположим, что у вас есть большой стандартный файл, определенный директивой #include, который вы хотели бы использовать, но для этого некоторые из его определений для одной из функций программы нужно будет временно изменить, Вместо того чтобы иметь дело с этим файлом, вы можете просто включить его, а затем окружить такую выделяющуюся функцию соответствующими директивами #define и #undef.

Или, предположим, что вы работаете с большой системой программ. Вы хотите задать макроопределение, но не уверены, использует ли ваше определение другие определения откуда-нибудь из другого места системы. В этом случае просто отмените ваше макроопределение в том месте, где оно вам больше не нужно, а оригинал этого макроопределения, если он есть, будет по-прежнему оставаться в силе для остальной части системы.

Другие упомянутые нами директивы позволяют выполнять условную компиляцию. Вот пример:

#ifdef MAVIS

#include " horse.h" /* выполнится, если MAVIS определен */

#define STABLES 5

#else

#include "cow.h" /*выполнится, если MAVIS не определен */

#define STABLES 15

#endif


Директива #ifdef сообщает, что если последующий идентификатор (MAVIS) определяется препроцессором, то выполняются все последующие директивы вплоть до первого появления #else или #endif. Когда в программе есть #else, то программа от #else до #endif будет выполняться, если идентификатор не определен.

Такая структура очень напоминает конструкцию if-else языка Си. Основная разница заключается в том, что препроцессор не распознает фигурные скобки {}, отмечающие блок, потому что он использует директивы #else (если есть) и #endif (которая должна быть) для пометки блоков директив.

Эти условные конструкции могут быть вложенными.

Директивы #ifdef и #if можно использовать с #else и #endif таким же образом. Директива #ifndef опрашивает, является ли последующий идентификатор неопределенным; эта директива противоположна #ifdef. Директива #if больше похожа на обычный оператор if языка Си. За ней следует константное выражение, которое считается истинным, если оно не равно нулю:

#if SYS == "IBM"

#include "ibm.h"

#endif

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

Эти краткие примеры иллюстрируют удивительную способность языка Си изощренно и строго управлять программами.

ЧТО ВЫ ДОЛЖНЫ БЫЛИ УЗНАТЬ В ЭТОЙ ГЛАВЕ

Как определять символьные константы директивой #define: #define FINGERS 10

Как включать другие файлы: #include "albanian.h"

Как определить макрофункцию: #define NEG(X) (-(X))

Когда использовать символические константы: часто.

Когда использовать макрофункции: иногда.

Опасность применения макрофункций: побочные эффекты.

ВОПРОСЫ И ОТВЕТЫ

Вопросы


1. Ниже приведены группы операторов, содержащих по одному и более макроопре делений, за которыми следуют строки исходных кодов, использующих эти макро определения. Какой результат получается в каждом случае? Правилен ли он?


a. #define FPM 5280 /* футов в миле */

dist = FPM * miles;


б. #define FEET 4

#define POD FEET + FEET

plort = FEET * POD;


в. #define SIX = 6;

nex = SIX;


г. #define NEW(X) X + 5

у = NEW(y);

berg = NEW(berg) * lob;

est = NEW(berg) / NEW(y);

nilp = lob * NEW(-berg);


2. Подправьте определение в вопросе 1.г, чтобы сделать его более надежным.


3. Определите макрофункцию, которая возвращает минимальное из двух значений.


4. Задайте макроопределение, в котором есть функция whitesp(с) считающая в программе пустые символы.


5. Определите макрофункцию, которая печатает представления значения двух целых выражений.

Ответы


1.

а. dist = 5280 * miles; правильно.


б. plot = 4 * 4 + 4; правильно, но если пользователь на самом деле хотел иметь 4 * (4 + 4), то следовало применять директиву #define POD (FEET + FEET).


в. nex = = 6; неправильно; очевидно, пользователь забыл, что он пишет для препроцессора, а не на языке Си.


г. у - у + 5; правильно.

berg = berg + 5 * lob; правильно, но, вероятно, это нежелательный результат.

est = berg + 5/у + 5; то же самое.

nilp = lob * -berg + 5; то же самое.


2. #define NEW(X) ((X) + 5)


3. #deline MIN(X,Y) ((X) < (Y) ? (X) : (Y))


4. #define WHITESP(C) ((С) == ' ' || (С) == '\n' || (С)) == '\t')


5. #define PR2(X,Y) printf(" X равно %d и Y равно %d.\n", X, Y)

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

УПРАЖНЕНИЕ

1. Создайте заголовочный файл определений препроцессора, которые вы хотите применять.

Загрузка...