АВТОМАТИЗАЦИЯ ОБРАБОТКИ ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ
             В ПРИКЛАДНЫХ ПРОГРАММАХ НА ЯЗЫКАХ С и С++
                                  
                            Р.Н.Шакиров
          
     Значительная доля ошибок в современных программах связана
с   неправильной  обработкой  исключительных   ситуаций  (ИС),
связанных  с разного  рода  отклонениями  от  нормального хода
выполнения  программы.  Чтобы  сократить  число  таких ошибок,
применяется   автоматизированная  обработка   ИС,  методика  и
средства   которой   рассматриваются   на  примере  библиотеки
поддержки прикладного программирования EXBASE.
     Библиотека    EXBASE    предназначена    для   разработки
высокопроизводительных  16  и 32-разрядных прикладных программ
для платформ DOS и Windows на языках С и С++. Она обеспечивает
такие возможности,  как ввод-вывод файлов с обработкой ошибок,
строковые  операции  с защитой памяти,  распределение памяти с
контролем переполнения  разрядной  сетки  индекса, многомерные
динамические  массивы,   ускоренный  грамматический  анализ  и
синтез текстов с табличной и скобочной структурой.
     
     1. Введение
     
     Язык  С  первоначально  создавался  для  целей системного
программирования,  но практика его применения уже  давно вышла
за пределы этой относительно узкой области.
     Применение языков С и C++  в  прикладном программировании
обусловлено  возможностью  глубокой  оптимизации  программного
кода,  а также широким выбором трансляторов  и вспомогательных
библиотек.  Но  эти  преимущества  достигаются  ценой нехватки
средств, предохраняющих программиста от совершения ошибок.
     Рассмотрим  простейшую  программу  копирования  файлов  и
приведем два варианта: один с ошибками, другой - без них.
     
#include <stdio.h>
void    main    (void)          // Этот вариант программы
{                               // содержит 7 ошибок,
  FILE *f1, *f2;                // но работает!
  char *s1, ch;
  printf ("Введите имя файла:\n");
  gets (s1);                    // Вводим имя файла.
  f1 = fopen (s1,"r");          // Открываем файл.
  f2 = fopen ("$","w");         // Открываем копию.
  while ((ch = getc(f1))!=EOF) putc (ch, f2); // Копируем.
  fclose (f1);                  // Закрываем файл.
  fclose (f2);                  // Закрываем копию.
}
            Листинг 1. Программа копирования файла
                               
#include <stdio.h>
#include <string.h>
void    main    (void)          // В этом варианте программы
{                               // ошибок, надо полагать, нет.
  FILE *f1, *f2;                // Он тоже работает.
  char s [256];
  int ch;
  printf ("Введите имя файла:\n");
  fgets (s, sizeof (s), stdin);
  if (s [strlen (s) - 1] ='\n') s [strlen (s) - 1] = 0;
  f1 = fopen (s,"r");
  if (!f1) { printf ("Can't read %s\n",s); return; }
  f2 = fopen ("$","w");
  if (!f2) { printf ("Can't write $\n"); fclose (f1); return; }
  while ((ch = getc(f1))!=EOF) putc (ch, f2);
  fclose (f1); if (ferror(f1)) printf ("Read error\n");
  fclose (f2); if (ferror(f2)) printf ("Write error\n");
}
            Листинг 2. Программа копирования файла
                               
     Прокомментируем  листинги.  В  хорошо  продуманном  языке
программирования  программа  без  ошибок  бывает   понятной  и
красивой,  а программа с ошибками -  запутанной и безобразной.
Здесь  же  дело обстоит с  точностью до наоборот.  Если даже в
простейшей программе на Листинге  1  можно допустить так много
скрытых  ошибок,  то сколько   ошибок  должно  быть в реальных
программах, содержащих  сотни тысяч строк программного  кода?
     Пользователи  коммерческих программ уже давно  привыкли к
тому,  что эти программы работают   не всегда.  А разработчики
вместо  написания  корректных  программ   практикуют  методику
тестирования    некорректных    программ.   При   тестировании
устраняются  только те ошибки,  которые проявляются регулярно.
Если же программа может выдержать хотя бы один час интенсивной
работы, то она считается пригодной к употреблению.
     Основная   доля  ошибок  в   нашем   примере   связана  с
отсутствием  или   неправильной  обработкой   ИС.  Аналогичная
картина наблюдается и в реальных программах   - ИС возможны во
многих точках программы,  но возникают редко и потому с трудом
поддаются  тестированию.  Поэтому ошибки,  связанные с ИС, при
тестировании обычно остаются невыявленными.
     Принципиальные   трудности   возникают   и   при  попытке
применить  к   обработке  ИС   формальные  методы  верификации
программ,  описанные в [1,2]. Программы с обработкой ИС обычно
имеют  сложную   логику  с   множеством   условных  переходов.
Доказательство   корректности   такой   программы,  полученное
формальными методами, оказывается громоздким.
     Единственный   способ,   позволяющий  справиться   с  ИС,
