
Глава 14
Как отлаживать программу
-----------------------------------------------------------------

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

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

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

     Посмотрим, с чего требуется начать, когда вы обнаружили, что
ваша программа работает неверно.

Что делать, если программа не работает?
-----------------------------------------------------------------

     Прежде всего,    не   следует   паниковать.   Даже   опытным
программистам редко удается с первого  раза  написать  программу,
которая сразу начинаяет правильно работать.

     Чтобы избежать   потери   большого   количества   времени  и
безрезультатных поисков,  постарайтесь преодолеть  соблазн  и  не
строить  беспорядочных  гипотез  о  том,  где бы могла находиться
ошибка.  Лучше  всего  сразу  воспользоваться   универсальным   и
проверенным принципом: разделяй и всластвуй.

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

     Если же  к  моменту  вызова функции xyz ошибки не произошло,
значит  исходное  предположение  было   неверным.   Тогда   можно
предположить,  что  ошибка  возникает где-то после вызова функции
xyz.  Выполнив несколько подобных проверок,  можно бысторо  найти
тот фрагмент программы, который вызывает ошибку.

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

Стиль отладки
-----------------------------------------------------------------

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

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

Проверка работы всей программы   --------------------------------

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

Последовательное тестирование   ---------------------------------

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

Виды ошибок
-----------------------------------------------------------------

     Ошибки в   программах   можно   разделить   на  две  большие
категории:  ошибки,  характерные для языка,  на котором  написана
программа  (Си,  Паскаля  или ассемблера),  и ошибки,  являющиеся
общими для всех языков программирования.

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

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

Общие ошибки   --------------------------------------------------

     Ниже кратко  перечислены примеры ошибок,  которые характерны
для программ на любых языках.

Скрытые эффекты
---------------

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

     char workbuf[20];
     strcpy(workbuf,"all done\n");
     convert("xyz");
     printf(workbuf);
     ...
     convert(char *p)
     {
        strcpy(workbuf, p);
        while (*p)
           ...
     }

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

Вы предположили, что данные уже инициализированы
------------------------------------------------

     Не полагайтесь    на    то,    что   переменные   уже   были
инициализированы где-то другой подпрограммой:

     char *workbuf;
     addworkstring(char *s)
     {
        strcpy(workbuf, s);   /* ошибка */
     }

     Здесь нужно записать более определенно:

     if (workbuf == 0) workbuf = (char *)malloc(20);

Не выполнена очистка памяти
---------------------------

     Следующая ошибка  может  привести  к программному сбою из-за
того, что израсходовано все пространство кучи:

     crunch_string(char *p)
     {
        char *work = (char *)malloc(strlen(p));
        strcpy(work, p);
        ...
        return(p);    /* при этом work еще распределена память */
     }

Ошибка "последнего столба изгороди"
-----------------------------------

     Этого рода  ошибки названы в честь старой загадки :  "Если я
хочу поставить изгородь длиной 100 футов со столбами через каждые
10 футов,  сколько мне понадобится столбов?" Быстрый, но неверный
ответ - 10 столбов (однако,  как насчет последнего столба в конце
изгороди?). Ниже   приводится   пример   такого   рода  ошибки  в
Си-программе:

     for (n = 1; n < 10; n++)
     {
        ...           /* ошибка - цикл выполнится только 9 раз */
     }

     Здесь фигурируют цифры 1 и 10,  и вам может показаться,  что
цикл будет выполнен 10 раз.  (Чтобы на самом  деле  так  и  было,
замените < на <=).

Ошибки, характерные для программирования на Си   ----------------

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

     Компилятор Turbo C может очень хорошо находить  ряд  ошибок,
специфичных  для  языка  Си,  которые  другими  компиляторами  не
обнаруживаются.  Можно сэкономить много времени, требующегося для
отладки, если включить формирование всех возможных предупреждений
компилятора.  (Информация  о  формировании  этих   предупреждений
приведена в Руководстве пользователя по языку Turbo C).

     Ниже приведено   описание   некоторых  характерных  случаев,
которые вызывают затруднения  при  программировании  на  Си.  Для
некоторых   из   описанных   ошибок  компилятор  Turbo  C  выдает
предупреждающие сообщения.  Не забывайте выяснять причину каждого
такого   сообщения,   поскольку  они  могут  предупредить  вас  о
допускаемой вами ошибке.

Использование неинициализированных автоматических переменных
------------------------------------------------------------

     В языке Си авто-переменная,  описанная внутри функции, имеет
неопределенное значение,  пока  оно  не  будет  каким-то  образом
загружено:

     do_ten_times()
     {
        int n;
        while (n < 10)
           {
           ...
           n++;
        }
     }

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

Использование = вместо ==
-------------------------

     Язык Си позволяет как присваивать значение (знак =),  так  и
проверять   равенство   значений   (знак  ==)  внутри  выражений,
например,

     if (x = y) {
        ...
     }

     Здесь неумышленно  значение  переменной  y  будет  присвоено
