Шаг 77.
Примеры использования директив препроцессора

    На этом шаге мы проиллюстрируем некоторые применения директив препроцессора.


    Пример 1. Найти большее из двух чисел.
#include <iostream.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
void main ()
{
      int x,y;
      cout << "Задайте 2 целых числа: ";
      cin >> x >> y;
      cout << "Большее из них: " << MAX(x,y);
}
Текст этой программы можно взять здесь.


    Пример 2. Использование вложенных макроимен.
#include <iostream.h>
#include <stdio.h>
#define PR(format,value) printf(format,value)
#define NL  putchar('\n')
#define PRINT1(f,x1) PR(f,x1),NL
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
void main ()
{
      int x,y;
      cout << "Задайте 2 целых числа: ";
      cin >> x >> y;
      PRINT1 ("%d",MAX(x,y));
}
Текст этой программы можно взять здесь.


    Пример 3.
#include <iostream.h>
#define S(x) x*x
#define P(x) cout << "x равен " << x << endl
void main ()
{
     int x=4;
     int z = S(x); P(z); z = S(2);
     P(z); P(S(x)); P(S(x+2)); P(100/S(2)); P(S(++x));
}
Текст этой программы можно взять здесь.

    Результат работы программы:

   x равен 16
   x равен 4
   x равен 16
   x равен 14
   x равен 100
   x равен 36


    Пример 4.
#include <iostream.h>
#define FUDGE(k) k+3
#define PR(a) cout << (int)(a) << "\t" 
#define PRINT(a) PR(a); cout << endl
#define PRINT2(a,b) PR(a); PRINT(b)
#define PRINT3(a,b,c) PR(a); PRINT2(b,c)
#define MAX(a,b) (a < b ? b:a)
void main()
{
     {
       int x = 2;
       x = x * FUDGE(x); PRINT (x);   /* Препроцессор 1 */
     }
     {
       int cel;
       for (cel=0; cel<=100; cel+=50)
          PRINT2 (cel,9/5*cel+32);    /* Препроцессор 2 */
     }
     {
        int x,y;
        x = 1; y = 2;
        PRINT3 (MAX(x++,y),x,y);
        PRINT3 (MAX(x++,y),x,y);      /* Препроцессор 3 */
     }
}
Текст этой программы можно взять здесь.

    Результаты работы программы:

   7
   0      50      100     182
   2      2       2
   3      4       2

    Комментарии.

    Препроцессор 1.

   int x = 2;
   Переменная x получает значение 2.
   x = x * FUDGE (x);  т.е. x = 2 * FUDGE (2);

    Чтобы понять эффект макроподстановки, нужно "провести" ее в месте вызова. Вместо FUDGE(2) подставим 2+3 (без скобок!). Получится x = 2 * 2 + 3;. Заменяя формальный параметр на фактический, получаем неожиданный результат. Сначала умножаем, затем складываем.

   PRINT (x);
   Производится макроподстановка:
   PR (x); cout << endl
   Опять производится макроподстановка, на этот раз PR.
   cout << (int)(x) << "\t"; cout << endl


    Внимание! Макроподстановки могут быть источником трудноуловимых ошибок, т.к. макроподстановка - это лишь замена одних строк другими, а препроцессор не знает языка C++. Поэтому многих неожиданных результатов можно избежать, если строго следовать нескольким правилам.


    Правило 1. Заключайте в скобки строки-подстановки, если они содержат операции.

    Нежелательного взаимодействия между строкой-подстановкой и контекстом в приведенной выше задаче могло бы и не быть, если бы FUDGE(k) определялось как (k+3).

    Препроцессор 2.

   for (cel=0; cel<=100; cel+=50)
      PRINT2 (cel,9/5*cel+32);

   for (cel=0; cel<=100; cel+=50)   Вначале производим макропод-
      PR (cel);                     становку PRINT2.
   PRINT (9/5*cel+32);

   for (cel=0; cel<=100; cel+=50)   Затем  производим  макропод-
      cout << (int)(cel) << "\t";   становку PR.
   PRINT (9/5*cel+32);

   for (cel=0; cel<=100; cel+=50)   Производим  макроподстановку
      cout << (int)(cel) << "\t";   PRINT.
   PR (9/5*cel+32); cout << endl;

   for (cel=0; cel<=100; cel+=50)   Производим  макроподстановку
      cout << (int)(cel) << "\t";   PR.
   cout << (int)(9/5*cel+32) << "\t";
   cout << endl;

    Обращение к PRINT2 выглядит как один оператор, но после макроподстановки появляются три. Только первое обращение к PR происходит после выполнения цикла for со значением cel = 150.


    Правило 2. Не давайте "расползаться" макроподстановке; лучше использовать выражение, а не оператор, и не несколько операторов, а один-единственный. .

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

    Препроцессор 3.

   int x=1,y=2;
   PRINT3 (MAX(x++,y),x,y);

    Макроподстановка PRINT3 происходит прежде MAX. Однако, чтобы не затемнять сути, в этой и последующей задачах подстановка PRINT не будет производиться. Тогда первый шаг состоит в подстановке строки вместо MAX.

   (x++ < y ? y: x++),x,y  Затем заменяем  аргументы макроподста-
                           новки на фактические.
   (1 < 2 ? y: x++)        Наконец производим вычисления.
                           Ответ: 2 (т.е. y), а x получает значе-
                           ние 2 в результате операции x++.
   PRINT3(MAX(x++,y),x,y); Теперь  выполняем  второе  обращение к
                            PRINT3.
   (x++ < y ? y : x++),x,y
   (2 < 2 ? y : x++)      Ответ: 3 (т.е. x++), а x  получает зна-
                          чение 4 в результате двух операций x++.

    При обращении x++ появляется только один раз, но в расширении фигурирует уже два раза, что приводит к увеличению x иногда на 1 и иногда на 2. Тяжесть ответственности за защиту от таких нежелательных побочных эффектов можно возлагать или на того, кто определяет макроподстановку, или на того, кто ею пользуется.


    Правило 3. Избегайте макроподстановок, которые могут привести к непредсказуемым или несообразным побочным эффектам. .

   


    Правило 3A. В обращении к макроподстановкам избегайте выражений, содержащих побочные эффекты. .

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

    В данной задаче следование правилу 3А не затронет саму MAX.


    Пример 5.