заключается  в  автоматизации  их  обработки.  Для  этой  цели
предложены различные методики, в том числе механизм исключений
[3-5].   Однако  возможности  применения   этих  этих  методик
ограничены тем,  что они  не  распространяются  на стандартные
библиотечные  функции,  унаследованные  из  первых  реализаций
языков С и С++. Разумеется, вместо стандартных библиотек можно
воспользоваться альтернативными,  но  лишь немногие прикладные
программисты   смогут    выбрать   библиотеку    осознанно   и
квалифицированно.
     Более  последовательно,  чем в С++,  обработка исключений
реализована в языке Java  [4].  Однако  и  этот  язык обладает
принципиальными  недостатками.  Вызывает недоумение, например,
отсутствие  динамических  массивов  с  переменными  размерами.
Кроме  того,  из-за  отсутствия  эффективных  реализаций сфера
применения Java в прикладном программировании ограничена.
     Таким  образом,   современное  состояние  дел  в  области
обработки ИС  следует  признать  неудовлетворительным. Главная
причина такого положения заключается не в недостатке требуемых
инструментальных  средств -  они  имеются  в  изобилии  -  а в
отсутствии  ясной  и  эффективной  методики  их использования,
пригодной для массового применения.

     2. Концепция обработки ИС
     
     Основным  способом  обработки  ИС   считается  прерывание
нормального хода выполнения программы.  Чтобы не применять для
этой цели условные  операторы,  в программу вводятся операторы
перехвата,  запуска  и  обработки  исключений  [3,4]. Оператор
перехвата   задает   блок   программы,   выполнение   которого
прекращается при запуске исключений, после чего, в зависимости
от  типа исключения,  управление передается на соответствующий
блок обработки.  Если  оператор  перехвата не  задан, то любое
исключение  приводит  в  прекращению  работы  программы. Таким
образом,   механизм   исключений  позволяет   описать  процесс
аварийного завершения как программы  в целом,   так и любой ее
составной части.
     При  обработке  исключения  следует  восстановить рабочий
контекст программы,  требуемый для ее последующего выполнения.
Например,  часто требуется  освободить  ресурсы,  выделенные в
блоке  до  возникновения  исключения,  иначе  возникают хорошо
известные  программистам  и  пользователям   проблемы  "Утечки
памяти",  "Общей  ошибки  защиты"  и  т.п.  Если  освобождение
ресурсов   делается   вручную,   то   возникает  необходимость
дублирования  операторов,  выполняющих  освобождение  ресурсов
(пример на Листинге 3).
     
     int *p;
     try {              // Перехват исключения.
       ...
       *p = new int [n];
       ...
       throw "Error";   // Запуск исключения.
       ...
       delete p;        // Освобождение памяти.
     }
     catch (...)        // Обработка исключения.
     {
       delete p;        // Освобождение памяти.
       throw "Error";   // Повторный запуск исключения.
     }
         Листинг 3. Проблема утечки памяти в языке С++
     Механизм конструкторов и деструкторов языка С++ позволяет
автоматизировать   освобождение   ресурсов,   но   ради  этого
программист  должен  отказаться  от  свободного  использования
указателей,  операторов new,  delete,  функций  Windows  API и
многих других "радостей" языка C++. Более продвинутый механизм
автоматической  сборки  "мусора"   в  языке  Java  освобождает
оперативную  память,  но  вовремя  не  выполняет  такие важные
операции, как закрытие файлов [3].
     Из-за несовершенства  универсального  механизма обработки
исключений   создаются    проблемно-ориентированные   методики
обработки  ИС.   В   каждой  проблемной   области  применяется
определенный набор библиотечных  функций,  выполняющих типовые
операции.   При   возникновении   ИС   каждая   функция  может
реагировать одним из следующих трех способов:
     1) Возбудить исключение. Если программист не предусмотрит
        перехват  исключения   с  восстановлением  нормального
        состояния   программы,    то    выполнение   программы
        прекратится, что во многих случаях является адекватным
        исходом.
     2) Сформировать признак ИС и выполнить коррекцию ИС, т.е.
        компенсировать   исключительную   ситуацию   путем  ее
        преобразования в нормальную.    От программиста обычно
        не требуются какие-либо действия, т.к. после коррекции
        ИС выполнение  программы может продолжаться  в обычном
        режиме.  Тем  не менее,  он может предусмотреть анализ
        признака  ИС,  влияющий на  работу программы. Наиболее
        часто эта методика применяется при обработке  ошибок в
        исходных данных.
     3) Сформировать  признак ИС,  не  выполняя преобразование
        исключительной  ситуации   в  нормальную.  Программист
        обязан   предусмотреть   анализ   признака   ошибки  и
        корректирующие   действия,   иначе   ход   дальнейшего
        выполнения программы будет неопределенным.
     Наиболее   желательна  автоматическая  обработка   ИС  по