переменной x и будут выполнены операторы в выражении if, если это
значение не равно нулю.  Вместо этого скорее  всего  должно  было
быть записано следующее:

     if (x = = y) {
        ...

Неправильная расстановка операций в выражении
---------------------------------------------

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

     x = 3 << 1 + 1

     будет равно 12,  а не 7,  как могло было быть,  если бы знак
<< стоял перед знаком +.

Неправильное вычисление указателей
----------------------------------

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

     int *intp;
     intp += sizeof(int);

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

     intp++

Неожидаемое расширение знака
----------------------------

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

     int i = OXFFFF;
     long l;
     l = i;
     if (l & 0X80000000) {
        ...                              /* это БУДЕТ выполняться */
     }

     Одно из   строгих  правил  языка  Си  может  вызвать  у  вас
затруднения, если вы не будете учитывать его важность. В Си можно
свободно  присваивать  значения  различных скалярных типов (char,
int  и  т.д.).  Когда  некоторое  целое  значение   присваивается
переменной  большего размера,  знак сохраняется в этой переменной
путем размножения знакового (старшего) бита  в  старших  разрядах
переменной.  Например,  значение  -2 (0xfffe) типа int становится
значением -2 (0xfffffffe) типа long.

Неожидаемое отсечение
---------------------

     Следующий пример приведен в противоположность предыдущему:

     int i;
     long l = 0X10000;
     i = l;
     while (i > 0) {
        ...                           /* это НЕ БУДЕТ выполняться */
     }

     Здесь присваивание  переменной  i  значения   переменной   l
приведет  к  тому,  что  старшие  16  разрядов переменной l будут
отсечены, а переменная i станет равна нулю.

Лишние точки с запятой
----------------------

     Следующий фрагмент   программы  на  первый  взгляд  выглядит
вполне нормально:

     for (x = 0; x < 10; x++);
     {
        ...                        /* выполняется только один раз */
     }

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

Макросы с побочными эффектами
-----------------------------

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

     #define toupper(c) 'a'<= (c)&&(c)<='z' ? (c)-'a'-'A' : (c)
     char c, *p;
     c = toupper(*p++);

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

Повторение имен автоматических переменных
-----------------------------------------

     Еще одна ошибка, которую сложно обнаружить:

     myfunc()
     {
        int n;
        for (n = 5; n >= 0; n--)
        {
           int n = 10;
           ...
           if (n == 0)
           {
              ...
           }
        }
     }

     Здесь имя автоматической переменной n  повторно используется
во внутреннем блоке, закрывая доступ к переменной n, описанной во
внешнем блоке.  Подобное повторение имен переменных во внутренних
блоках нужно использовать с особой осторожностью. Допустить такую
ошибку значительно проще,  чем вы могли  бы  подумать,  поскольку
большинство  программистов  использует  ограниченный  набор  имен
переменных для счетчиков  локальных  циклов  (например,  i,  n  и
т.д.).

Неправильное использование автоматических переменных
----------------------------------------------------

     Следующая функция   должна   была    возвращать    указатель
результата вычисления:

     int *divide_by_3(int n)

     {
        int i;
        i = n/3;
        return(&i);
     }

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

Неопределенное значение, возвращаемое функцией
----------------------------------------------

     Если функция  не  заканчивается   зарезервированным   словом
return,  за  которым  следует некоторое выражение,  функция будет
возвращать неопределенное значение. Например,

     char *first_capital_letter(char *p)
     {
        while (*p)
        {
           if ('A' <= *p && *p <= 'Z')
               return(p);
           p++;
        }
                                   /* функция ничего не возвращает*/
     }

     Если в   строке   нет  заглавных  букв,  функция  возвращает
бессмысленное значение.  В качестве последней строки этой функции
должен быть записан оператор return(0).

Неправильное использование зарезервированного слова break
---------------------------------------------------------

     Оператор break   осуществляет   выход   только   из   одного
вложенного цикла do, for, switch или while:

     for (...)
     {
        while (...)
        {
           if (...)
             break;                /* мы хотим выйти из цикла for */
        }
     }

     Здесь оператор break  осуществляет  выход  только  из  цикла
while.  Это  один  из тех немногих случаев,  где вполне оправдано
использование оператора goto.

Код работает неправильно
------------------------

     Иногда нормально  компилируемые  фрагменты  исходного текста
дают не тот результат, который ожидался:

     a + b;

     Здесь должна была стоять строка a += b.


Ошибки, характерные для программирования на Паскале   -----------

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

Использование неинициализированных переменных
---------------------------------------------

     Turbo Pascal не может инициализировать переменные за вас; вы
должны  это  сделать  самостоятельно  либо  с  помощью  оператора
присваивания,   либо   объявив   их   типизованными  константами.
Рассмотрим следующую программу.

     program Test;
     var
       I,J,Count : Integer;
     begin
       for I := 1 to Count do begin
         J := I*I;
         Writeln(I:2, ' ',J:4);
       end;
     end.

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

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

Ошибки при работе с указателями
-------------------------------

     Есть три характерные ошибки,  которые допускаются при работе
с  указателями.  Во-первых,  как  упоминалось  выше,  не  следует
использовать  указатели прежде,  чем им будет присвоено некоторое
значение (nil или какое-то другое).  Так же,  как и любые  другие
переменные,  указатели не инициализируются автоматически после их
описания.  При первой же возможности им обязательно  должно  быть
задано начальное значение (путем передачи их в качестве параметра
процедуре New или присвоения им значения nil).

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

     function FindNode(Head : NodePtr; Val : integer);
     var
       Temp : NodePtr;
     begin
       Temp := Head;
       while (Temp^.Key <> Val) and (Temp <> nil) do
         Temp := Temp^.Next;
       FindNode := Temp
     end.  { функции FindNode }

     Если значение Val не равно значению поля Key любого из узлов
связного  списка,  эта  подпрограмма  будет  пытаться   вычислить
значение переменной Temp^.Key, когда Temp равна nil, что приведет
к непредсказуемому результату. Где же решение проблемы? Перепишем
условие цикла while следующим образом:

     while (Temp <> nil) and (Temp^.Key <> Val) do

     и включим  режим сокращенного вычисления булевских выражений
с помощью директивы {$B-} компилятора Turbo  Pascal  или  команды
Options /Compiler/Boolean .  В результате этого,  если Temp равна
nil, второй терм выражения никогда не будет вычисляться.

     И наконец,  никогда не следует думать,  что указателю  будет
присвоено  значение  nil в результате того,  что оно передается в
качестве параментра в процедуру Dispose  или  FreeMem.  Указатель
сохранит свое прежнее значение,  но область памяти, на которую он
указывал,  освободится и может быть занята другими  динамическими
переменными.  Поэтому  после удаления из памяти структуры данных,
на котороую указывает ссылочная переменная,  ей обязательно  надо
присвоить значение nil.

Ошибки, связанные с областью действия
-------------------------------------

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

     program Confused;
     var
       A,B : integer;

     procedure Swap(var A,B : integer);
     var
       T : integer;
     begin
       Writeln('2: A,B,T = ', A:3,B:3,' ',T);
       T := A;
       A := B;
       B := T;
       Writeln('3: A,B,T = ', A:3,B:3,' ',T);
     end;  { процедуры Swap }

     begin { основное тело программы Confused }
       A := 10; B := 20; T := 30;
       Writeln('1: A,B,T = ', A:3,B:3,' ',T);
       Swap(B,A);
       Writeln('4: A,B,T = ', A:3,B:3,' ',T);
     end. { программы Confused }

     Что будет выдавать на экран эта  программа?  Нечто  подобное
приведенному ниже.

     1: A,B,T =  10 20 30
     2: A,B,T =  20 10 22161
     3: A,B,T =  10 20 20
     4: A,B,T =  20 10 30

     Это произошло потому,  что мы имеем по две версии каждой  из
переменных  A,  B  и  T.  Глобальные  переменные  используются  в
основном теле программы,  в то время как функция Swap имеет  свои
локальные  версии  этих переменных:  формальные параметры A и B и
локальную  переменную  T.  Чтобы  еще  больше  все  запутать,  мы
выполняем  вызов  Swap(B,A),  который  означает,  что  формальный
параметр A в действительности является глобальной переменной  B и
наоборот.  И,  конечно  же,  нет  никакой связи между локальной и
глобальной переменными T.

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

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

     type
       RecType = record
         A,B : integer;
       end;

     var
       A,B : integer;
       Rec : RecType;

     В пределах оператора with обращения к A и B  будут  являться
обращениями к полям записи, а не к переменным.

Лишние точки с запятой
----------------------

     Так же как и в Си,  в Паскале допускается "пустой" оператор,
состоящий  только  из  точки  с запятой.  Поставленный в неверном
месте,  он может вызвать все виды  ошибок.  Рассмотрим  следующую
программу.

     program Test;
     var
       I,J, : Integer;
     begin
       for I := 1 to 20 do;
       begin
         J := I * I;
         Writeln(I:2,' ',J:4);
       end;
       Writeln('Все сделано!')
     end.

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

     20 400
     Все сделано!

     Это произошло  потому,  что  оператор  for  I := 1 to 20 do;
заканчивается точкой с запятой.  Это означает,  что 20 раз  будет
выполняться   пустой   оператор.   После  этого  будут  выполнены
операторы блока begin..end,  а  затем  второй  оператор  Writeln.
Чтобы  исправить  эту ошибку,  надо просто убрать точку с запятой
после слова do.

Неопределенное значение, возвращаемое функцией
----------------------------------------------

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

     const
       NLMax = 100;
     type
       NumList = array[1..NLMax] of integer;
       ...
     function FindMax(List : NumList; Count : integer) : integer;
     var
       I,Max : integer;
     begin
       Max := List[1];
       for I := 2 to Count do
         if List[I] > Max then
         begin
           Max := List[I];
           FindMax := Max
         end
     end; { функции FindMax }

     Функция будет   работать   прекрасно   до   тех   пор,  пока
максимальным не будет значение List[1].  В  этом  случае  функции
FindMax не будет присвоено никакого значения.  Правильный вариант
этой функции будет выглядеть так:

     begin
       Max := List[1];
       for I := 2 to Count do
         if List[I] > Max then
           Max := List[I];
           FindMax := Max
     end; { функции FindMax }

Уменьшение значений переменных типа byte и word
-----------------------------------------------

     Не следует  допускать  отрицательного  приращения  скалярных
беззнаковых переменных (типа byte и word) при проверке их  на  >=
0. В приведенном ниже фрагменте имеется бесконечный цикл.

     var
       w : word;
     begin
       w := 5;
       while w >= 0 do
         w := w - 1;
     end.

     После пятой итерации значение переменной w становится равным
нулю.  При следующем проходе ее значение "уменьшается"  до  65535
(поскольку значения типа word лежат в диапазоне от 0 до 65535), и
продолжает удовлетворять условию >= 0. В таких случаях необходимо
использовать переменные типа integer или longint.

Игнорирование границ или особых случаев
---------------------------------------

     Обратите внимание,  что в  обеих  версиях  функции  FindMax,
приведенных в предыдущем разделе, предполагается, что Count >= 1.
Однако в некоторых случаях значение Count может быть  равно нулю,
то  есть  список  будет пуст.  Если в этом случае вызвать функцию
FindMax,  она возвратит значение,  которое  хранится  в  List[1].
Аналогично,  если  Count  > NLMax,  вызов функции либо приведет к
ошибке периода выполнения (если включен режим  контроля  границ),
либо  поиск  максимального значения будет производиться в ячейках
памяти, находящихся за пределами массива List.

     В данном случае имеется два возможных решения проблемы. Одно
из них,  конечно же, заключается в том, чтобы никогда не вызывать
функцию FindMax,  если Count не находится в  диапазоне  1..NLMax.
Причем это относится не только к данному примеру;  один из важных
аспектов  хорошего  программирования   состоит   в   том,   чтобы
определять  для каждой подпрограммы условия ее вызова и проверять
эти условия при каждом вызове подпрограммы.

     Другое решение состоит в том, чтобы проверять значение Count
и  возвращать  некоторое  предопределенное значение,  если оно не
лежит в диапазоне 1..NLMax.  Например,  можно было бы  переписать
тело функции FindMax следующим образом:

     begin
       if (Count < 1) or (Count > NLMax) then
         Max := -32768
       else
       begin
         Max := List[1];
         for I := 2 to Count do
           if List[I] > Max then
           Max := List[I];
       end;
       FindMax := Max
     end; { функции FindMax }

     Эта функция вызовет следующий вид ошибки программирования на
Паскале: ошибку выхода за границы диапазона.

Ошибки выхода за границы диапазона
----------------------------------

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

     Первым шагом в нахождении подобных ошибок является включение
режима  контроля  границ  с  помощью директивы компилятора {$R+},
помещенной в  текст  программы,  компилирование  программы  и  ее
повторный   запуск.  Если  вы  знаете  (или  предполагаете),  где
находится   ошибка,   вы   можете   поместить    эту    директиву
непосредственно перед этим фрагментом программы и соответствующую
директиву {$R-} после него, включая таким образом контроль границ
только  для  данного  фрагмента.  Если возникнет ошибка выхода за
границы диапазона,  программа будет остановлена  и  будет  выдано
соответствующее  сообщение об ошибке периода выполнения,  а Turbo
Pascal покажет вам, где произошла ошибка.

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

     function FindVal(List: NumList; Count,Val: integer) : integer;
     var
       I : integer;
     begin
       FindVal := 0;
       I := 1;
       while (I <= Count) and (List[I] <> Val) do
         Inc(I);
       if I <= Count then
         FindVal := I
     end; { функции FindVal }

     Все здесь очень очень хорошо,  однако функция может  вызвать
ошибку  периода  выполнения,  если  значение  Val  не находится в
пределах массива List и используется режим нормального вычисления
булевских   выражений.   Почему?  Потому  что  когда  выполняется
последняя проверка в вершине цикла while,  значение I будет равно
Count+1.  Если  Count=NLMax,  произойдет выход за границы массива
List.

     Такая проблеме  может  иметь  два  решения.  Одно   из   них
заключается в том, чтобы отменить контроль диапазона. Однако, это
может внести незаметные ошибки, особенно если рассматриваемый код
фактически изменяет  значения.  Лучшее,  показанное  выше решение
состоит в том,  чтобы выбрать  вычисление  логического  выражения
либо при помощи команды Options|Compiler|Boolean, либо при помощи
директивы {$B-}. Таким образом, если I > Count, то выражение

     List[I] <> Val

не вычисляется никогда.

Ошибки, характерные для программирования на ассемблере   --------

     Ниже описаны    некоторые    ошибки,     характерные     для
программирования   на   языке  ассемблера.  Для  получения  более
подробной информации об этих ошибках  и  способах  их  устранения
следует  обратиться  к  Руководству  пользователя  по языку Turbo
Assembler (Turbo Assembler User's Guide).

Отсутствие команды возврата в DOS
---------------------------------

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

Отсутствие команды RET
----------------------

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

Формирование возврата неправильного типа
----------------------------------------

     Директива PROC  выполняет  два  действия.   Во-первых,   она
определяет имя, по которому будет вызываться некоторая процедура.
Во-вторых,  она задает тип процедуры:  дальняя (far) или  ближняя
(near).  Команды возврата RET в процедурах должны соответствовать
типу процедуры, не правда ли?

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

Неправильная расстановка операндов
----------------------------------

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

     mov ax,bx

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

Отсутствие стека или резервирование слишком маленького стека
------------------------------------------------------------

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

Вызов подпрограммы, который уничтожает содержимое нужных регистров
------------------------------------------------------------------

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


Неправильное использование условных переходов
---------------------------------------------

     Обилие команд  условного  перехода  в  языке ассемблера (JE,
JNE,  JC, JNC, JA, JB, JG и т.д.) обеспечивают большую гибкость в
написании   программы   и   в   то  же  время  может  привести  к
использованию не той  команды,  которая  требуется  в  конкретном
случае.  Кроме  того,  поскольку  для  обработки  условия в языке
ассемблера требуется как минимум две строки (одна для сравнения и
одна  для  условного  перехода),  а для обработки сложных условий
значительно  больше,  обработка  условий   в   ассемблере   менее
интуитивна и больше подвержена ошибкам, чем в Паскале или Си.

Ошибки при повторении команд обработки строк
--------------------------------------------

     Команды обработки строк  имеют  одну  необчную  особенность:
после  их  выполнения используемые ими указатели сдвигаются таким
образом,  что указывают на адрес,  отличающийся на 1 байт (или  2
байта,  если если длина команды равна одному слову) от последнего
обработанного адреса. Это может привести к некоторой путанице при
повторении команд обработки строк, особенно команд REP SCAS и REP
CMPS.

Неправильный расчет на нулевое значение CX
------------------------------------------

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

Неправильная установка флага направления
----------------------------------------

     При выполнении  команды  обработки  строк  связанные  с  ней
указатели  (SI,  DI   или   оба)   получают   положительное   или
отрицательное   приращение.   Это   зависит  от  состояния  флага
направления.  С помощью команды CLD флаг направления  может  быть
сброшен в 0;  в этом случае при выполнении команд обработки строк
указатель получает положительное приращение (смещается  в сторону
старших   адресов).   С  помощью  команды  STD  флаг  направления
устанавливается  в  1;   в   этом   случае   указатель   получает
отрицательное  приращение (сдвигается в сторону младших адресов).
После того,  как флаг направления был установлен  в  определенное
состояние,  он  будет оставаться в нем до тех пор,  пока не будет
выполнена еще одна команда CLD или STD,  или пока значения флагов
не  будут  извлечены из стека с помощью команды POPF или IRET.  С
одной  стороны,  удобно  иметь  возможность  устанавливать   флаг
направления  в  определенное  состояние только один раз,  а затем
выполнять серию  команд,  которые  должны  использовать  заданное
направление.  С  другой  стороны,  это может привести к появлению
неустойчивых и труднообнаруживаемых ошибок,  в результате которых
команды  обработки  строк  работают  по-разному  в зависимости от
работы команд, которые были выполнены значительно раньше.

Ошибки при повторении команд сравнения строк
--------------------------------------------

     Команда CMPS  сравнивает содержимое двух областей памяти,  а
команда SCAS  сравнивает  содержимое  накапливающего  регистра  с
содержимым области памяти. Когда перед одной из этих команд стоит
префикс REPE, она выполняет сравнение, либо пока CX не становится
равным  нулю,  либо  пока не обнаружится,  что операнды не равны.
Когда  перед  командой  стоит  префикс   REPNE,   она   выполняет
сравнение,  либо пока CX не становится равным нулю,  либо пока не
обнаружится что операнды равны.  К несчастью,  легко  перепутать,
где какой префикс нужно использовать.

Ошибки при назначении сегмента строк
------------------------------------

     Все строковые команды по  умолчанию  используют  в  качестве
сегмента исходных данных (если он есть) сегмент DS,  а в качестве
сегмента результирующих данных (если он есть) сегмент  ES.  Легко
забыть об этом и попытаться,  скажем, выполнить команду STOSB над
сегментом  данных,  поскольку  все  данные,   обрабатываемые   не
строковыми командами, обычно находятся именно в этом сегменте.

Неправильное преобразование из байта в слово
--------------------------------------------

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

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

     shr cx,l

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

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

Использование нескольких префиксов
----------------------------------

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

Необязательные операнды в командах обработки строк
--------------------------------------------------

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

Уничтожение содержимого регистра при умножении
----------------------------------------------

     Умножение (8 на 8 битов, 16 на 16 битов, либо 32 на 32 бита)
всегда уничтожает содержимое  как  минимум  одного  регистра,  не
являющегося   накапливающим  регистром,  который  используется  в
качестве исходного операнда.

Ошибки, связанные с изменением содержимого некоторых
регистров командами обработки строк
----------------------------------------------------

     Команды обработки строк,  такие как MOVS, STOS, LODS, CMPS и
SCAS,  могут  влиять  на  состояние некоторых флагов и содержимое
трех  регистров  при   выполнении   единственной   команды.   При
использовании   команд   обработки  строк  следует  помнить,  что
содержимое одного из  регистров  SI  или  DI  (или  обоих  сразу)
получает    положительное   или   отрицательное   приращение   (в
зависимости от состояния флага направления) при каждом выполнении
команды  обработки  строк.  Содержимое регистра CX также получает
отрицательное  приращение  как  минимум  один  раз  и,  возможно,
уменьшается  до  нуля  при каждом использовании команды обработки
строк с префиксом REP.

Неправильное предположение о том, что некоторые
команды изменяют состояние флага переноса
-----------------------------------------------

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

Слишком длительное ожидание использования флагов
------------------------------------------------

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

Смешение операндов в памяти и промежуточных операндов
-----------------------------------------------------

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

Ошибки, связанные с возвратом в начало сегмента
-----------------------------------------------

     Один из   самых  сложных  моментов  в  программировании  для
микропроцессора  8086  состоит  в  том,  что  к   памяти   нельзя
обращаться  как  к  одному большому массиву байтов.  Вместо этого
память делится на части (сегменты) размером 64 КБайта, и доступ к
ним осуществляется через сегментные регистры.  Сегментация памяти
может  вызвать  труднообнаруживаемые   ошибки,   поскольку   если
программа  пытается  обратиться  к  адресу,  который находится за
границами сегмента,  в действительности вместо  этого  происходит
возврат в начало того же сегмента.

Сохранение содержимого регистров при обработке прерываний
---------------------------------------------------------

     Каждый обработчик прерываний  должен  обязательно  сохранять
содержимое   всех   регистров.   Хотя   и  допускается  сохранять
содержимое  только  тех  регистров,  которое  изменяется   данным
обработчиком   прерываний,   для   надежности   работы   все   же
рекомендуется заносить содержимое всех регистров в стек при входе
в обработчик прерываний и извлекать его из стека при выходе.

Ошибки, связанные с игнорированием групп в таблицах
операндов и данных
---------------------------------------------------

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

     К сожалению,  тот способ, который используется для обработки
сегментных групп в языке Macro Assembler фирмы  Microsoft (MASM),
может  вызвать некоторые проблемы,  и пока не появился язык Turbo
Assembler,  сегментные  группы  доставляли  программистам   много
неприятостей.  И  хотя  этих неприятностей практически невозможно
было  избежать,  сегментные   группы   были   нужны   для   связи
ассемблерного кода с языками высокого уровня, такими как Си.

     В режиме Quirks языка MASM Turbo Assembler эмулирует MASM, и
это означает,  что в этом режиме он имеет те же проблемы,  что  и
MASM. Если вы не собиратесь использовать режим Quirks языка MASM,
можете больше ничего о нем не читать,  однако если вы  планируете
работать с этим режимом, вам следует обратиться за дополнительной
информацией к Руководству пользователя по языку  Turbo Assembler.

Проверка программы на точность
-----------------------------------------------------------------

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

Проверка граничных условий   ------------------------------------

     Для того чтобы убедиться в том,  что подпрограмма  нормально
работает  в  некотором  диапазоне  значений входных данных,  надо
проверить ее  работоспособность  со  значениями  входных  данных,
лежащих на границах допустимого диапазона. Например, если имеется
подпрограмма,  которая отображает на экране список длиной от 1 до
20 элементов,  следует убедиться в правильности ее работы как при
одном элементе в списке,  так и при двадцати.  Эта проверка может
выявить  одну  из ошибок типа "столбы в изгороди" (на один больше
или на один меньше), описанных выше.

Ошибочные входные данные   --------------------------------------

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

Пустые входные данные   -----------------------------------------

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

Отладка как стадия разработки программы
-----------------------------------------------------------------

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

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

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

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

 Пример сеанса отладки
 ----------------------------------------------------------------

     В этом  примере  отладки  используются  некоторые   способы,
которые  были  рассмотрены в предшествующих разделах.  Программа,
которую   мы   будем   отлаживать   представляет   собой   версию
демонстрационной программы, рассмотренной в главе 3 (TCDEMO.C или
TPDEMO.PAS) с той  лишь  разницей,  что  она  содержит  несколько
умышленно вставленных ошибок.

     Убедитесь в том, что в текущей директории имеются два файла,
необходимые для  выполнения  демонстрационной  отладки.  Если  вы
собираетесь  отлаживать  программу  на  языке  Turbo Pascal,  вам
потребуются   файлы   TPDEMOB.PAS   и   TPDEMOB.EXE.   Если    вы
программируете   на   Си,   вам  понадобятся  файлы  TCDEMOB.C  и
CDEMOB.EXE (Буква "B" в именах программ означвает  "buggy",  т.е.
"с ошибкой".

Сеанс отладки си-программы
-----------------------------------------------------------------

     В этом разделе в качестве примера используется  программа на
языке  Turbo C.  Если вы программируете на Паскале,  обратитесь к
разделу , в котором описан сеанс отладки программы на языке Turbo
Pascal.

Поиск ошибок    -------------------------------------------------

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

     TCDEMOB

     Программа попросит  вас  ввести   строки   текста.   Введите
следующие две строки:

     one two three
     four five six

     Ввод данных заканчивается последней  пустой  строкой.  После
этого  программа TCDEMOB распечатает на экране результаты анализа
введенного текста:

     Arguments:
     Enter a line (empty line to end): one two three
     (Введите строку (пустую строку для окончания): )
     Enter a line (empty line to end): four five six
     Enter a line (empty line to end):
     Total number of letters = 7
     (Общее количество букв)
     Total number of lines = 6
     (Общее количество строк)
     Total word count = 2
     (Общее количество слов)
     Average number of words per line = 0.3333333
     (Среднее количество слов в строке)
     'E' occurs 1 times, 0 times at start of a word
     ('E' встретилась 1 раз, 0 раз в начале слова)
     'F' occurs 1 times, 1 times at start of a word
     'N' occurs 1 times, 0 times at start of a word
     'O' occurs 2 times, 1 times at start of a word
     'R' occurs 1 times, 0 times at start of a word
     'U' occurs 1 times, 0 times at start of a word
     There is 1 word 3 characters long
     (Имеется 1 слово длиной в три символа)
     There is 1 word 4 characters long

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

Разработка плана действий   -------------------------------------

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

     Теперь, после   того,   как   мы  немного  поразмышляли  над
проблемой и разработали примерный план  действий,  пора  начинать
отладку  программы.  Теперь  наша  задача  состоит  в том,  чтобы
проверить подпрограмму makeintwords и  посмотреть,  правильно  ли
она  разбивает  строку  на  слова  с нулевым окончанием,  а затем
посмотреть,  правильно  ли  подпрограмма  analyzewords  вычисляет
количество слов в анализируемой строке.

Запуск отладчика Turbo Debugger   -------------------------------

     Для того   чтобы   начать   сеанс  отладки  демонстрационной
программы на Си, введите с клавиатуры:

     TD TCDEMOB

     Turbo Debugger    загрузит    демонстрационную    программу,
содержащую  ошибки,  и  отобразит  свой  исходный экран.  Если вы
захотите закончить учебный сеанс и вернуться  в  DOS,  вы  можете
сделать это в любой момент, нажав клавиши Alt-X. Если вы что-либо
безнадежно испортите,  вы сможете в  любой  момент  перезагрузить
демонстрационную  программу  и начать все сначала,  нажав клавиши
Ctrl-F2.  (Заметим,  что при этом не удаляются точки  останова  и
выражения, занесенные в окно слежения.)

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

     Поскольку подпрограмма    makeintwords    выполняет     ясно
определенную  задачу,  можно  легко определить,  правильно ли она
работает, просто проверив содержимое ее выходного буфера. Поэтому
давайте  выберем  второй способ.  Переместите выделяющий курсор к
строке 42 и нажмите клавишу F4, чтобы выполнить программу до этой
строки.  Появится  экран  вывода программы,  и вы должны ввести с
клавиатуры:

     one two three

а затем нажать клавишу Enter.

Проверка значений переменных   ----------------------------------

     Теперь программа остановлена на строке,  которая следует  за
вызовом  подпрограммы  makeintwords.  Давайте проверим содержимое
массива buffer,  и убедимся в том,  что  он  содержит  правильные
значения.  Переместите курсор на одну строку вверх, подведите его
к слову buffer и нажмите клавиши Alt-F10 I  (команда  Inspector),
чтобы открыть окно проверки и посмотреть в нем содержимое массива
buffer.  Для  просмотра  в  окне  элементов  массива  используйте
клавиши перемещения курсора.  Обратите внимание, что makeintwords
действительно помещает один нулевой символ (()) в  конце  каждого
обнаруженного слова.  Это означает,  что теперь следует выполнить
еще один небольшой фрагмент программы  и  убедиться  в  том,  что
подпрограмма  analyzewords  работает  правильно.  Сначала удалите
окно проверки,  нажав клавишу Esc.  Затем дважды нажмите  клавишу
F7,    чтобы   выполнить   программу   до   начала   подпрограммы
analyzewords.

Точки останова   ------------------------------------------------

     Убедитесь в том,  что analyzewords была вызвана с правильным
указателем на буфер,  подведя курсор к слову bufp и нажав клавиши
Alt-F10 I. Вы увидите, что bufp действительно указывает на строку
с нулевым окончанием 'one'.  Удалите окно проверки, нажав клавишу
Esc. Поскольку мы предполагаем, что ошибка возникает при подсчете
количества  символов  и слов,  давайте установим точку останова в
тех местах, где производится подсчет символов и слов.

     1. Переместите курсор на строку 93  и  нажмите  клавишу  F2,
чтобы установить точку останова.

     2. Переместите  курсор  на  строку  97 и установите еще одну
точку останова.

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

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

Окно слежения   -------------------------------------------------

     Запустите программу,  нажав  клавишу  F9.  Работа  программы
остановится,  когда  будет  достигнута  точка  останова  в строке
93.Теперь мы хотим посмотреть на значение  переменной  charcount.
Поскольку  желательно  проверять  ее  значения каждый раз,  когда
будет достигнута точка  останова,  это  идеальный  случай,  чтобы
использовать  команду Watch и занести переменную в окно слежения.
Подведите курсор к слову charcount и нажмите клавиши  Alt-F10  W.
Теперь  в  окне  слежения,  расположенном  в нижней части экрана,
отображается текущее значение данной переменной,  равное  0.  Для
того   чтобы   убедиться,  что  количество  символов  вычисляется
правильно,  выполним одну строку исходного текста,  нажав клавишу
F7.   Окно   слежения  покажет,  что  теперь  значение  charcount
действительно равно 1.

Блок диалога Evaluate/Modify   ----------------------------------

     Снова запустите  программу,  нажав  клавишу  F9.  Вы   опять
возвращаетесь  к  строке  93,  добавив  еще один символ.  Нажмите
клавишу F9 еще два раза,  чтобы ввести последнюю букву в слове  и
нулевое  окончание.  Теперь charcount правильно содержит значение
3,  а массив wordcounts готов обновиться,  чтобы сосчитать слово.
Пока  все прекрасно.  Нажмите клавишу F9,  чтобы начать обработку
следующего слова в буфере. Ага! Что-то не так.

     Вы ожидали,  что программа снова остановится  на  строке  93
,однако этого не произошло. Она выполнилась до оператора, который
выполняет возврат из функции. На строку 99 можно попасть только в
том  случае,  если  условие,  проверяемое в цикле while,  который
начинается на строке 83, больше не выполняется. То есть выражение
*bufp !=0 должно быть ложным.

     Чтобы проверить   это,  вернитесь  обратно  к  строке  83  и
пометьте все выражение *bufp !=0,  поместив курсор на его  первый
символ,  нажав  клавишу  Ins и переместив курсор к его последнему
символу.  Теперь вычислите значение этого выражения,  открыв блок
диалога  Data|Evaluate  Modify  и нажав Enter,  чтобы подтвердить
помеченное выражение.  Его значение действительно равно 0. Дважды
нажмите клавишу Esc, чтобы вернуться в окно модуля.

Эврика!   -------------------------------------------------------

     Теперь пришло время исправлять ошибку.  Причина,  по которой
bufp указывает на 0,  заключается  в  том,  что  внутренний  цикл
while,  начинающийся  на  строке  86,  оставляет этот указатель в
конце  слова.  Чтобы  перейти  к  следующему  слову,   необходимо
нарастить  значение  bufp  после того,  как он будет указывать на
нулевое окончание предыдущего  слова.  Чтобы  это  сделать,  надо
вставить  перед строкой 96 оператор bufp++.  Для этого можно было
бы перекомпилировать программу  с  вставленным  этим  оператором,
однако Turbo Debugger позволяет вставлять в программу выражения с
помощью специальных точек останова.

     Для этого сначала  перезагрузите  программу,  нажав  клавиши
CtrlF2,  чтобы  эксперимент был чистым.  Теперь удалите все точки
останова,  установленные вами в предыдущем сеансе,  нажав клавиши
Alt-B  D.  Перейдите  к строке 97 и снова установите на ней точку
останова,  нажав клавишу  F2.  Теперь  нажатием  клавиш  Alt-V  B
откройте  окно  точек останова.  Определите данную точку останова
таким образом,  чтобы при каждом ее проходе выполнялось выражение
bufp++.


     1. Выберите View|Breakpoints.

     2. Откройте локальное меню окна Breakpoints, нажав Alt-F10.

     3. Выберите   Set|Options   для   открытия   блока   диалога
Breakpoint Options.

     4. Установите селективные кнопки  в положение Execute.

     5. Нажмите Tab для перехода к запросу Action Expression.

     6. Введите выражение bufp++.

     7. Нажмите  Esc  для закрытия диалогового блока и Alt-F3 для
возврата в окно Module.

     Теперь запустите программу,  нажав клавишу F9.  Введите  две
обычные строки:

     one two three
     four five six

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

     Обратите внимание,  что программа стала работать значительно
лучше.  Общее  количество слов и строк осталось неправильным,  но
таблицы  рассчитываются  верно.  Остановите  программу  в  начале
функции  printstatistics  и  проверьте,  правильные  ли  исходные
данные она получает для вывода на  экран.  Сначала  перезагрузите
программу  нажатием  клавиш Ctrl-F2,  чтобы вернуть ее в исходное
состояние.  Затем перейдите к строке 104 и  нажмите  клавишу  F4,
чтобы  выполнить  программу  до  этой  точки.  Подведите курсор к
аргументу nlines и нажмите клавиши Alt-F10 I, чтобы проверить его
значение. Оно равно 6, хотя должно быть равно 2.

     Теперь вернитесь к тому месту,  где происходит вызов функции
из main и снова проверьте значение nlines.  Переместите курсор  к
строке 36, подведите его к слову nlines и нажмите клавиши Alt-F10
I,  чтобы проверить значение.  Здесь значение nlines равно 2, что
является  правильным.  Если  вы  спуститесь ниже к строке 46,  вы
заметите,  что аргументы  nwords  и  nlines  поменялись  местами.
Компилятор никак не мог знать, что вы собирались использовать эти
аргументы в другой последовательности.

     Если вы  исправите эти две ошибки,  программа будет работать
правильно.  Файл TCDEMO.EXE  содержит  исправленную  версию  этой
программы, и, если хотите, вы можете запустить ее.

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

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

Поиск ошибок   --------------------------------------------------

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

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

     TPDEMOB first second third

     Программа попросит   вас   ввести  несколько  строк  текста.
Введите две строки,  которые в точности совпадают в  приведенными
ниже:

     ABC DEF GHI
     abc def ghi

     Последняя пустая строка завершает ввод данных.  После  этого
программа   TPDEMOB  распечатает  на  экране  результаты  анализа
введенных данных:

     9 letter(s) in 3 word(s) in 2 lines
     (9 букв в 3 словах в 2 строках)
     Average of 0.67 words per line
     (В среднем 0.67 слов на строку)
     Word length:  1  2  3  4  5  6  7  8  9  10
     (Длина слова)
     Frequency:    0  0  3  0  0  0  0  0  0  0
     (Частота)

     Letter:       M
     (Буква)
     Frequency:    1   1   1   1   1   1   1   1   1   0   0   0   0
     (Частота)
     Word starts:  1   0   0   1   0   0   1   0   0   0   0   0   0
     (В начале слова)

     Letter        Z
     Frequency:    0   0   0   0   0   0   0   0   0   0   0   0   0
     Word starts:  0   0   0   1   0   0   0   0   0   0   0   0   0

     Program name: C:\td\tpdemob.ex?
     (Имя программы)
     Command line parameters: firs? secon? third
     (параметры командной строки)

     Проанализировав результаты  работы программы,  можно выявить
пять самостоятельных ошибок:

     1. Неправильно подсчитано количество слов (3 вместо 6);

     2. Неправильно вычислено среднее количество  слов  в  строке
(0.67 вместо 3);

     3. В  заголовках второй и третьей таблиц указана только одна
буква (вместо A..M и N..Z);

     4. Вы ввели две строки,  каждая из которых содержит буквы от
A до I, однако в таблицах частоты употребления букв для каждой из
них указано единичное значение.

     5. Последние символы введенных параметров  командной  строки
были  утеряны  и  вместо них отображаются случайные символы (хотя
последний параметр отображается правильно).

Разработка плана действий   -------------------------------------

     Прежде всего  вы  должны  решить,  какую  ошибку   атаковать
первой.  Здесь можно воспользоваться хорошо проверенным правилом,
которое  гласит,  что  начинать  надо  с  той   ошибки,   которая
предположительно   возникает   первой.  В  этой  программе  после
инициализации данных с помощью процедуры Init данные, введенные с
клавиатуры,  считываются функцией GetLine, а затем обрабатываются
процедурой  ProcessLine,  пока  пользователь  не  введет   пустую
строку. Процедура ProcessLine проверяет каждую введенную строку и
изменяет значения глобальных счетчиков.  Затем результаты анализа
отображаются  на  экране  процедурой  ShowResults.  И наконец,  в
полностью независимой подпрограмме процедура ParamsOnHeap создает
в динамической памяти связный список параметров командной строки,
а затем разделяет его на элементы и печатает список  параметров в
конце программы.

     Среднее количество  слов  в  строке  вычисляется  процедурой
ShowResults  на  основе  количества  строк  и  количества   слов.
Поскольку количество слов по-видимому вычисляется неправильно, вы
должны,  прежде   всего   проверить   процедуру   ProcessLine   и
посмотреть,  как  изменяется  значение переменной NumWords.  Даже
если взять неверное значение NumWords,  среднее количество слов в
строке, равное 0.67, является совершенно бессмысленным. Возможно,
ошибка   допущена   в   вычислениях,    выполняемых    процедурой
ShowResults, которая также заслуживает вашего внимания.

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

     И наконец,  после  того,  как  вы тщательно исследуете части
программы,  в которых производится расчет количества слов и букв,
проверьте   процедуру   ParmsOnHeap,   чтобы  найти  и  исправить
последнюю (пятую) ошибку.

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

Запуск отладчика Turbo Debugger   -------------------------------

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

     TD TPDEMOB first second third

     Turbo Debugger    загрузит    демонстрационную    программу,
содержащую ошибки,  и отобразит свой исходный экран,  меню и т.д.
Если вы захотите закончить учебный сеанс и вернуться  в  DOS,  вы
можете сделать это в любой момент,  нажав клавиши Alt-X.  Если вы
что-либо  безнадежно  испортите,  вы  сможете  в   любой   момент
перезагрузить  демонстрационную  программу  и начать все сначала,
нажав клавиши CtrlF2.  (Заметим,  что при этом не удаляются точки
останова и выражения, занесенные в окно слежения.)

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

Навигация по программе   ----------------------------------------

     Итак, мы решили запустить программу и проследить в пошаговом
режиме   выполнение   процедуры   ProcessLine.   Для  этого  надо
воспользоваться одним из нескольких способов.  Можно четыре  раза
нажать  клавишу F8 (чтобы выполнить вызовы процедур и функций, не
заходя в них),  а затем один раз нажать клавишу F7 (чтобы войти в
процедуру  ProcessLine  и начать ее пошаговое выполнение).  Можно
также переместить курсор к строке 231, нажать клавишу F4 (команда
Go to Cursor),  а затем нажать клавишу F7, чтобы начать пошаговое
выполнение процедуры ProcessLine.

     Верится в это или нет,  но этот список можно еще продолжить,
однако  вы  попытайтесь  воспользоваться  только  одним  из  них:
нажмите клавиши Alt-F9 и  на  экране  появится  поле  запроса,  в
которое  надо  ввести  адрес кода,  к которому требуется перейти.
Введите  слово  processline  и  нажмите  Enter.  Программа  будет
выполняться  до тех пор,  пока управление не перейдет к процедуре
ProcessLine.  Введите те же данные, что и раньше, когда программа
просила ввести строку (то есть ABC DEF GHI).

     В этой  подпрограмме имеется несколько циклов.  Внешний цикл
проверяет всю строку.  Внутри этого цикла имеется  цикл,  который
служит для пропуска символов,  которые не являются буквами, и еще
один  цикл,  в  котором  производится  обработка  слов  и   букв.
Переместите курсор к строке 133, в которой начинается цикл while,
и нажмите клавишу F4 (команда Go to Cursor).

     В этом цикле производится проверка введенных данных  до  тех
пор,  пока  не  будет достигнут конец строки или не будет найдена
буква.  Последнее  условие  проверяется  путем  вызова  булевской
функции  IsLetter.  Нажмите  клавишу F7,  чтобы войти внутрь этой
функции.  IsLetter является  вложенной  функцией,  которая  берет
символьное  значение  и  возвращает  True,  если  символ является
буквой,  и False в противном случае.  Даже  не  очень  тщательная
проверка  позволяет  обнаружить,  что  она проверяет только буквы
верхнего  регистра.  Она  должна  была  бы  проверять  символы  в
диапазонах  'A'..'Z'  и  'a'..'z'  или  преобразовывать  символ в
символ верхнего регистра прежде чем выполнять проверку.

     Беглый анализ введенных  строк  текста  дает  дополнительный
ключ к нахождению источника ошибки.  Вы ввели буквы от 'A' до 'I'
как верхнего,  так и нижнего регистров,  однако лишь половина  из
них  была отражена при распечатке результатов.  Теперь вы знаете,
почему это произошло.

     Вернитесь обратно к строке,  в  которой  вызывается  функция
IsLetter,  воспользовавшись  еще  одним приемом отладки:  нажмите
клавиши Alt-F8,  по которым программа выполняется до возврата  из
текущей  процедуры или функции.  Поскольку вторая строка исходных
данных,  abc def ghi,  содержит только символы нижнего  регистра,
каждый  из  них  был воспринят программой как пустой (не буква) и
был пропущен.  Это объясняет получение неверных  результатов  при
подсчете  количества  букв  и слов и открывает первой и четверной
ошибок.

Блок диалога Evaluate/Modify   ----------------------------------

     Между прочим,  существует еще один способ,  который позволит
выявить  неправильную  работу  функции  IsLetter.  Откройте  блок
диалога  Evaluate/Modify,  нажав  клавиши  Alt-D  E   и   введите
следующее выражение:

     IsLetter('a') = IsLetter('A')

     Оба этих символа,  A и a,  являются буквами,  однако функция
возвращает  значение  False,   которое   говорит   о   том,   они
обрабатываются   функцией  IsLetter  неодинаково.  (Блок  диалога
Evaluate/Modify и окно слежения можно использовать для вычисления
выражений, выполнения присваиваний или вызова процедур и функций.
Для получения более подробной информации обратитесь к главе 6).

Проверка значений переменных   ----------------------------------

     Две ошибки позади и три впереди. Ошибку #2 найти значительно
проще, чем две предыдущие. Нажмите клавиши Alt-F8, чтобы выйти из
процедуры ProcessLine,  затем переместите курсор к строке  234  и
нажмите клавишу F4, чтобы выполнить программу до этой строки.

     Программа попросит вас ввести строку.  Введите abc def ghi и
нажмите Enter,  затем нажмите Enter еще раз, когда появится новый
запрос. Теперь нажмите F7, чтобы войти в процедуру ShowResults.

     Напомним, что   мы   пытаемся   определить,  почему  среднее
количество слов в строке отображается неправильно.  Первая строка
в  процедуре  ShowResults  вычисляет  количество  строк  в слове,
вместо того,  чтобы вычислять количество слов  в  строке.  Теперь
понятно: эти два терма надо поменять местами.

     Теперь хорошо бы убедиться в том,  что переменные NumLines и
NumWords содержат те  значения,  которые  вы  ожидаете.  Значение
NumLines  должно быть равно 2,  а значение NumWords (поскольку мы
еще не исправили ошибку в функции IsLetter) должно быть  равно 3.
Подведите  курсор  к  слову NumLines и нажмите клавиши Alt-F10 I,
чтобы проверить значение этой переменной.  В окне проверки  будет
показан  адрес  переменной NumLines,  ее тип и текущее значение в
десятичном   и   шестнадцатиричном    форматах.    Ее    значение
действительно  равно  2,  поэтому  можно  оставить  его в покое и
проверить значение NumWords.  Нажмите  Esc,  чтобы  закрыть  окно
проверки, затем подведите курсор к слову NumWords и снова нажмите
клавиши Alt-F10 I (вместо этого можно было  использовать активную
клавишу  Ctrl-I).  Переменная  NumWords  действительно  имеет  то
значение (неправильное),  которое мы ожидали,  то есть 3, поэтому
ее тоже можно оставить в покое.

     Можно ли?  Есть  ведь  еще  одна проблема,  связанная с этим
вычислением,  и  она  даже  не  включена  в  наш  список.   Перед
выполнением деления программа не производит проверку на равенство
нулю второго терма.  Если  запустить  программу  с  начала  и  не
вводить  никаких  данных  (просто  нажать  Enter  в  ответ  на ее
запрос), то произойдет фатальный сбой (даже если поменять местами
делимое и делитель).

     Чтобы убедиться в этом,  нажмите клавишу Esc,  чтобы закрыть
окно проверки,  нажмите клавиши Alt-R P,  чтобы завершить текущий
сеанс  отладки,  нажмите клавишу F9,  чтобы запустить программу с
начала,  а затем в ответ  на  запрос  программы  TPDEMOB  нажмите
Enter.  Работа  программы  будет остановлена и на экране появится
сообщение об ошибке периода выполнения.  Чтобы исправить  ошибку,
вы должны модифицировать данный оператор следующим образом:


     if NumLines <> 0 then
       AvgWords := NumWords / NumLines
     else
       AvgWords := 0;

     Теперь покончено  с  ошибками #2 и #2б.  Но раз уж вы начали
работать  с  окном  слежения,  попробуйте  использовать  его  для
просмотра  содержимого  структуры  данных.  Переместите  курсор к
описанию массива LetterTable в  строке  50.  Подведите  курсор  к
слову  LetterTable и нажмите клавиши Alt-F10 I.  Вы увидите,  что
это массив записей,  содержащий 26 элементов. Для просмотра всего
массива  можно  прокручивать  содержимое  окна  с  помощью клавиш
управления курсором и нажимать Enter,  чтобы  проверить  значение
нужных  элементов.  Это  очень  мощное средство проверки структур
данных,  и оно еще пригодится нам для проверки  связного  списка,
созданного процедурой ParmsOnHeap.

Слежение за переменными   ---------------------------------------

     Теперь перейдем  к  поиску  ошибки  в процедуре ShowResults,
которая приводит к неправильному отображению  заголовков столбцов
в  таблицах  (ошибки  #3).  Поскольку  работа  программы уже была
остановлена в результате возникновения ошибки  деления  на  ноль,
подготовьте ее к следующему сеансу,  нажав клавиши Alt-R P (чтобы
вернуть программу в исходное состояние).  Затем  нажмите  клавиши
AltF9, введите showresults и нажмите Enter. Теперь введите все те
же данные ABC DEF GHI и нажмите Enter еще раз. И наконец, введите
abc def ghi и дважды нажмите Enter.  Теперь Turbo Debugger должен
остановить программу в начале процедуры ShowResults.

     ShowResults использует вложенную  процедуру  ShowLetterInfo,
которая отображает таблицы букв. Переместите курсор вниз к строке
103, нажмите клавишу F4, а затем нажмите клавишу F7, чтобы начать
выполнение процедуры ShowLetterInfo в пошаговом режиме.

     Она содержит  три  цикла  for.  В  первом цикле отображаются
заголовки  столбцов,  а  во  втором  и  в   третьем   -   частота
употребления букв.  Используйте клавишу F7,  чтобы войти в первый
цикл,  который  начинается  в  строке  62.  Подведите  курсор   к
переменным FromLet и ToLet и с помощью клавиш Alt-F10 I проверьте
их значения.  Они выглядят вполне нормально  (первая  равна  'A',
вторая  равна  'M').  Нажмите  клавиши Alt-F5,  чтобы просмотреть
экран пользователя и убедиться в том,  что все осталось на месте.
Нажмите любую клавишу, чтобы вернуться в окно модуля.

     Для проверки   циклов,   подобных   данному,   очень  удобно
использовать окно слежения. Подведите курсор к слову ch и нажмите
клавиши  Ctrl-W.  Теперь  с  помощью клавиши F7 начните выполнять
цикл for.  Как и ожидалось,  выполняется оператор Write в  строке
64.  Однако  если  теперь  вы  посмотрите  на  окно слежения,  вы
увидите, что значение ch уже равно 'M' (то есть цикл уже выполнен
полностью!).  Все  дело  в лишней точке с запятой,  которая стоит
сразу после зарезервированного  слова  do.  Из-за  нее  цикл  for
выполняется  13  раз  подряд,  совершенно ничего не делая.  Когда
управление  передается  оператору  Write  в  строке  64,  текущее
значение  переменной  ch  ('M') выводится на экран,  и выполнение
программы продолжается.  Удалив эту лишнюю точку  с  запятой,  мы
устраним ошибку #3.

И еще одна ошибка...   ------------------------------------------

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

     Для поиска  ошибки  воспользуемся  окном  слежения.  Нажмите
клавиши Alt-F9,  введите parmsonheap и нажмите Enter. В цикле for
просматриваются все параметры командной строки, создается связный
список,  и  каждая  строка  копируется  в  динамическую  область.
Указатель Head указывает на  начало  списка,  Tail  указывает  на
последний узел списка,  а Temp используется в качестве временного
буфера для  выделения  и  инициализации  нового  узла.  Поскольку
данные строки были запорчены,  нажмите клавишу Ctrl-F7 и занесите
следующее выражение в окно слежения:

     Tail^.Parm^

     Оно позволит нам следить за данными строки, которые хранятся
в   последнем   узле  списка.  Конечно  же,  это  значение  будет
бессмысленным,  пока переменная Tail не будет инициализирована  в
строке 207.

     Вместо того,  чтобы  выполнять процедуру в пошаговом режиме,
просто будем проверять значение  в  окне  слежения  после  каждой
итерации.  Переместите  курсор к строке 208 и нажмите клавишу F2,
чтобы установить на ней точку останова.  Теперь  нажмите  клавишу
F9, чтобы выполнить программу до этой строки. Если вы используете
операционную систему DOS версии 3.x,  вы увидите в окне  слежения
полный  маршрут  программы  TPDEMOB.EXE  (если вы используете DOS
версии 2.x,  вы увидите пустую строку;  в этом случае просто  еще
раз нажмите клавишу F9,  чтобы возобновить выполнение программы).
Данные строки выглядят очень хорошо.

     Нажмите клавишу F9,  чтобы выполнить цикл еще раз.  И  снова
данные выглядят хорошо. Теперь вы знаете, что данные копируются в
динамическую область  правильно.  Чтобы  проверить,  не  были  ли
данные уже запорчены, можно использовать окно проверки. Подведите
курсор к слову Head в строке 203 и нажмите клавиши Alt-F10 I.

     Нажав клавишу  Enter  и  затем  "стрелку  вниз",   проверьте
значение,   на  которое  указывает  переменная  Parm.  Вы  видите
содержимое первого узла списка,  и данные  строки  уже  искажены.
Если вы нажмете клавиши Esc, "Стрелка вниз", а затем снова Enter,
вы откроете окно проверки для второго узла списка. Нажмите Enter,
чтобы проверить данные строки.  Данные остались нетронутыми и это
именно  тот  узел,  на   который   указывает   переменная   Tail.
Определенно происходит что-то странное с концом строки.

     По мере  пошагового  выполнения  цикла  с помощью клавиши F7
следите за содержимым окна слежения.  "Виновником" является вызов
функции GetMem в строке 199. До этого вызова значение Tail^.Parm^
равно first.  Сразу же  после  вызова  GetMem,  последний  символ
строки пропадает.

     Что же  происходит?  Для  каждого параметра командной строки
цикл for выделяет сначала запись,  затем строковые данные,  затем
следующую запись и т.д.  Функция GetMem, вызываемая в строке 199,
должна выделять достаточно места для размещения самой строки и ее
байта  длины,  однако,  как  вы  можете видеть,  она не добавляет
единицы к значению Length(s).  И  хотя  оператор  присваивания  в
строке 200 правильно выполняет копирование строки,  он фактически
берет на 1 байт больше чем  было  выделено  памяти.  Поэтому  при
вызове   процедуры   New(Temp)  последний  символ  каждой  строки
перекрывается  первым  байтом  следующей  записи,  размещенной  в
памяти. Последний параметр остается нетронутым, потому что за ним
не следует еще одна запись ParmRec.

     Вот и все ошибки в этой программе (известные).  Возможно,  с
помощью  пошаговой отладки вам удастся обнаружить еще что-нибудь.
Вы можете исправить ошибки (для удобства они  помечены  в  тексте
двумя звездочками) и перекомпилировать программу,  либо запустить
программу TPDEMO.PAS,  описанную в главе 3, в которой отсутствуют
указанные ошибки.