#include <iostream.h>
#define NEG(a) -a
#define weeks(mins) (days(mins)/7)
#define days(mins) (hours(mins)/24)
#define hours(mins) (mins/60)
#define mins(secs) (secs/60)
#define TAB(c,i,oi,t) if(c=='\t')\
                        for(t=8-(i-oi-1)%8,oi=i;t;t--)\
                          cout << " "
#define PR(a) cout << (int)(a) << "\t"
#define PRINT(a) PR(a); cout << endl
void  main ()
{
     {
        int x=1;
        PRINT(-NEG(x));               /* Препроцессор 1 */
     }
     {
        PRINT (weeks(10080));
        PRINT (days(mins(86400)));    /* Препроцессор 2 */
     }
     {
        static char input[]="\twhich\tif?";
        char c;
        int i,oldi,temp;
        /* ------------------------------------- */
        for  (oldi=-1,i=0; (c=input[i])!='\0'; i++)
           if  (c< ' ')
              TAB (c,i,oldi,temp);
           else  cout << c;
        cout << "#\n";               /* Препроцессор 3 */
        cout << "123456789ABCD\n";
     }
}
Текст этой программы можно взять здесь.

    Результаты работы программы:

   0
   1
   1
              #
   123456789ABCD

    Комментарии.

    Препроцессор 1.

   int x=1;
   PRINT(-NEG(x));  NEG(x) заменяется на -x, что приводит к появ-
                    лению оператора PRINT(--x);. Далее вычисляет-
                    ся значение выражения --x,  которое,  в  силу
                    инициализации x,  равно 0. Дальнейшее очевид-
                    но.

    Если, следуя правилу 1, определить NEG(a) как (-а), то получим иной результат (подумайте, какой?).

    Препроцессор 2.

   PRINT(weeks(10080));       Заменяем каждое  обращение на соот-
   PRINT(days(10080)/7);      ветствующую  строку.  Заметим,  что
                              здесь  нет никакого конфликта между
                              параметром mins и именем макроподс-
                              тановки mins.
   PRINT((hours(10080)/24)/7);
   PRINT(((10080/60)/24)/7);  Вычисляем.
   Аналогично:
   PRINT(days(mins(86400)));
   PRINT(hours(mins(86400))/24);
   PRINT((mins(86400)/60)/24);
   PRINT(((86400/60)/60)/24);  Вычисляем.

    Препроцессор 3.

   static char input[]="\twhich\tif?";
   if  (c< ' ')
      TAB (c,i,oldi,temp);
   else cout << c;
Выполняем макроподстановку:
   if  (c<' ')
      if  (c=='\t')
         for (temp=8-(i-oldi-1)%8,oldi=i;temp;temp--)
            cout << " ";
      else  cout << c;

    TAB включает "открытый" условный оператор (не содержащий части else), поэтому после подстановки он "поглощает" ближайшую else-часть.


    Правило 4. Подстановка макроопределения должна быть законченной конструкцией языка C++ - выражением, оператором (без последней точки с запятой) или блоком. .

    В предыдущей задаче трудность устраняется добавлением в определение TAB "пустой" else-части.


    О функциях и макроподстановках. Часто подпрограмму можно реализовать и с помощью функции, и с помощью макроподстановки. Преимущество использования макроподстановки в том, что программа работает быстрее, поскольку нет обращений к функциям. Кроме того, функция определена для данных того типа, который указан в спецификации ее параметров и возвращает значение только одного конкретного типа. Макрос пригоден для обработки параметров любого типа, допустимых в выражениях, формируемых при обработке строки замещения. Тип получаемого значения зависит только от типов параметров и от самих выражений. Таким образом, макрос может заменять несколько функций. С другой стороны, использование функции гарантирует, что не возникают всякие неприятные ситуации наподобие тех, которые рассматривались в предыдущих задачах. Кроме того, в случае функций, если они вызываются неоднократно, потребуется, вероятно, меньше памяти. Это подводит нас к заключительному правилу по использованиям макроподстановок.


    Правило 5. Макроопределение должно быть простым; если это не удается, используйте функцию. .


    На следующем шаге мы рассмотрим директиву включения файлов.


Предыдущий шаг Содержание Следующий шаг