первому или второму способу,  а наименее  желательна  - ручная
обработка ИС по третьему способу.  Функции, реализующие третий
способ,  не только нагружают программиста лишней работой, но и
провоцируют его на совершение ошибок. Показательно, что именно
третий  способ характерен для стандартных библиотек  С  и С++.
Что  же  касается первого  и второго способов,  то выбор между
ними зависит от типа ИС.
     Первый способ  наиболее  эффективен в  тех случаях, когда
требуется просто  прекратить  выполнение  программы,  т.к. при
этом нет необходимости восстанавливать рабочий контекст.
     Второй    способ    позволяет   продолжить   естественное
выполнение  программы.   Однако  следует  учитывать,  что  при
автоматической коррекции ИС возможна потеря части информации в
рабочем контексте программы и  как  следствие  - возникновение
новых ИС. Поэтому программы, реализующие второй способ, должны
быть устойчивыми по отношению к различным ИС.
     На основе изложенного,  предложим следующую классификацию
ИС:
     1) Исключение.   Исключения  запускаются  при   некоторых
        случаях   неправильного   использования   библиотечных
        функций  (например,  при передаче  в  функцию нулевого
        указателя),  а  также как  стандартная  реакция  на ИС
        некоторых   других   типов.    Если   программист   не
        предуcмотрел   перехват   исключения,   то  выполнение
        программы прекратится.
     
     2) Критическая ошибка, делающая невозмозможным дальнейшее
        выполнение  программы  в  штатном  режиме. Критические
        ошибки возникают при  некоторых  случаях неправильного
        использования  библиотечных  функций,   а   также  при
        нехватке оперативной  памяти.  В  качестве стандартной
        реакции на критическую  ошибку запускается исключение.
        Программист может запретить   запуск  исключения, но в
        этом случае он обязан предусмотреть обработку ошибки с
        восстановлением штатного состояния программы.
     
     3) Корректируемая ошибка,  допускающая продолжение работы
        программы,   но  без  гарантии  получения  достоверных
        результатов.   Корректируемые  ошибки   возникают  при
        распределении   ресурсов  и   вводе-выводе  данных.  В
        качестве   стандартной   реакции   на   корректируемую
        ошибку запускается исключение.  По выбору программиста
        вместо исключения может быть  проведена автоматическая
        коррекция ошибки с выдачей  диагностического сообщения
        и установкой  признака ошибки.  После коррекции ошибки
        работа  программы  продолжается.  Чтобы  предотвратить
        возможное  искажение  выходных  данных,  все  операции
        файлового  ввода-вывода  блокируются до  тех пор, пока
        признак ошибки не будет сброшен.
     
     4) Ошибка  в  исходных  данных,  допускающая  продолжение
        работы программы.  Выдается предупреждающее сообщение,
        далее проводится автоматическая коррекция  ошибки, что
        позволяет   продолжать  работу  программы   в  штатном
        режиме.  Исключения не запускаются,  признак ошибки не
        используется.
     
     5) Особая   ситуация,   допускающая   продолжение  работы
        программы.  Особые  ситуации  обычно  возникают  из-за
        недостаточных  или избыточных  исходных данных. Особая
        ситуация  корректируется  автоматически, что позволяет
        продолжать   работу   программы   в   штатном  режиме.
        Сообщения  не  выдаются,  исключения  не  запускаются,
        признак ошибки не используется.
     
     Далее  мы   рассмотрим,   как   концепция   обработки  ИС
реализована в библиотеке EXBASE.
     
     
     3. Управление обработкой ИС
     
     Механизм  обработки  ИС,   предусмотренный  в  библиотеке
EXBASE,  не  предполагает  использование исключений  С++, если
только   это   явным   образом   не   затребовано   прикладным
программистом.  Такое  решение позволяет  содавать программный
код,  которые  могут  обрабатываться  различными компиляторами
яхыков C и С++,  в том числе и такими, которые не поддерживают
механизм исключений.
     Для  управления  обработкой   ИС   в   библиотеке  EXBASE
применяются  глобальные  функции  и  переменные.   Для  каждой
переменной указывается тип и начальное значение.
     
     
     3.1. Указатель функции запуска исключений.
     
     void (*_exthrow_handler) (void);
     
     Все функции библиотеки exbase выполняют запуск исключения
путем  вызова  функции  *_exthrow_handler.  Первоначально этому
указателю  присвоена  функция  _exthrow,  выполняющая  выход из
программы с кодом 2,  т.е. вызывающая exit(2). Для того, чтобы
вместо   этого  выполнялся  запуск   исключения  C++,  следует
присвоить данному указателю функцию exthrow:
     _exthrow_handler = exthrow;
     
     3.2. Запрет исключений при критических ошибках.
     
     int   excatch_fail;
     
     Если  excatch_fail  <= 0,  то  при  появлении критической
ошибки запускается исключение (см. п. 3.3).
     Если    excatch_fail    > 0,    то    при   возникновении
корректируемой    ошибки    проводится    установка   признака
корректируемой ошибки excatch (см.  п.3.4), после чего функция
EXBASE  выполняет автоматическую  коррекцию ошибки  так, чтобы
обеспечить продолжение работы программы.
     Начальное значение - 0.
     Изменение значения  excatch_fail  рекомендуется проводить
с помощью операторов excatch_fail++ и excatch_fail--.
     
     3.3. Запрет исключений при корректируемых ошибок.
     
     int excatch_error;
     
     Если excatch_error <= 0,  то при появлении корректируемой
ошибки запускается исключение (см. п. 3.3).
     Если    excatch_error > 0,     то    при    возникновении
корректируемой    ошибки    проводится    установка   признака
корректируемой ошибки excatch  (см. п.3.4), после чего функция
EXBASE выполняет автоматическую  коррекцию  ошибки  так, чтобы
обеспечить продолжение работы программы.
     Начальное значение - 0.
     Изменение значения excatch_error  рекомендуется проводить
с помощью операторов excatch_error++ и excatch_error--.
     
     3.4. Признак перехвата исключения при ошибке.
     
     int excatch;
     
     Ненулевое  значение  excatch  автоматически блокирует все
файловые  операции  и  операции  файлового  ввода-вывода через
функции  библиотеки  EXBASE. 
     Начальное  значение  -   0.
     Если excatch_error > 0,   то  при  корректируемой  ошибке
автоматически выполняется операция excatch |= 2.
     Если excatch_fail  > 0,  то при возникновении критической
ошибки автоматически выполняется операция excatch |= 2.
     
     3.5. Запрет корректируемой ошибки открытия файла.
     
     void exfind();
     
     Функция exfind воздействует только на следующую после нее
функцию,  выполняющую  открытие файла  или какую-нибудь другую
файловую  операцию.   При  отсутствии  входного  файла  вместо
обработки  корректируемой  ошибки  проводится  обработка особй
ситуации,  т.е.  имитируется  чтение пустого  файла без выдачи
каких-либо диагностических сообщений.

     4. Блочный ввод-вывод
     
     В  библиотеке  EXBASE  предусмотрены  следующие  основные
функции блочного ввода-вывода:
     
     int  exopenb   (pszFile);     // Двоичное чтение/запись
     int  exopenbr  (pszFile);     // Двоичное чтение
     int  exopenbw  (pszFile);     // Двоичная запись
     int  exopent   (pszFile);     // Текстовое чтение/запись
     int  exopentr  (pszFile);     // Текстовое чтение
     int  exopentw  (pszFile);     // Текстовая запись
     
     int  exread    (int fd, void *buf, int len); // Чтение.
     int  exwrite   (int fd, void *buf, int len); // Запись.
     long exlseek   (int fd, long pos, int from); // Позиция.
     
     void exclose   (int fd);      // Закрытие.
     
     Привила  использования этих  функций в целом  совпадают с
правилами  использования  аналогичных  функций  из стандартной
библиотеки io.h языка С. Отличия касаются обработки ИС.
     При  ошибке  ввода-вывода  запускаеться  исключение, если
excatch_error = 0, иначе выдается диагностика, устанавливается
глобальный признак ошибки excatch и проводится коррекция:
     - Функции  exopen...  выдают  дескриптор  fd < 0, который
       всеми остальными функциями воспринимается, как дескрип-
       тор  файла с нулевой длиной.  При  любых  операциях над
       таким "файлом" никакие ИС не возникают.
     - Функция exread заполняет массив buf нулями и выдает 0 в
       качестве длины прочитанной порции.
     - Функция exwrite всегда  выдает  длину  входного массива
       len, независимо от числа реально записанных байт.
     - Функция exlseek сохраняет текущую позицию, а в качестве
       значения текущей позиции выдает 0.
     Если установлен  признак excatch,  то  все функции, кроме
exclose,   не  выполняют  никаких  действий  и  ограничиваются
описанной выше коррекцией.  Функция exclose  выполняет штатное
закрытие файла, за исключением случая fd < 0.
     
     Если  при  выполнении  функции  exread  длина прочитанной
порции окажется меньше len,  то остаток buf заполнится нулями.
Нулевая длина порции свидетельствует о конце файла или ИС.
     Следует подчеркнуть,  что правила коррекции  ИС исключают
какую-либо неоднозначность. В частности, невозможно заполнение
массива  buf случайной информацией  -  там  может  быть только
информация  из  файла и/или нули.  Поэтому  процесс выполнения
программы,  использующей блочный ввод-вывод, не будет зависеть
от случайных факторов,  что облегчает верификацию и тестирова-
ние  программы.  Изложенный принцип  о д н о з н а ч н о с т и
соблюдается во всех библиотечных функциях EXBASE.
     Отметим также,  что в соответствии  с правилами коррекции
ИС функции EXBASE всегда выдают осмысленные с прикладной точки
зрения результаты.
     В качестве  примера  использования  блочного ввода-вывода
приведем Листинг 4 с программой копирования файлов.

void    main (int argc, char **argv)
{
  if (argc > 2)
  {
    static char buf [512];
    int src = exopenbr (argv [1]);
    int dst = exopenbr (argv [2]);
    while (exwrite (dst, buf, exread (src, buf, sizeof(buf))))
      continue;
    exclose (dst);
    exclose (src);
  }
}
               Листинг 4. Программа копирования файла
     
     Похожую программу можно написать и с  помощью стандартных
функций open, read, write и close, но при возникновении ИС она
будет  поступать  весьма  своеобразно.  Например,  при  ошибке
чтения функция read  выдает  -1,  что  воспринимается функцией
write, как предписание записать 65535 байт данных.


     5. Операции над строками
     
     Одной из  наиболее  популярных  функций языка  С является
функция strcpy. Начинающие программисты любят использовать эту
функцию для целей "присваивания" строки указателю:
          char *p; strcpy (p, argv [1]);
     Более опытные программисты знают, что перед присваиванием
следует распределить память: 
          char p[20]; strcpy (p, argv [1]);
     Здесь  важно правильно оценить число  символов.  Если  их
будет больше, чем предполагалось, то программа подвергнет себя
экзекуции,  последствия  которой иногда  бывают заметными  для
конечного  пользователя.  Львиная доля ошибок  в распределении
памяти бывает связана именно с операциями над  строками. Чтобы
исключить эти ошибки,  в библиотеке EXBASE реализованы модифи-
цированные строковые функции, обеспечивающие защиту памяти:
     BSTRING (buf, max);      // декларация пустого буфера,
                              // вмещающего max символов
     char* bscpy  (bstring buf, char* s);
                                   // копирование
     char* bscat  (bstring buf, char* s);
                                   // конкатенация
     char* bslcpy (bstring buf, int l, char* str);
                                   // копирование по индексу l
     char* bsval  (bstring buf);   // значение строки
     int   bslen  (bstring buf);   // текущая длина
     int   bsmax  (bstring buf);   // максимальная длина
     В случае переполнения буфера строка  автоматически усека-
ется без запуска  исключений  и  выдачи  диагностики. Поэтому,
даже  в случае  неправильного расчета памяти,  программа будет
работать вполне предсказуемым образом.
     Помимо  основных,  предусмотрены  синтаксические функции,
приспособленные к работе  с именами каталогов и  файлов, пред-
ставляемыми в формате  диск:\имя\имя... Синтаксические функции
используют статический буфер и трактуют  его переполнение, как
критическую ошибку (см. пп. 2 и 3):
     char* fncpy  (char* s);       // копирование
     char* fnlcpy (int l, char* s);// копирование по индексу l
     char* fncat  (char* s);       // конкатенация
     char* fnval  (void);          // значение строки
     int   fnlen  (void);          // текущая длина
     int   fnmax  (void);          // максимальная длина
     char* fnfile (void);          // последне имя
     int   fnfadd (char *s);       // добавить имя
     int   fnfset (char *s);       // заменить последнее имя
     int   fnfdel (char *s);       // удалить последнее имя
     Другие подходы  к обработке строк,  основанные на исполь-
зовании возможностей C++, изложены в руководствах [3,5].
     
     6. Динамическое распределение памяти
     
     Стандартные  функции  динамического  распределения памяти
языков С и С++ обладают множеством удивительных особенностей.
     Как Вы думаете,  массив какого размера будет создан в 16-
-разрядной программе по оператору new int [40000] ? Правильно,
этот массив будет содержать чуть  больше  7000 элементов. И...
никакой диагностики. 
     Теперь попробуем создать массив  new char [65533]. Не все
знают, что в этом массиве будет ровно 1 байт...
     О забавной привычке new возвращать NULL  при нехватке па-
мяти даже и говорить не хочется:  программисты уже давно пере-
стали обращать на это внимание, надеясь на то, что пронесет. 
     В качестве  альтернативы функции new  в библиотеке EXBASE
предусмотрена функция  exalloc,  которая  запускает исключение
при любых проблемах,  вызванных переполнением  разрядной сетки
индекса или нехваткой памяти.  В частности, исключение возбуж-
дается при вызовах  exalloc(int,40000)  и  exalloc(char,65533)
в 16-разрядной  программе.  Функция exalloc никогда  не выдает
NULL,   т.к.  запрет  исключений   excatch_error   на  нее  не
действует.   В   соответствии   с   принципом   однозначности,
создаваемый  массив  заполняется  нулями.   Изменение  размера
массива  может  быть  выполнено  с  помощью  функции  exreloc,
освобождение памяти - с помощью функции exfree.
     Для  того,  чтобы  облегчить  расчет  оперативной памяти,
программисту предоставляются функции  exmul (a, b) = a * b   и
exmuladd (a, b, c) = a * b + c. Эти функции выполняют операции
над  числами  unsigned,  а  при  переполнении  разрядной сетки
выдают UINT_MAX.
     7. Динамические массивы
     
     Средства  динамического  распределения  памяти  не  могут
застраховать программиста от  всех ошибок,  связанных с непра-
вильной индексацией массивов.  Как известно,  в языках С и С++
значение  индекса ограничено только параметрами  его разрядной
сетки,  но не реальным размером области памяти, отведенной под
массив.  Поэтому программист может "предусмотреть" обращение к
массиву по индексу несуществующего элемента.  Результат такого
обращения зависит от многих трудно предсказуемых факторов.
     Ошибки индексации встречается гораздо чаще,  чем хотелось
бы.  Поэтому в языке Java  предусмотрен  динамический контроль
индексов при каждом обращении к массиву:  если индекс неверен,
то запускается исключение [4].
     В  библиотеке  EXBASE  реализована  другая  возможность -
автоматическое  увеличение  длины  массива  при  обращении  по
любому индексу.  Для этого декларируется динамический массив с
указанием типа его элементов:
          exarray<тип> имя;
     Например, целочисленный массив vector объявляется так:
          exarray<int> vector;
     Размер  массива не задается.  Вместо этого постулируется,
что  массив  имеет  неограниченное  число  элементов,  которым
первоначально присвоены нулевые значения. Реальная возможность
обратиться  к любому  элементу  массива  зависит  от разрядной
сетки индекса и объема доступной памяти; при исчерпании любого
из  этих  ресурсов  запускается  исключение.  Если  исключения
запрещены,  то выдается  диагностика с  установкой глобального
признака  ошибки;  далее  при  чтении  неразмещенного элемента
выдается 0, а запись в неразмещенный элемент не производится.
     Для  обращения к  любому  элементу  массива  с  целью его
чтения или записи применяется стандартная нотация:
          имя [индекс]
     Предусмотрена возможность объявления многомерных массивов
с  неограниченными  размерами  по  всем  измерениям. Например,
двумерная целочисленная матрица matrix объявляется так:
          typedef exarray<int> vector;
          exarray<vector> matrix;
     
     Если требуется,  то можно объявить комбинированный массив
с фиксированными и динамическими измерениями, например:
          typedef int int5 [5];
          exarray<int5> matrix;
     Для передачи динамического массива в функцию в ней должен
быть объявлен параметр вида:
          exarray<тип> &имя;
     Реализация  динамических  массивов  основана  на  технике
хранения элементов в непрерывной  области  памяти  с удвоением
ее  размеров  при каждом  обращении по  индексу неразмещенного
элемента   [4,5].    В   Табл.1   сравнивается   эффективность
динамических  массивов  и  динамических  структур, реализующих
размещение элементов в несмежных областях памяти -  таких, как
связный список и двоичное дерево [5,6].
-----------------T-----------------T-------------------------¬
¦      Структуры ¦ Динамический    ¦ Динамические структуры  ¦
¦ Свойства       ¦ массив          ¦ с несмежным размещением ¦
+----------------+-----------------+-------------------------+
¦ Время доступа  ¦ Не зависит от   ¦ Пропорционально числу   ¦
¦ к элементу по  ¦ числа элементов ¦ элементов или логарифму ¦
¦ его индексу    ¦                 ¦ числа элементов         ¦
+----------------+-----------------+-------------------------+
¦ Число операций ¦ Пропорционально ¦ Пропорционально числу   ¦
¦ распределения  ¦ логарифму числа ¦ элементов               ¦
¦ памяти         ¦ элементов       ¦                         ¦
L----------------+-----------------+--------------------------
      Табл. 1. Сравнение различных динамических структур
     
     Динамические массивы избавляют  программиста от необходи-
мости заниматься  распределением памяти и  обеспечивают полную
защиту от любых  ошибок индексации,  при этом их эффективность
сопоставима с эффективностью  обычных массивов,  не защищенных
от  ошибок  индексации.  Эксперименты  показывают,  что  после
перевода 32-разрядной программы  на использование динамических
массивов время выполнения и требования  к памяти увеличиваются
не более, чем в 1.5 раза.
     В целях дальнейшего повышения  эффективности динамических
массивов в библиотеке EXBASE  предусмотрена возможность отклю-
чения контроля индексов и ручного управления размером массива.
     Интересно отметить, что в стандартной библиотеке шаблонов
С++  [5] и в языке Java [4] имеется класс Vector,  реализующий
некоторые функции динамического массива. Автоматическое увели-
чение размера массива при доступе по произвольному  индексу не
реализовано -  вместо этого программисту  предлагается указать
размер с помощью методов reserve и ensureCapacity. При попытке
доступа  к несуществующему элементу  в языке  Java запускается
исключение; в библиотеке STL реакция программы не определена.
     Благодаря контролю  индексов  реализация  класса Vector в
