netlib.narod.ru< Назад | Оглавление | Далее >

5.12. Сложные объявления

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

    int *f();    /*  f - функция, возвращающая указатель на int */
    int (*pf)(); /* pf - указатель на функцию, возвращающую int */

Приоритет префиксного оператора * ниже, чем приоритет скобок (), поэтому во втором случае скобки необходимы.

Хотя на практике по-настоящему сложные объявления встречаются редко, все же важно знать, как их понимать, а если потребуется, и как их конструировать.

Укажем хороший способ: объявления можно синтезировать, двигаясь небольшими шагами с помощью оператора typedef; этот способ рассмотрен в параграфе 6.7. В настоящем параграфе на примере двух программ, осуществляющих преобразования правильных Си-объявлений в соответствующие им словесные описания и обратно, мы демонстрируем иной способ конструирования объявлений.

Первая программа, dcl, — более сложная. Она преобразует Си-объявления в словесные описания так, как показано в следующих примерах:

    char **argv
        argv: pointer to pointer to char
    int (*daytab)[13]
        daytab: pointer to array [13] of int
    int *daytab[13]
        daytab: array [13] of pointer to int
    void *comp()
        comp: function return pointer to void
    void (*comp)()
        comp: pointer to function return void
    char (*(*x())[])()
        x: function return pointer to array [] of
           pointer to function return char
    char(*(*x[3])())[5]
        x: array [3] of pointer to function return
           pointer to array [5] of char

Функция dcl в своей работе использует грамматику, специфицирующую объявитель. Эта грамматика строго изложена в параграфе 8.5 приложения А, а в упрощенном виде записывается так:

объявитель:                  необязательные * непосредственный-объявитель
непосредственный-объявитель: имя
                             (объявитель)
                             непосредственный-объявитель()
                             непосредственный-объявитель[необяз. размер]

Говоря простым языком, объявитель есть непосредственный-объявитель, перед которым может стоять * (т.е. одна или несколько звездочек), где непосредственный-объявитель есть имя, или объявитель в скобках, или непосредственный-объявитель с последующей парой скобок, или непосредственный-объявитель с последующей парой квадратных скобок, внутри которых может быть помещен размер.

Эту грамматику можно использовать для грамматического разбора объявлений. Рассмотрим, например, такой объявитель:

    (*pfa[])()

Имя pfa будет классифицировано как имя и, следовательно, как непосредственный-объявитель. Затем pfa[] будет распознано как непосредственный-объявитель, а *pfa[] — как объявитель и, следовательно (*pfa[]) есть непосредственный-объявитель. Далее (*pfa[])() есть непосредственный-объявитель и, таким образом, объявитель. Этот грамматический разбор можно проиллюстрировать деревом разбора, приведенным на рисунке.


Рис. 14.

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

  /* dcl: разбор объявителя */
  void dcl(void)
  {
      int ns;

      for (ns = 0; gettoken() == '*';)  /* подсчет звездочек */
          ns++;
      dirdcl();
      while (ns-- > 0)
          strcat(out, " pointer to");
  }

  /* dirdcl: разбор непосредственного объявителя */
  void dirdcl(void)
  {
      int type;

      if (tokentype == '(') {    /* (объявитель) */
          dcl();
          if (tokentype != ')')
              printf("Ошибка: пропущена )\n");
      } else if (tokentype == NAME) /* имя переменной */
          strcpy(name, token);
      else
          printf("Ошибка: должно быть имя или (объявитель)\n");
      while ((type = gettoken()) == PARENS || type == BRACKETS)
          if (type == PARENS)
              strcat(out, " function return");
          else {
              strcat(out, " array");
              strcat(out, token);
              strcat(out, " of");
          }
  }

