Основы программирования на языке C

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

Разбить на страницы
Показывать лекцию целиком

Общие сведения

В интегрированную среду подготовки программ на Си или в компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора - обработка исходного текста программы до ее компиляции. Препроцессорная обработка включает несколько стадий, выполняемых последовательно. Конкретная реализация может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись в следующем порядке:

  • Все системно-зависимые обозначения перекодируются в стандартные коды.
  • Каждая пара из символов ' \ ' и "конец строки" вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов.
  • В тексте распознаются директивы и лексемы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка.
  • Выполняются директивы препроцессора и производятся макроподстановки.
  • Эскейп-последовательности в символьных константах и символьных строках заменяются на их эквиваленты.
  • Смежные символьные строки конкатенируются, то есть соединяются в одну строку.
  • Каждая препроцессорная лексема преобразуется в текст на языке Си.
  • Поясним, что понимается под препроцессорными лексемами или лексемами препроцессора. К ним относятся символьные константы, имена включаемых файлов, идентификаторы, знаки операций, препроцессорные числа, знаки препинания, строковые константы и любые символы, отличные от пробела.

    Стадия обработки директив препроцессора. При ее выполнении возможны следующие действия:

  • замена идентификаторов заранее подготовленными последовательностями символов;
  • включение в программу текстов из указанных файлов;
  • исключение из программы отдельных частей ее текста, условная компиляция;
  • макроподстановка, то есть замена обозначения параметризованным текстом, формируемым препроцессором с учетом конкретных аргументов.
  • Символические константы: #define

    Если в качестве первого символа в строке программы используется символ #, то эта строка является командной строкой препроцессора (макропроцессора). Командная строка препроцессора заканчивается символом перевода на новую строку. Если непосредственно перед концом строки поставить символ обратной косой черты " \ ", то командная строка будет продолжена на следующую строку программы.

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

    Замена идентификаторов

    #define идентификатор строка

    Пример:

    #define ABC 100

    Заменяет каждое вхождение идентификатора ABC в тексте программы на 100:

    #undef идентификатор

    Пример:

    #undef ABC

    Отменяет предыдущее определение для идентификатора ABC.

    Пример:

    /* Простые примеры директивы препроцессора */
    #define TWO 2 /* можно использовать комментарии*/
    #define MSG "Текст 1.\
    Продолжение текста 1"
    /* обратная косая черта продолжает определение на следующую строку */
    #define FOUR TWO*TWO
    #define PX printf("X равен %d.\n", x)
    #define FMT "X равен %d.\n"
    int main( )
    {
    	int x = TWO;
    	PX;
    	x = FOUR;
    	printf(FMT,x);
    	printf("%s\n",MSG);
    	printf("TWO: MSG\n");
    	return TWO;
    }

    В результате выполнения нашего примера будем иметь:

    X равен 2
    X равен 4
    Текст 1. Продолжение текста 1
    TWO: MSG

    Разберем, что произошло. Оператор

    int x = TWO;

    превращается в

    int x = 2;

    Затем оператор

    PX;

    превращается в

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

    поскольку сделана полная замена. Теперь мы видим, что макроопределение может представлять любую строку, даже целое выражение на языке Си. Заметим, что это константная строка. PX напечатает только переменную, названную x.

    В следующей строке выполняется следующее:

    x = FOUR;

    превращается

    x = TWO*TWO;

    превращается в

    x = 2*2;

    и на этом все заканчивается. Фактическое умножение имеет место не во время работы препроцессора и не при компиляции, а всегда без исключения при работе программы (Уточнение: это зависит от конкретного компилятора). Препроцессор не выполняет вычислений. Он только очень точно делает предложенные подстановки. Заметим, что макроопределение может включать другие определения. Некоторые компиляторы не поддерживают это свойство вложения. В следующей строке

    printf(FMT,x);

    превращается в

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

    когда FMT заменяется соответствующей строкой. Этот подход может оказаться очень удобным, если есть длинная строка, которую мы используем несколько раз. В следующей строке программы MSG заменяется соответствующей строкой. Кавычки делают замещающую строку константой символьной строки. Поскольку программа получает ее содержимое, эта строка будет запоминаться в массиве, заканчивающемся нуль-символом. Так,

    #define HAL 'X' определяет символьную константу, а 
    #define HAR "X" определяет строковую строку X\0

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

    printf("TWO: MSG");

    печатает буквально TWO: MSG вместо печати следующего текста:

    2: "Текст 1.
    Продолжение текста 1"

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

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

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

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

    Использование аргументов с #define

    Во избежание ошибок при вычислении выражений параметры макроопределения необходимо заключать в скобки.

    #define идентификатор1 (идентификатор2, . . .) строка

    Пример:

    #define abs(A) (((A) > 0)?(A) : -(A))

    Каждое вхождение выражения abs(arg) в тексте программы заменяется на

    ((arg) > 0) ? (arg) : -(arg),

    причем параметр макроопределения А заменяется на arg.

    Пример:

    #define nmem(P,N)\
    (P) -> p_mem[N].u_long

    Символ \ продолжает макроопределение на вторую строчку. Это макроопределение уменьшает сложность выражения, описывающего массив объединений внутри структуры.

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

    /* макроопределение с аргументами */ 
    #define SQUARE(x) x*x
    #define PR(x) printf("x равно %d.\n", x)
    int 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(SQUARE(++x));
    	return 0;
    }

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

    z равно 16.
    z равно 4.
    SQUARE(x) равно 16.
    SQUARE(x+2) равно 14.  
    100/SQUARE(2) равно 100.
    SQUARE(++x) равно 36.

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

    PR(SQUARE(x));

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

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

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

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

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

    SQUARE(x) равно x*x.

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

    Таким образом,

    x*x становится x+2*x+2

    Если x равно 4, то получается

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

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

    Макроопределение или функция?

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

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

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

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

    Запомним!

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

    Включение файла: #include

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

    assert.h - диагностика программ

    ctype.h - преобразование и проверка символов

    errno.h - проверка ошибок

    float.h - работа с вещественными данными

    limits.h - предельные значения целочисленных данных

    locale.h - поддержка национальной среды

    math.h - математические вычисления

    setjump.h - возможности нелокальных переходов

    signal.h - обработка исключительных ситуаций

    stdarg.h - поддержка переменного числа параметров

    stddef.h - дополнительные определения

    stdio.h - средства ввода-вывода

    stdlib.h - функции общего назначения (работа с памятью)

    string.h - работа со строками символов

    time.h - определение дат и времени

    В конкретных реализациях количество и наименование заголовочных файлов могут быть и другими. Например, в компиляторах для MS-DOS активно используются заголовочные файлы mem.h, alloc.h, conio.h, dos.h и другие. В компиляторах Turbo C, Borland C++ для связи с графической библиотекой применяется заголовочный файл graphics.h.

    Командная строка #include может встречаться в любом месте программы, но обычно все включения размещаются в начале файла исходного текста.

    #include <имя_файла>

    Пример:

    #include <math.h>

    Препроцессор заменяет эту строку содержимым файла math.h. Угловые скобки означают, что файл math.h будет взят из некоторого стандартного каталога (обычно это /usr/include ). Текущий каталог не просматривается:

    #include "имя_файла"

    Пример:

    #include "ABC"

    Препроцессор заменяет эту строку содержимым файла ABC. Так как имя файла заключено в кавычки, то поиск производится в текущем каталоге (в котором содержится основной файл исходного текста). Если в текущем каталоге данного файла нет, то поиск производится в каталогах, определенных именем пути в опции -l препроцессора. Если и там нет файла, то просматривается стандартный каталог.

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

    В конкретных реализациях количество и наименования заголовочных файлов могут быть разными:

    #include <stdio.h>	ищет в системном каталоге
    #include "my.h"         ищет в текущем рабочем 
                            каталоге
    #include "/user/1/my.h"	ищет в каталоге /user/1

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

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

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

    Некоторые файлы включены в систему, например, stdio.h, но можно создать и свой файл.

    Многие программисты разрабатывают свои стандартные заголовочные файлы, чтобы использовать их в программах.

    Условная компиляция

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

    #if константное_выражение

    Пример:

    #if ABC + 3

    Истина, если константное выражение ABC + 3 не равно нулю.

    #ifdef идентификатор

    Пример:

    #ifdef ABC

    истина, если идентификатор ABC определен ранее командой #define.

    #ifndef идентификатор

    Пример:

    #ifndef ABC

    истина, если идентификатор ABC не определен в настоящий момент.

    #else
     . . .
    #endif

    Если предшествующие проверки #if, #ifdef или #ifndef дают значение "Истина", то строки от #else до #endif игнорируются при компиляции.

    Если эти проверки дают значение "Ложь", то строчки от проверки до #else (а при отсутствии #else - до #endif ) игнорируются.

    Команда #endif обозначает конец условной компиляции.

    Пример:

    #ifdef DEBUG
    fprintf (stderr, "location: x = %d\n", x);
    #endif

    Вспомогательные директивы

    Номер строки и имя файла

    #line целая_константа "имя_файла"

    Пример:

    #line 20 "ABC"

    Препроцессор изменяет номер текущей строки и имя компилируемого файла. Имя файла может быть опущено.

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

    Пример:

    #define N 3/*определение константы */
    void main( )
    {
    #line 55  "file.c"
    double x[3*N];
    }

    Реакция на ошибки

    #error последовательность лексем

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

    Пример:

    #define NAME 15

    В дальнейшем можно проверить ее значение и выдать сообщение, если у NAME окажется другое значение:

    #if (NAME !=15)
    #error NAME должно быть равно 15!

    Сообщение будет выглядеть так:

    error <имя_файла><номер_строки >;  
    error directive: NAME должно быть равно 15!

    Пустая директива

    #

    Использование этой директивы не вызывает никаких действий.

    Прагмы

    #pragma

    Эта директива определяет действия, зависящие от конкретной реализации компилятора. Например в некоторые компиляторы входит вариант этой директивы для извещения компилятора о наличии в тексте программы команд на языке Ассемблер. Возможности команды #pragma могут быть разнообазными. Стандарта для них не существует. Если конкретный препроцессор встречает прагму, которая ему неизвестна, он ее просто игнорирует как пустую директиву. В некоторых реализациях включена прагма.

    #pragma pack(n), где n= 1, 2, 4. Прагма pack позволяет влиять на упаковку смежных элементов в структурах и объединениях (см. лекцию 14).

    Соглашение может быть таким:

    pack(1) - выравнивание элементов по границам байтов;

    pack(2) - выравнивание элементов по границам слов;

    pack(4) - выравнивание элементов по границам двойных слов;

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

    Встроенные макроимена

    Существуют встроенные (заранее определенные) макроимена, доступные препроцессору во время обработки. Они позволяют получить следующую информацию:

    __DATE__ - строка символов в формате: "месяц число год", определяющая дату начала обработки исходного файла. Например, после препроцессорной обработки текста программы, выполненной 29 января 2005 года, оператор

    printf(__DATE__);

    станет таким

    printf("%s", "January 29 2005");

    __LINE__ - десятичная константа - номер текущей обрабатываемой строки файла с программой на Си. Принято, что номер первой строки исходного файла равен 1 ;

    __FILE__ - строка символов - имя компилируемого файла. Имя изменяется всякий раз, когда препроцессор встречает директиву #include с указанием имени другого файла. Когда включения файла по команде #include завершаются, востанавливается предыдущее значение макроимени __FILE__ ;

    __TIME__ - строка символов вида "часы:минуты:секунды", определяющая время начала обработки препроцессором исходного файла;

    __STDC__ - константа, равная 1, если компилятор работает в соответствии с ANSI-стандартом. В противном случае значение микроимени __STDC__ не определено. Стандарт языка Си предполагает, что наличие имени __STDC__ определяется реализацией, так как макрос __STDC__ относится к нововведениям стандарта. В конкретных реализациях набор предопределенных имен гораздо шире. Для получения более полных сведений о предопределенных препроцессорных именах следует обращаться к документации по конкретному компилятору.

    Страницы:

    Общие сведения

    В интегрированную среду подготовки программ на Си или в компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора - обработка исходного текста программы до ее компиляции. Препроцессорная обработка включает несколько стадий, выполняемых последовательно. Конкретная реализация может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись в следующем порядке:

  • Все системно-зависимые обозначения перекодируются в стандартные коды.
  • Каждая пара из символов ' \ ' и "конец строки" вместе с пробелами между ними убираются, и тем самым следующая строка исходного текста присоединяется к строке, в которой находилась эта пара символов.
  • В тексте распознаются директивы и лексемы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка.
  • Выполняются директивы препроцессора и производятся макроподстановки.
  • Эскейп-последовательности в символьных константах и символьных строках заменяются на их эквиваленты.
  • Смежные символьные строки конкатенируются, то есть соединяются в одну строку.
  • Каждая препроцессорная лексема преобразуется в текст на языке Си.
  • Поясним, что понимается под препроцессорными лексемами или лексемами препроцессора. К ним относятся символьные константы, имена включаемых файлов, идентификаторы, знаки операций, препроцессорные числа, знаки препинания, строковые константы и любые символы, отличные от пробела.

    Стадия обработки директив препроцессора. При ее выполнении возможны следующие действия:

  • замена идентификаторов заранее подготовленными последовательностями символов;
  • включение в программу текстов из указанных файлов;
  • исключение из программы отдельных частей ее текста, условная компиляция;
  • макроподстановка, то есть замена обозначения параметризованным текстом, формируемым препроцессором с учетом конкретных аргументов.
  • Символические константы: #define

    Если в качестве первого символа в строке программы используется символ #, то эта строка является командной строкой препроцессора (макропроцессора). Командная строка препроцессора заканчивается символом перевода на новую строку. Если непосредственно перед концом строки поставить символ обратной косой черты " \ ", то командная строка будет продолжена на следующую строку программы.

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

    Замена идентификаторов

    #define идентификатор строка

    Пример:

    #define ABC 100

    Заменяет каждое вхождение идентификатора ABC в тексте программы на 100:

    #undef идентификатор

    Пример:

    #undef ABC

    Отменяет предыдущее определение для идентификатора ABC.

    Пример:

    /* Простые примеры директивы препроцессора */
    #define TWO 2 /* можно использовать комментарии*/
    #define MSG "Текст 1.\
    Продолжение текста 1"
    /* обратная косая черта продолжает определение на следующую строку */
    #define FOUR TWO*TWO
    #define PX printf("X равен %d.\n", x)
    #define FMT "X равен %d.\n"
    int main( )
    {
    	int x = TWO;
    	PX;
    	x = FOUR;
    	printf(FMT,x);
    	printf("%s\n",MSG);
    	printf("TWO: MSG\n");
    	return TWO;
    }

    В результате выполнения нашего примера будем иметь:

    X равен 2
    X равен 4
    Текст 1. Продолжение текста 1
    TWO: MSG

    Разберем, что произошло. Оператор

    int x = TWO;

    превращается в

    int x = 2;

    Затем оператор

    PX;

    превращается в

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

    поскольку сделана полная замена. Теперь мы видим, что макроопределение может представлять любую строку, даже целое выражение на языке Си. Заметим, что это константная строка. PX напечатает только переменную, названную x.

    В следующей строке выполняется следующее:

    x = FOUR;

    превращается

    x = TWO*TWO;

    превращается в

    x = 2*2;

    и на этом все заканчивается. Фактическое умножение имеет место не во время работы препроцессора и не при компиляции, а всегда без исключения при работе программы (Уточнение: это зависит от конкретного компилятора). Препроцессор не выполняет вычислений. Он только очень точно делает предложенные подстановки. Заметим, что макроопределение может включать другие определения. Некоторые компиляторы не поддерживают это свойство вложения. В следующей строке

    printf(FMT,x);

    превращается в

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

    когда FMT заменяется соответствующей строкой. Этот подход может оказаться очень удобным, если есть длинная строка, которую мы используем несколько раз. В следующей строке программы MSG заменяется соответствующей строкой. Кавычки делают замещающую строку константой символьной строки. Поскольку программа получает ее содержимое, эта строка будет запоминаться в массиве, заканчивающемся нуль-символом. Так,

    #define HAL 'X' определяет символьную константу, а 
    #define HAR "X" определяет строковую строку X\0

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

    printf("TWO: MSG");

    печатает буквально TWO: MSG вместо печати следующего текста:

    2: "Текст 1.
    Продолжение текста 1"

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

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

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

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

    Использование аргументов с #define

    Во избежание ошибок при вычислении выражений параметры макроопределения необходимо заключать в скобки.

    #define идентификатор1 (идентификатор2, . . .) строка

    Пример:

    #define abs(A) (((A) > 0)?(A) : -(A))

    Каждое вхождение выражения abs(arg) в тексте программы заменяется на

    ((arg) > 0) ? (arg) : -(arg),

    причем параметр макроопределения А заменяется на arg.

    Пример:

    #define nmem(P,N)\
    (P) -> p_mem[N].u_long

    Символ \ продолжает макроопределение на вторую строчку. Это макроопределение уменьшает сложность выражения, описывающего массив объединений внутри структуры.

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

    /* макроопределение с аргументами */ 
    #define SQUARE(x) x*x
    #define PR(x) printf("x равно %d.\n", x)
    int 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(SQUARE(++x));
    	return 0;
    }

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

    z равно 16.
    z равно 4.
    SQUARE(x) равно 16.
    SQUARE(x+2) равно 14.  
    100/SQUARE(2) равно 100.
    SQUARE(++x) равно 36.

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

    PR(SQUARE(x));

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

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

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

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

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

    SQUARE(x) равно x*x.

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

    Таким образом,

    x*x становится x+2*x+2

    Если x равно 4, то получается

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

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

    Макроопределение или функция?

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

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

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

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

    Запомним!

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

    Включение файла: #include

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

    assert.h - диагностика программ

    ctype.h - преобразование и проверка символов

    errno.h - проверка ошибок

    float.h - работа с вещественными данными

    limits.h - предельные значения целочисленных данных

    locale.h - поддержка национальной среды

    math.h - математические вычисления

    setjump.h - возможности нелокальных переходов

    signal.h - обработка исключительных ситуаций

    stdarg.h - поддержка переменного числа параметров

    stddef.h - дополнительные определения

    stdio.h - средства ввода-вывода

    stdlib.h - функции общего назначения (работа с памятью)

    string.h - работа со строками символов

    time.h - определение дат и времени

    В конкретных реализациях количество и наименование заголовочных файлов могут быть и другими. Например, в компиляторах для MS-DOS активно используются заголовочные файлы mem.h, alloc.h, conio.h, dos.h и другие. В компиляторах Turbo C, Borland C++ для связи с графической библиотекой применяется заголовочный файл graphics.h.

    Командная строка #include может встречаться в любом месте программы, но обычно все включения размещаются в начале файла исходного текста.

    #include <имя_файла>

    Пример:

    #include <math.h>

    Препроцессор заменяет эту строку содержимым файла math.h. Угловые скобки означают, что файл math.h будет взят из некоторого стандартного каталога (обычно это /usr/include ). Текущий каталог не просматривается:

    #include "имя_файла"

    Пример:

    #include "ABC"

    Препроцессор заменяет эту строку содержимым файла ABC. Так как имя файла заключено в кавычки, то поиск производится в текущем каталоге (в котором содержится основной файл исходного текста). Если в текущем каталоге данного файла нет, то поиск производится в каталогах, определенных именем пути в опции -l препроцессора. Если и там нет файла, то просматривается стандартный каталог.

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

    В конкретных реализациях количество и наименования заголовочных файлов могут быть разными:

    #include <stdio.h>	ищет в системном каталоге
    #include "my.h"         ищет в текущем рабочем 
                            каталоге
    #include "/user/1/my.h"	ищет в каталоге /user/1

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

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

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

    Некоторые файлы включены в систему, например, stdio.h, но можно создать и свой файл.

    Многие программисты разрабатывают свои стандартные заголовочные файлы, чтобы использовать их в программах.

    Условная компиляция

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

    #if константное_выражение

    Пример:

    #if ABC + 3

    Истина, если константное выражение ABC + 3 не равно нулю.

    #ifdef идентификатор

    Пример:

    #ifdef ABC

    истина, если идентификатор ABC определен ранее командой #define.

    #ifndef идентификатор

    Пример:

    #ifndef ABC

    истина, если идентификатор ABC не определен в настоящий момент.

    #else
     . . .
    #endif

    Если предшествующие проверки #if, #ifdef или #ifndef дают значение "Истина", то строки от #else до #endif игнорируются при компиляции.

    Если эти проверки дают значение "Ложь", то строчки от проверки до #else (а при отсутствии #else - до #endif ) игнорируются.

    Команда #endif обозначает конец условной компиляции.

    Пример:

    #ifdef DEBUG
    fprintf (stderr, "location: x = %d\n", x);
    #endif

    Вспомогательные директивы

    Номер строки и имя файла

    #line целая_константа "имя_файла"

    Пример:

    #line 20 "ABC"

    Препроцессор изменяет номер текущей строки и имя компилируемого файла. Имя файла может быть опущено.

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

    Пример:

    #define N 3/*определение константы */
    void main( )
    {
    #line 55  "file.c"
    double x[3*N];
    }

    Реакция на ошибки

    #error последовательность лексем

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

    Пример:

    #define NAME 15

    В дальнейшем можно проверить ее значение и выдать сообщение, если у NAME окажется другое значение:

    #if (NAME !=15)
    #error NAME должно быть равно 15!

    Сообщение будет выглядеть так:

    error <имя_файла><номер_строки >;  
    error directive: NAME должно быть равно 15!

    Пустая директива

    #

    Использование этой директивы не вызывает никаких действий.

    Прагмы

    #pragma

    Эта директива определяет действия, зависящие от конкретной реализации компилятора. Например в некоторые компиляторы входит вариант этой директивы для извещения компилятора о наличии в тексте программы команд на языке Ассемблер. Возможности команды #pragma могут быть разнообазными. Стандарта для них не существует. Если конкретный препроцессор встречает прагму, которая ему неизвестна, он ее просто игнорирует как пустую директиву. В некоторых реализациях включена прагма.

    #pragma pack(n), где n= 1, 2, 4. Прагма pack позволяет влиять на упаковку смежных элементов в структурах и объединениях (см. лекцию 14).

    Соглашение может быть таким:

    pack(1) - выравнивание элементов по границам байтов;

    pack(2) - выравнивание элементов по границам слов;

    pack(4) - выравнивание элементов по границам двойных слов;

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

    Встроенные макроимена

    Существуют встроенные (заранее определенные) макроимена, доступные препроцессору во время обработки. Они позволяют получить следующую информацию:

    __DATE__ - строка символов в формате: "месяц число год", определяющая дату начала обработки исходного файла. Например, после препроцессорной обработки текста программы, выполненной 29 января 2005 года, оператор

    printf(__DATE__);

    станет таким

    printf("%s", "January 29 2005");

    __LINE__ - десятичная константа - номер текущей обрабатываемой строки файла с программой на Си. Принято, что номер первой строки исходного файла равен 1 ;

    __FILE__ - строка символов - имя компилируемого файла. Имя изменяется всякий раз, когда препроцессор встречает директиву #include с указанием имени другого файла. Когда включения файла по команде #include завершаются, востанавливается предыдущее значение макроимени __FILE__ ;

    __TIME__ - строка символов вида "часы:минуты:секунды", определяющая время начала обработки препроцессором исходного файла;

    __STDC__ - константа, равная 1, если компилятор работает в соответствии с ANSI-стандартом. В противном случае значение микроимени __STDC__ не определено. Стандарт языка Си предполагает, что наличие имени __STDC__ определяется реализацией, так как макрос __STDC__ относится к нововведениям стандарта. В конкретных реализациях набор предопределенных имен гораздо шире. Для получения более полных сведений о предопределенных препроцессорных именах следует обращаться к документации по конкретному компилятору.

    Вернуться к учебному плану