языке Java  выглядит   более привлекательной, чем в библиотеке
STL.  Однако в Java-классе Vector  разрешается  хранить только
ссылки на объекты, но не сами объекты и тем более - элементар-
ные  данные.  Кроме  того,  для  обращения  к  элементу вместо
удобной нотации имя [индекс]  применяются  методы  elementAt и
setElementAt.  Ситуация  здесь серьезнее,  чем  в  случае C++.
Язык C++, по крайней мере, допускает возможность качественной,
удобной и эффективной реализации динамических массивов. Что же
касается языка Java,  то из-за присущих  ему ограничений любая
реализация динамических массивов будет неполноценной.
     
     8. Грамматический анализ и синтез текстов
     
     Стандартные возможности по обработке  текстов, встроенные
в  языки  С,   С++  и  Java,  сводятся  к  наличию  операторов
потокового  ввода-вывода  и  примитивных  средств лексического
анализа и синтеза. Программы, написанные с использованием этих
средств,  характеризуются медлительностью [6],  а также отсут-
ствием устойчивости по отношению к ошибкам в исходных данных.
     Для того,  чтобы повысить качество программ, разработчику
предлагается изучить теорию  синтаксического анализа, перевода
и компиляции  [7].  Важность  теоретической  подготовки трудно
отрицать.  Тем не менее,  существуют некоторые сомнения в том,
что  многоступенчатые  схемы  лексического,  синтаксического и
семантического анализа и синтеза позволяют получать компактные
и  производительные  прикладные  программы.  В конечном счете,
прикладной   программист   не   обязан   заниматься  созданием
полноценных трансляторов  -  будет вполне достаточно,  если он
обеспечит   предсказуемую  реакцию  программы  на   ошибки   и
требуемую производительность при вводе-выводе данных.
     В библиотеке EXBASE предусмотрены функции грамматического
анализа и  синтеза,  ориентированные  на  обработку  текстов с
табличной или списковой структурой.
     В тексте с табличной структурой (Пример 1a) каждая строка
состоит из последовательности элементов, разделенных запятыми.
     Текст с списковой  структурой (Пример 1б)  - это иерархи-
ческий  список  элементов.  Каждый  элемент  может  иметь имя,
перечень весовых имен  в косых скобках  и  подсписок элементов
в круглых скобках.  Число вложений  подсписков  не ограничено,
пробелы игнорируются, разбиение на строки произвольно.
     
          A,,01     TAB/T,0/(входы(A,B),выходы(C),(0110-X))
          B,,10
     a)   ,C,-X     b)
         Пример 1. Примеры табличной и списковой структуры
     
     Методика обработки текстов, принятая в библиотеке EXBASE,
ориентирована на сокращение числа промежуточных  копирований и
перекодировок информации.  Это  позволяет  обрабатывать тексты
быстрее, чем это возможно при потоковом вводе-выводе.
     При чтении табличной структуры файл целиком загружается в
оперативную память и автоматически преобразуется  в "байт-код"
путем замены запятых и байтов'\r' нулями. Для доступа к "байт-
-коду"  программисту предоставляются функции последовательного
сканирования,  выдающие указатели на элементы. Чтобы сократить
накладные расходы, функции сканирования лишены параметров.
     Аналогичный подход  применяется  и  при  чтении списковой
структуры.  "Байт-код"  списковой  структуры  содержит  имена,
завершающиеся нулем  и при каждом имени - информацию об уровне
вложения   скобок.   Синтаксические  разделители,   пробелы  и
переводы строк в "байт-код" не попадают.
     Запись текстов выполняется через промежуточный буфер, при
этом списковые структуры автоматически нарезаются на строки.
     Основные  функции  обработки  текстов  перечислены  ниже.
Примеры их использования приведены на листингах 5 и 6.
     
     int  dbget_open (char *file); // Начать чтение таблицы
     int  dbget_nlines   (void);   // Число строк
     int  dbget_nitems   (void);   // Число колонок
     int  dbgetl         (void);   // Чтение очередной строки
     const char *dbget   (void);   // Чтение очередного эл-та
     int  dbvalid    (char *psz);  // Элемент существует?
     void dbget_close    (void);   // Завершить чтение таблицы
     
     void dbput_open (char *file); // Начать запись таблицы
     void dbput      (char *psz);  // Записать элемент
     void dbputl         (void);   // Завершить строку
     void dbget_close    (void);   // Завершить запись таблицы
     
     int  lcget_open (char *file); // Начать чтение списка
     const char *lcget   (void);   // Чтение очередного имени
     int  lcvalid    (char *psz);  // Элемент существует?
     void lcget_wsub     (void);   // Читать весовые имена
     void lcget_wend     (void);   // Читать обычные имена
     void lcget_sub      (void);   // Читать подсписок
     void lcget_end      (void);   // Читать надсписок
     void lcget_close    (void);   // Завершить чтение списка
     
     void lcput_open (char *file); // Начать запись списка
     void lcput      (char *psz);  // Записать имя
     void lcput_wsub     (void);   // Начать запись веса
     void lcput_wend     (void);   // Завершить вес
     void lcput_sub      (void);   // Начать запись подсписка
     void lcput_end      (void);   // Завершить подсписок
     void lcput_close    (void);   // Завершить запись списка
     
     Функции dbget_open() и lcget_open() автоматически коррек-
тируют синтаксические  ошибки  в исходных  данных. Сообщения о
синтаксических ошибках  не  выдаются.  Критические  ошибки при
чтении-записи обрабатываются так, как описано в п.4.
     Автоматическая  обработка ИС,  связанных  с  избытком или
недостатком данных,  выполняется в  функциях сканирования. Эти
функции  автоматически  пропускают  данные,  не востребованные
программой,  а также имитируют наличие пустых элементов  в том
случае,  если востребованные  данные  отсутствуют. Диагностика
не выдается, т.к. подобные ситуации могут быть штатными.


     Функция dbgetl()  выдает 1, если очередная строка имеется
и 0,  если  текст закончился.  После  dbgetl()  можно вызывать
dbget()  для чтения элементов строки;  в  конце текста  имити-
руется чтение пустой строки.  Вызов dbgetl() может выполняться
любое число раз, независимо от реального числа строк.
     Функция dbget()  выдает очередной элемент строки,  а если
его  нет,  то пустой элемент.  Вызов dbget() может выполняться
любое число раз, независимо от реального числа элементов.
     Функция dbvalid() позволяет проверить указатель, выданный
dbget на предмет фактического наличия элемента  в строке: если
элемент имеется, то выдается 1, а иначе - 0.
     Функция lcget() последовательно выдает имена элементов на
текущем  уровне  вложения  скобок,  пропуская  весовые имена и
подсписки.  Если элементы закончились, то выдается пустое имя.
Вызов lcget() может выполняться любое число раз, независимо от
реального числа элементов. 
     Функция lcvalid() позволяет проверить указатель, выданный
lcget на предмет фактического наличия  элемента: элемент есть,
то выдается 1, а иначе - 0.
     Текущий  уровень  вложения  скобок  изменяется  с помощью
функций lcget_wsub(), lcget_wend(), lcget_sub() и lcget_end().

void    main (int argc, char **argv)
{
  if (argc > 2)                 // Пример исходной таблицы:
  {                             // A,,01
    dbget_open (argv [1]);      // B
    dbput_open (argv [2]);      // ,C
    while (dbgetl())
    {                           // Результирующая таблица:
      dbput (dbget());          // A,
      dbput (dbget());          // B,
      dbputl()                  // ,C
    }                           
    dbput_close();              
    dbget_close();
  }
}
       Листинг 5. Программа копирования двух колонок таблицы
void    main (int argc, char **argv)
{
  if (argc > 2)                 // Пример исходного списка:
  {                             // TAB/T/(входы(A,B),выходы(C))
    lcget_open (argv [1]);      
    lcput_open (argv [2]);      // Результирующий список:
    lcput (lcget());            // TAB(входы,выходы)
    lcget_sub();
    lcput_sub();    
    const char *p; while (lcvalid (p = lcget())) lcput(p);
    lcget_end();
    lcput_end();    
    lcput_close();
    lcget_close();
  }
}
          Листинг 6. Программа копирования имен подсписков
     
     Методика  обработки  ИС,  принятая  в  библиотеке EXBASE,
обеспечивает разработку программ,  обладающих устойчивостью по
отношению к ошибкам в исходных данных,  адекватной реакцией на
нехватку ресурсов  и ошибки  ввода-вывода,  а также стабильным
поведением во время тестирования.
     
     Литература
     1. В.А.Непомнящий, О.М.Рякин. Прикладные методы верифика-
ции  программ. М.: Радио и связь, 1988. 256c. 
     2. Роберт  Лоренс  Бейбер.  Программное  обеспечение  без
ошибок. M.: Радио и связь, 1996. 176с.
     3. В.М.Пентковский. Автокод эльбрус. M.:Наука, 1982. 352с
     4. Патрик   Нотон.   Java.  Справочное  руководство.  M.:
Восточная книжная компания, 1996. 448с.
     5. Rogue Wave.   Standard  C++   Library   User's  Guide,
Tutorial and  Class Reference.  Rogue Wave Software Corvallis,
Oregon USA.
     6. Р.Уинер. Язык Турбо С. M.: Мир, 1991. 384с.
     7. А.Ахо,  Дж.Ульман.   Теория  синтаксического  анализа,
перевода и компиляции. Том 1. Мир, 1978.