Приведенные программы служат только иллюстративным целям и не вполне надежны. Что касается dcl, то ее возможности существенно ограничены. Она может работать только с простыми типами вроде char и int и не справляется с типами аргументов в функциях и с квалификаторами вроде const. Лишние пробелы для нее опасны. Она не предпринимает никаких мер по выходу из ошибочной ситуации, и поэтому неправильные описания также ей противопоказаны. Устранение этих недостатков мы оставляем для упражнений.

Ниже приведены глобальные переменные и главная программа main.

  #include <stdio.h>
  #include <string.h>
  #include <ctype.h>

  #define MAXTOKEN 100

  enum {NAME, PARENS, BRACKETS};

  void dcl(void);
  void dirdcl(void);

  int gettoken(void);
  int tokentype;           /* тип последней лексемы */
  char token[MAXTOKEN];    /* текст последней лексемы */
  char name [MAXTOKEN];    /* имя */
  char datatype[MAXTOKEN]; /* тип = char, int и т.д. */
  char out[1000];          /* выдаваемый текст */

  /* Преобразование объявления в словесное описание */
  main()
  {
      while (gettoken() != EOF) {  /* 1-я лексема в строке */
          strcpy(datatype, token); /* это тип данных */
          out[0] = '\0';
          dcl();    /* разбор остальной части строки */
          if (tokentype != '\n')
              printf("Синтаксическая ошибка\n");
          printf("%s: %s %s\n", name, out, datatype);
      }
      return 0;
  }

Функция gettoken пропускает пробелы и табуляции и затем получает следующую лексему из ввода; лексема (token) — это имя, или пара круглых скобок, или пара квадратных скобок (может быть с помещенным в них числом), или любой другой единичный символ.

  int gettoken(void)   /* возвращает следующую лексему */
  {
      int c, getch(void);
      void ungetch(int);
      char *p = token;

      while ((c = getch()) == ' ' || c == '\t')
          ;
      if (c == '(') {
          if ((c = getch()) == ')') {
              strcpy(token, "()");
              return tokentype = PARENS;
          } else {
              ungetch(c);
              return tokentype = '(';
          }
      } else if (c == '[') {
          for (*p++ = c; (*p++ = getch()) != ']'; )
              ;
          *p = '\0';
          return tokentype = BRACKETS;
      } else if (isalpha(c)) {
          for (*P++ = c; isalnum(c = getch()); )
              *p++ = c;
          *p = '\0';
          ungetch(c);
          return tokentype = NAME;
      } else
          return tokentype = c;
  }

Функции getch и ungetch были рассмотрены в главе 4.

Обратное преобразование реализуется легче, особенно если не придавать значения тому, что будут генерироваться лишние скобки. Программа undcl превращает фразу вроде «x есть функция, возвращающая указатель на массив указателей на функции, возвращающие char»,которую мы будем представлять в виде

    x () * [] * () char

в объявление

    char (*(*x())[])()

Такой сокращенный входной синтаксис позволяет повторно использовать функцию gettoken. Функция undcl использует те же самые внешние переменные, что и dcl.

  /* undcl: преобразует описание в объявитель */
  main()
  {
      int type;
      char temp[MAXTOKEN];

      while (gettoken() != EOF) {
          strcpy(out, token);
          while ((type = gettoken()) != '\n');
              if (type == PARENS || type == BRACKETS)
                  strcat(out, token);
              else if (type = '*') {
                  sprintf(temp, "(*%s)", out);
                  strcpy(out, temp);
              } else if (type == NAME) {
                  sprintf(temp, "%s %s", token, out);
                  strcpy(out, temp);
              } else
                  printf("Неверный элемент %s в фразе\n", token);
          print("%s\n", out);
      }
      return 0;
  }

Упражнение 5-18


Видоизмените программу dcl таким образом, чтобы она обрабатывала ошибки во входной информации.



Упражнение 5-19


Модифицируйте программу undcl так, чтобы она не генерировала лишних скобок.



Упражнение 5-20


Расширьте возможности программы dcl, чтобы она обрабатывала объявления с типами аргументов функции, квалификаторами вроле const и т.п.



netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz