Text
                    
стшь,
РАЗРАБОТКА,
ЭФФЕКТИШС
ОТ/1АПКА
И ИСЛЬПШИ
ПРОГРАММ


ВАН ТАСШ1 Д. Перевод с английского Е. К. МАСЛОВСКОГО, В. А. ПРОНИНОЙ под редакцией Э. А. ТРАХТЕНГЕРЦА жхпе'Юю w
PROGRAM STYLE, DESIGN, EFFICIENCY, DEBUGGING, AND TESTING Second Edition Dennie Van Tassel University of California Santa Cruz, California Prentice-Hall, Inc. Englewood Cliffs, New Jersey 1878
УДК 681.32 •Ф7.3 Т23 Ван Тассел Д- Т23 Стиль, разработка, эффективность, отладка и испытание про- грамм: Пер. с англ. — М.: Мир, 1981.—320 с., ил. В книге американского автора рассмотрен широкий круг вопросов, ка- сающихся стиля написания программ, рациональных методов их разработ- ки и оптимизации, стратегии отладки и тестирования. Приводимые реко- мендации основаны на многолетнем опыте автора. Для специалистов с разной подготовкой — от начинающих программи- стов до профессионалов, а также для студентов, обучающихся программи- рованию, ; Al Jb - . „ 30502-142 _ Т 041(01)-81 142’81, **• 1 1502000000 6Ф7.3 Редакция литературы по новой технике © 1978 by Dennie Van Tassel © Перевод на русский язык, «Мир>, 1981
ПРЕДИСЛОВИЕ РЕДАКТОРА ПЕРЕВОДА В каждой области знаний имеется набор методических прие- мов, которыми должен владеть хороший специалист. Данная книга посвящена описанию таких приемов и правил их применения в про- граммировании. Весь процесс программирования — от постановки задачи до получения рабочей программы — автор делит на этапы и для каждого из них рассматривает методы и приемы, облегчаю- щие их выполнение. Для получения необходимых навыков читателю предлагается большое число упражнений разной степени трудности. Книга является прекрасным руководством по программирова- нию. Особую ценность она представляет для студентов и начинаю- щих программистов, так как помогает им в практическом развитии навыков программирования, ориентируя в направлении создания удобочитаемых, эффективных и правильных программ. Большинство методических указаний, содержащихся в данной книге, не являются новыми для квалифицированных программи- стов. Тем не менее пособие принесет пользу и этому контингенту читателей, так как позволит им систематизировать и уточнить свои приемы и методы разработки программ. Книга также представля- ет интерес для преподавателей по программированию как пособие для чтения лекций и ведения практических занятий. Предисловие и гл. 1—3 переведены канд. техн, наук В. А. Про- ниной, гл. 4—6 и приложение — канд. техн. наук. Е. К. Маслов- ским. Большие трудности при переводе возникли из-за значитель- ного числа новых терминов, введенных автором, которые не имеют устоявшихся эквивалентов в русском языке. Э. А. Трахтенгерц
Посвящается моей жене Цинтии ПРЕДИСЛОВИЕ Эта книга предназначена для тех, кто уже умеет программиро- вать, но хочет повысить свой профессиональный уровень, и явля- ется по существу руководством по программированию. В книге нашли отражение пять вопросов, редко рассматривае- мых в литературе для начинающих программистов: стиль (или удобочитаемость), проектирование, эффективность (или оптимиза- ция) , отладка и тестирование программ. Знание этих вопросов при- ходит с опытом. Опыт является хорошим учителем, но обучающим довольно медленно и бессистемно. Большинство из тех, кто умеет программировать, убедились в том, что нельзя полностью ла него полагаться в изучении столь важных вопросов. Поскольку книга выходит вторым изданием, отметим его основ- ные отличия от первого издания. Глава 1 «Стиль программирования» расширена и частично пе- реработана. Глава 2 «Проектирование программ» полностью пере- работана с использованием совершенно нового материала: особое внимание уделяется методу проектирования сверху вниз и струк- турному программированию. Глава 3 «Эффективность программ» выдержала испытание временем и претерпела лишь незначитель- ные изменения. Глава 4 «Отладка программ» переработана немно- го: в центре внимания находится вопрос о правильности написа- ния программы с первого раза. Глава 5 «Тестирование программ» переделана довольно значительно: особо отмечены преимущества метода тестирования сверху вниз; обсуждаются также современ- ные, более сложные методы тестирования программ. Глава 6 «101 задача для программиста» также переработана, расширена, зада- чи систематизированы; хотя здесь приведена 101 задача, в дейст- вительности их значительно больше, поскольку в некоторых из них предлагается разработать несколько программ. Задачи этой главы рассчитаны на любой уровень знаний читателя и охватыва- ют достаточно полный набор вопросов по программированию. Уп- ражнения в конце каждой главы претерпели существенные изме- нения. Они пересмотрены, перекомпонованы и расширены за счет добавления новых задач. В конце каждой главы помещены советы программисту, отра- жающие ключевые положения главы. Справочно-библиографичес- кий материал первого издания также подвергся изменениям. Мне было приятно работать над вторым изданием книги, пото- му что она мне очень нравится (обычно авторы не любят своих
ПРЕДИСЛОВИЕ 7 произведений), и, кроме того, у меня накопилось много нового материала. По-видимому, книгой широко пользовались, так как я получил многочисленные предложения по ее улучшению. Судя по отзывам, первое издание книги привлекло внимание широкого круга читателей: от профессиональных программистов, работающих с книгой самостоятельно, до лиц, обучающихся на курсах по программированию. В связи с этим упражнения, приво- димые в конце каждой главы, я разделил на четыре категории: «Повторение пройденного», «Задания», «Программы» и «Проекты». Упражнения первого типа служат для того, чтобы выяснить, насколько хорошо усвоен материал главы. Обучающийся должен быть в состоянии ответить на предлагаемые вопросы устно, не прибегая к помощи карандаша и бумаги. Упражнения второго типа требуют большего времени на обду- мывание, и, как правило, необходимы карандаш и бумага, чтобы решить задачи и написать ответы. Многие из этих задач сущест- венно помогают в процессе изучения материала, поэтому читателю предлагается попытаться решить их или по крайней мере про- честь. /” Небольшие вычислительные программы, связанные с темой гла- вы,— упражнения третьего типа. Составление этих программ не отнимет много времени, они могут быть предложены для ежене- дельных домашних заданий. К упражнениям последнего типа относятся программы или проекты, для которых требуются значительные усилия и время. Многие из проектов могут быть разработаны с различной степенью детализации, т. е. можно дать или краткую, или очень полную вер- сию решения в зависимости от времени и желания. Кроме того, при решении этих задач зачастую необходимо использование лите- ратурных источников. В приложении подробно обсуждаются возможности коллектив- ной разработки предлагаемых проектов. Я признателен многим читателям, которые помогли мне при создании второго издания, особенно Ричарду Остингу из Мэри- лендского университета и Брайену Кернингену из лаборатории Белла. • Денни Ван Тассел
г Цель программирования —не создание программы, а получение результатов вычисления. Кодирование, увы, само по себе ничего не стоит — существенны результаты! Глава 1 СТИЛЬ ПРОГРАММИРОВАНИЯ Когда-нибудь электронные вычислительные машины (ЭВМ) бу- дут сами составлять для себя программы или же мы будем писать их на естественном языке. Однако сейчас нам приходится разра- батывать программы, которые смогли бы читать другие лица. Не многие стали бы возражать против той простой мысли, что про- граммист должен быть в состоянии прочесть по крайней мере свою собственную программу. Отсюда следует, какое важное значение имеет стиль написания программ. ' Стиль программирования связан с удобочитаемостью програм- мы. Если бы каждый программист придерживался своего особого стиля, то программы были бы недоступны для других. Под стилем мы будем подразумевать набор приемов или методов программи- рования, которые используют опытные программисты, чтобы полу- чить правильные, эффективные, удобные для применения и легко- читаемые программы. Правила хорошего стиля программирова- ния— это результат соглашения между опытными программиста- ми. Когда программист усвоит определенный стиль программиро- вания, его программа-значительно легче для восприятия. Если бы все программисты придерживались своего индивидуального стиля, то результатом было бы вавилонское столпотворение. Программы должны составляться таким образом, чтобы их мог- ли прочитать в первую очередь люди, а не машины. Людям это необходимо для корректировки, применения и модификации про- граммы. Если бы мы были связаны только с машинами, то про- граммы были бы написаны так, чтобы машина могла читать их легче, чем человек. Кроме того, программа — это документ для последующего использования, учебный материал по кодированию алгоритмов и средство для дальнейшей разработки более совер- шенных программ. Следовательно, языки программирования дол- жны обеспечивать возможность создания удобочитаемых операто- ров. Слишком часто, стремясь побыстрее получить работающую программу, забывают о ее удобочитаемости.
10 ГЛАВА 1 Помните, программы читаются людьми. Вряд ли кто-нибудь согласится с тем, что удобочитаемость про- грамм не существенна. Программисты должны быть всегда в со- стоянии прочесть свои программы. В этом им должны помогать стандарты стиля. В естественном языке для улучшения читаемости текста используются знаки пунктуации, разделение на параграфы, упорядочение и интервалы. Программисты могут пользоваться по- добными средствами, чтобы результатом их работы не оказалась трудная для восприятия программа. Трудночитаемые программы обычно сложно модифицировать, особенно если это приходится делать не автору программы. Как правило, легче полностью переписать чужую программу, чем ее модифицировать. Спецификации программы в общем случае по- стоянно изменяются. Часто мы не только не знаем с самого на- чала, чего хотим, но и после получения результатов появляется же- лание изменить программу. Как правило, к разработке программы приступают со скромными целями, а в дальнейшем постоянно расширяют ее возможности. Следуя определенному стилю про- граммирования, можно избежать некоторых трудностей, возникаю- щих при разработке и модификации программ. Когда программист обращается к чужой программе и видит, что она хорошо организована и легкочитаема, то естественные от- рицательные эмоции, вызванные необходимостью обновления или модификации этой программы, отступают. Легкочитаемая програм- ма создает впечатление, что ее автор хорошо знал, что делал. Программа должна передавать логику и структуру алгоритма на- столько, насколько это возможно. Если программы составляются для какой-либо организации, то применение согласованного стиля поможет сделать их достоянием этой организации, а не личной собственностью отдельногопрограм- миста. 1.1. СТАНДАРТИЗАЦИЯ СТИЛЯ Один из аргументов против стандартизации стиля программи- рования звучит так: стиль программирования — это вопрос лично- го мнения и вкуса, поэтому не следует вводить на него каких-либо ограничений. Этот аргумент в сущности утверждает, что хаос луч- ше порядка. Правило стандартизации стиля заключается в следующем: если существует более одного способа сделать что-либо и выбор произ- вольный, остановитесь на одном способе и всегда его придержи- вайтесь. Преимущества такого подхода состоят в том, что, исключая произвольные параметры, можно сделать связи более точными и,
СТИЛЬ ПРОГРАММИРОВАНИЯ 11 делая одно и то же каждый раз одинаковым образом, легче избе- жать путаницы. Однако процессу стандартизации свойственны и недостатки: применение стандартов может замедлить будущее развитие средств программирования; стандарты могут оказаться ограниченными или слишком громоздкими для универсального применения. Предлагаемые в книге стандарты стиля являются результатом здравого смысла и опыта программистов, а не правилами, раз и навсегда зафиксированными. . Хороший набор стандартных приемов может стимулировать бу- дущие разработки, поскольку позволит сконцентрировать внима- ние на действительно новых задачах. Например, не надо больше думать о выборе способа записи циклов. По-видимому, предпринято недостаточно усилий для установле- ния единого промышленного стандарта стиля для всех разработок, но устойчивые стандарты в пределах одной разработки — обычное явление. Многими рекомендациями, приводимыми в этой книге, можно воспользоваться для установления таких стандартов. Если хорошо изучить стандарты, то потребуется немного дополнитель- ных усилий для их применения. 1.2. КОММЕНТАРИИ Желательность^комментариев, казалось бы, очевидна, однако далеко не всегда их включают в программу. Комментарии опуска- ют с целью экономии времени. Иногда утверждают, что «коммен- тарии будут вставлены позже». Но такая отговорка неубедитель- на, потому что через удивительно короткое время авторы програм- мы обнаруживают, что забыли ее многие детали. Программы с по- яснительными комментариями значительно легче отлаживать, так как они содержат дополнительную информацию для работы с про- граммой. Просматривая чужую программу, программист часто тра- тит много времени, отслеживая логику программы или просто пе- реписывая недокументированную программу, если необходимо вне- сти в нее изменения. В этом случае все первоначально «сэконом- ленное» время расходуется с превышением во много раз. Некомментируемая программа — это, вероятно, наихудшая ошибка, которую может сделать программист, а также свидетель- ство дилетантского подхода (пусть даже программист имеет деся- тилетний опыт работы); более того, это веская причина для уволь- нения программиста. Последнее утверждение может показаться слишком сильным, но, вероятно, многие руководители одобрили бы его. Комментарии подобны ориентирам в незнакомом лесу. Только неразумный не оставляет ориентиров, затрудняя таким образом отладку и тестирование программы. Когда следует писать комментарии? Хорошее правило — вклю- чать комментарии в процессе написания -программы, Именно в это
12 ГЛАВА 1 время вы в наибольшей степени вникаете во все детали програм- мы. Редко удается получить удовлетворительные результаты при более поздней вставке комментариев: при этом приходится вспоми- нать, что следует прокомментировать. Общее правило при написа- нии комментариев — чем больше комментариев, тем лучше. Очень немногие программы бывают перенасыщены комментариями. : Делайте комментариев больше, чем это кажется необходимым. Хорошие комментарии написать непросто. Так как цель ком- ментариев— облегчить понимание программы, — они должны быть так же хорошо продуманы и проработаны, как и кодировка про- граммы. Многие комментарии могут быть перенесены из первона- чальной разработки проекта, если он создается методом сверху вниз (см. гл. 2). Спецификации проекта описывают программу, и часто можно воспользоваться некоторыми из них для составлен ния комментариев. Комментарии нужны как на стадиях проекти- рования и отладки программы, так и позже. В связи с этим бес- смысленно вставлять комментарии после того, как программа за- вершена. Это подобно изучению маршрута по карте после оконча- ния путешествия. Если вы испытываете трудности при составлении комментариев для описания того, что вы делаете, то скорее всего вы «не ведаете, что творите». Существуют три типа комментариев: вводные, оглавления и пояснительные. 1.2.1. ВВОДНЫЕ КОММЕНТАРИИ Каждая программа, подпрограмма или процедура должна на- чинаться с комментариев, поясняющих, что она делает. Минималь- ная информация, содержащаяся в вводных комментариях, должна включать следующие пункты: 1. Назначение программы. 2. Указания по вызову программы и ее использованию. 3. Список и назначение основных переменных или массивов. 4. Указания по вводу-выводу. Список всех файлов. 5. Список используемых подпрограмм. 6. Название применяемых математических методов, а также ссылки на литературные источники, где содержится их описание. 7. Сведения о времени выполнения программы. 8. Требуемый объем памяти. 9. Специальные указания оператору. 10. Сведения об авторе. 11. Дату написания программы. Эти данные необходимы для документирования программы, и наилучшим местом для размещения этой информации является сама программа. На рис. 1.1 показан пример подобной докумен- тации. Делайте вводные комментарии.
’ TALL 10 /“* а,а.а.и.^4,****************#**************** р^***********************Й**** TALL TALL 20 30 С с ПОДПРОГРАММА TALLY TALL TALL 40 50 с TALL- 60 с с Ut ВЫЧИСЛИТЬ СУММУ. СРЕДНЕЕ ЗНАЧЕНИЕ, СТАНДАРТНОЕ TALL 70 с ОТКЛОНЕНИЕ. МАКСИМУМ, МИНИМУМ ДЛЯ КАЖДОЙ ПЕРЕ- TALL 80 <С МЕННОЙ ИЗ МНОЖЕСТВА НАБЛЮДЕНИЙ TALL 90 TALL 100 с ИСПОЛЬЗОВАНИЕ , TALL 110 с CALL TALLA (A, S, TOTAL, AVER, SD, VMIN, VMAX, NO, TALL 120 N\ , IER) TALL 130 TALL 140 с ОПИСАНИЕ ПАРАМЕТРОВ TALL 150 С’ A . - МАТРПНА НАБЛЮДЕНИИ, NO x NV TALL 160 с S - ВХОДНОЙ ВЕКТОР НАБЛЮДЕНИЙ, УКАЗЫВАЮЩИЙ TALL 170 (J ПОДМНОЖЕСТВО А МНОЖЕСТВА А. РАССМАТРИВА- TALL 180 с ЮТСЯ ТОЛЬКО ТЕ НАБЛЮДЕНИЯ, ДЛЯ КОТОРЫХ TALL 190 S (J ) - НЕНУЛЕВОЕ. ДЛИНА ВЕКТОРА РАВНА NO TALL 200 TOTAL - ВЫХОДНОЙ ВЕКТОР СУММ КАЖДОЙ ПЕРЕМЕННОЙ. TALL 210 с ДЛИНА ВЕКТОРА РАВНА NV TALL 226 с А\ ER - ВЫХОДНОЙ ВЕКТОР СРЕДНИХ ЗНАЧЕНИЙ КАЖДОЙ TALL 236 ПЕРЕМЕННОЙ. ДЛИНА ВЕКТОРА РАВНА NV TALL 240 с . SD - ВЫХОДНОЙ ВЕКТОР СТАНДАРТНЫХ ОТКЛОНЕНИЙ TALL 250 € КАЖДОЙ ПЕРЕМЕННОЙ. ДЛИНА ВЕКТОРА РАВНА NV TALL 260 С VM IN - ВЫХОДНОЙ ВЕКТОР МИНИМАЛЬНЫХ ЗНАЧЕНИЙ TALL 270 С ' КАЖДОЙ ПЕРЕМЕННОЙ. ДЛИНА ВЕКТОРА РАВНА NV TALL 280. € VMAX - ВЫХОДНОЙ ВЕКТОР МАКСИМАЛЬНЫХ ЗНАЧЕНИЙ TALL 290 С КАЖДОЙ ПЕРЕМЕННОЙ. ДЛИНА ВЕКТОРА РАВНА NV TALL 300 с NO - КОЛИЧЕСТВО НАБЛЮДЕНИЙ TALL 310 С NV - ЧИСЛО ПЕРЕМЕННЫХ ДЛЯ КАЖДОГО НАБЛЮДЕНИЯ TALL 320 с IER = 0/ЕСЛИ НЕТ ОШИБКИ TALL 330 с = 1, ЕСЛИ S = 0, VMIN = -1.Е 75, VMAX = SD= AVER = TALL 340 с = 1.Е75 TALL 350 с = 2. ЕСЛИ S ИМЕЕТ ЕДИНСТВЕННЫЙ НЕНУЛЕВОЙ TALL 360 с ЭЛЕМЕНТ, VMAX = VMIN, SD = 0. 0 TALL 370 -с TALL 380 с ЗАМЕЧАНИЯ TALL 390 с НЕТ TALL 400 с - TALL 410 с ТРЕБУЕМЫЕ ПОДПРОГРАММ^ И ФУНКЦИИ TALL 420 с НЕТ TALL 430 с TALL 440 с МЕТОД TALL 450 с АНАЛИЗИРУЮТСЯ ВСЕ НАБЛЮДЕНИЯ, СООТВЕТСТВУЮЩИЕ TALL 460 с НЕНУЛЕВОМУ ЭЛЕМЕНТУ ВЕКТОРА S ДЛЯ КАЖДОЙ ПЕРЕ- TALL 470 с МЕННОЙ В МАТРИЦЕ А. НАКАПЛИВАЮТСЯ СУММЫ И НАХО- TALL 480 с ДЯТСЯ МИНИМАЛЬНЫЕ И МАКСИМАЛЬНЫЕ ЗНАЧЕНИЯ. ДАЛЕЕ TALL 490 с ПОДСЧИТЫВАЮТСЯ СРЕДНИЕ ЗНАЧЕНИЯ И СТАНДАРТНЫЕ TALL 500 € ОТКЛОНЕНИЯ. ДЕЛИТЕЛЬ ДЛЯ СТАНДАРТНОГО ОТКЛОНЕ- TALL 510 с НИЯ НА ЕДПШИЬ МЕНЬШЕ ЧИСЛА ИСПОЛЬЗУЕМЫХ НАБЛЮ- TALL 520 € НИЙ TALL 530 С TALL 540 С ССЫЛКИ TALL 550 с MURRAY R. SPIEGEL. STATISTICS TALL 560 с SCHAUM PUBLISHING COMPANY TALL 570 •С TALL 580 с ТРЕБУЕМОЕ ВРЕМЯ TALL 590 с ДЛЯ 10 ПЕРЕМЕННЫХ - 20 СЕКУНД TALL 600 с ДЛЯ 20 ПЕРЕМЕННЫХ - 45 СЕКУНД TALL 610 с хз ДЛЯ 65 ПЕРЕМЕННЫХ - 95 СЕКУНД TALL 620 € с ( с °АЗМЕР 120 КАРТ ОБЪЕКТНЫЙ КОД 1960 TALL TALL TALL TALL 630 640 650 660 с € с ПРОГРАММИСТ БЕЗОШИБОЧНЫЙ КОДИРОВЩИК TALL TALL TALL 670 680 690 € С С ДАТА НАПИСАНИЯ ИЮЛЬ 1978 TALL TALL TALL 700 710 720 с TALL TALL 730 740 / ' TALL 750 Рис. 1.1. Подпрограмма TALLY.
14 ГЛАВА 1 1.2.2. ОГЛАВЛЕНИЕ Если программа очень большая, то целесообразно в ее начале помещать оглавление в виде комментариев. Оглавление должно содержать название, размещение и функцию каждого программно- го модуля. Естественно, что модули должны быть заранее снабже- ны именами или комментариями, указывающими их функции, Делайте оглавление в больших программах. 1.2..3. ПОЯСНИТЕЛЬНЫЕ КОММЕНТАРИИ Пояснениями нужно сопровождать те части программы, кото- рые трудно понять без комментариев. Перед существенными для понимания логики программы циклами или условными оператора- ми должны появляться комментарии с указаниями действия, кото- рое будет производиться. Надлежащим образом составленные ком- ментарии обеспечивают словесное описание логики программы и изменения данных. Сопровождайте комментариями те действия, которые, с вашей точки зрения, могут быть не совсем понятны дру- гому. Эта документация будет всегда находиться вместе с про- граммой. Она поможет другому программисту понять вашу про- грамму, а вам вспомнить написанные ранее разделы программы в. в то время, когда вы работаете над ее новыми разделами. Сред- ней нормой можно считать одну строку комментариев на десять строк программы, написанной на языке высокого уровня. Это вовсе не означает, что после каждых десяти строк програм- мы следует давать одну строку комментариев. Каждый логически выделенный кусок программы следует комментировать. Считается, что программы на языках высокого уровня легко читаются и как бы сами себя документируют, однако часто логи- ка одного программиста — автора программы — не очевидна для другого. В таких случаях (а их значительно больше, чем кажется) роль комментариев неоценима. Комментарии следует давать в та- ких местах программы, которые хочется пояснить, когда вы знако- мите кого-либо со своей программой. Весьма существенно содержание комментариев. Нет необходи- мости переводить с английского каждый оператор программы. Счи- тается, что читатель знаком с языком программирования. Следо- вательно, комментарии должны объяснять цель группы операто- ров программы, а не описывать действия, производимые этими операторами. /* ПРОВЕРИТЬ, ЯВЛЯЕТСЯ ЛИ ВЕЛИЧИНА ОТРИЦАТЕЛЬНОЙ */
СТИЛЬ ПРОГРАММИРОВАНИЯ 15 Это плохой комментарий, потому что читающий программу зна- ком с языком программирования и в состоянии определить, что имеет место такая проверка. Но он не знает, зачем это делается. Предполагается, что именно комментарий должен ответить на этот вопрос. Оператор программы сообщает, какая операция выполня- ется, комментарии же должны пояснить ее цель. Вместо вышепри- веденного бесполезного комментария следовало бы дать такой: у* ВЫПОЛНИТЬ ОБРАБОТКУ ОТРИЦАТЕЛЬНОГО САЛЬДО (СУММАРНЫЕ РАСХОДЫ ПРЕВЫШАЮТ ДОХОДЫ.) ♦/ Этот комментарий сообщает, зачем должна быть сделана про- верка. Комментарии не должны объяснять синтаксис языка про- граммирования, а должны указывать цель действия или объяснять логику программы. Делайте комментарии, содержащие полезную информацию. комментарии должны содержать некоторую дополнительную информацию, а не перефразировать программу. О качестве комментариев можно судить по тому, понятна ли логика программы только на основании комментариев (без обра- щения к какой-либо другой документации). Одна из причин сла- бой комментируемое™ программы — переоценка наших возможно- стей. Мы уверены, что легко вспомним логику той или иной части программы. Более того, мы не ожидаем большого количества оши- бок в нашей программе, и комментарии кажутся нам излишними. Однако опыт говорит об обманчивости подобных ожиданий. 1.2.4. РАСПОЛОЖЕНИЕ КОММЕНТАРИЕВ При составлении программ на языке ассемблера комментария- ми сопровождается большинство строк. Комментарии, которые пе- ремежаются с текстом программы, легче читать, когда они выде- лены пустыми строками (до и после комментариев). Дополнитель- ный метод выделения комментариев — заключение их в прямо- угольник из специальных символов. Такой прямоугольник может быть использован в нескольких случаях: 1. Чтобы поместить в этот прямоугольник комментарии. 2. Чтобы сгруппировать множество команд. Это достигается расположением до и после этой группы команд строк комментари- ев, заполненных специальными символами. 3. Чтобы указать, что комментарий относится к нескольким строкам программы. Для выделения комментариев, приведенных на рис. 1.1, исполь- зуются строки, состоящие из звездочек, расположенные до и пос- ле комментариев. Если комментарии включают в строку текста программы, то используют установленную позицию (колонку) для начала каждо*
16 ГЛАВА 1 го комментария. Например, начинают комментарии с 40-й позиции, оставляя для текста программы позиции с 1-й по 39-ю. В строке комментариев для простоты распознавания желательно начинать текст комментариев, отступив от позиции начала строк операто- ров программы. Для структурированных программ (гл. 2) обычно требуется меньше комментариев, чем для неструктурированных, так как про- граммы первого вида понятнее и в них меньше переходов. Раньше часто прибегали к комментариям, чтобы объяснить бес- порядочное кодирование. Однако очевидно, что в этом случае на- до не комментировать, а перепрограммировать — упорядочить программу. Структурное программирование выявляет логику программы разделением ее на параграфы; комментарии должны быть разме- щены так, чтобы не мешать введению параграфов. Иногда разме- щают комментарии справа от программы. При этом необходимо делать такой отступ от строки программы, чтобы это не мешала структурированию программы. Располагайте комментарии таким образом, чтобы это не делало ее менее наглядной. 1.2.5. ПРАВИЛЬНЫЕ КОММЕНТАРИИ Комментарии должны быть правильными. Другими словами, они должны быть правильными сначала и изменяться в соответст- вии с изменениями программы. Очевидно, что неправильные ком- ментарии— это хуже, чем их отсутствие, поскольку такие коммен- тарии вводят в заблуждение. Неправильные комментарии хуже, чем их' отсутствие. 1.3. ПРОПУСК СТРОК Пропуск строк — часто недостаточно оцениваемый метод улуч- шения наглядности программ. Этот метод можно использовать для вертикальной разрядки. Как в естественном языке мы пользуемся пропуском строк для отделения параграфов, так и в программе таким образом можно разделять ее отдельные фрагменты. Пропус- ком одной строки можно отделять каждую группу логически свя- занных операторов, пропуском двух строк — основные логические фрагменты программы. Использование незаполненных строк облег- чает поиск отдельных частей в программе. Пропуск строки должен следовать за каждой командой безус- ловной передачи управления, указывая на нарушение последова- тельности выполнения команд. Пропуск строк до и после коммен- тариев помогает выделить последние. Пропуск строк можно обес-
СТИЛЬ ПРОГРАММИРОВАНИЯ 17 печить, вложив пустые карты в исходную колоду или пустыми строками комментариев. Некоторые компиляторы языков КОБОЛ, ПЛ/1, ФОРТРАН и ассемблера имеют специальные операторы управления кареткой,, фиксирующие интервалы между операторами программы. Поль- зователи КОБОЛа могут применять операторы EJECT, SKIP1, SKIP2 и SKIP3. Пользователи языка ПЛ/1 с помощью языка управления зада- ниями могут определить контрольную позицию перфокарты, кото- рая будет управлять интервалами в листинге .исходной программы. В версии ФОРТРАНа WATFIV имеются режимы $EJECT в $SPACE, обеспечивающие возможность вертикальной разрядки. Пропуск страницы указывает на начало новой главы в книге» т. е. на начало более важного раздела, чем параграф (что отме- чается пропуском строки). Таким образом, пустые области делят программу на логически самостоятельные части. 1.4. ПРОБЕЛЫ В языках программирования пробелы довольно часто ставятся произвольно. Действительно, в изъятии пробелов из программы не больше смысла, чем в том, чтобы их убрать из текста. Что вы ска- жете, например, о такой фразе: Явсегдамогунаписатьнечтоподобное ивысможетепрочестьноэтопотребуетотвасслишкоммногоусилий. Пробелы следует ставить везде, где это приводит к улучшению читаемости программы. Можно написать такой оператор: DO101= 1,23,2 Но написанный ниже оператор читать значительно легче: DO 101 = 1,23,2 Широкое использование пробелов существенно облегчает чте- ние программы. Ставьте пробелы между элементами списка дан- ных, а также до и после операций +, —, =. Иногда желательна отделять пробелами операции (*, /). Пробелы можно также ис- пользовать для указания приоритета операций. Например, запись- вида 1+А*В предпочтительнее, чем вводящее в заблуждение выражение 1+А * В Делайте пробелы для улучшения читаемости программы. 2—899
18 ГЛАВА 1 1.5. ИДЕНТИФИКАЦИЯ И ПОСЛЕДОВАТЕЛЬНАЯ НУМЕРАЦИЯ В большинстве языков программирования под идентификацию я нумерацию строк исходной программы отводятся 73—80 позиции перфокарты. Идентификатор обычно пробивается в позициях 73— 76, а позиции 77—80 могут использоваться для последовательной нумерации. Нумерацию следует давать с приращением 10, чтобы можно было вставлять новые строки. В КОБОЛе для нумерации отводятся позиции 1—6, а для идентификации — позиции 73—80. Можно также указывать изменения или добавления в програм- му, пробивая в позициях 73—80 символы NEW или FIX. Зачастую бывает полезно знать, что было недавно изменено. Последовательная нумерация помогает предотвратить ошибки, связанные с нарушением порядка следования операторов в про- грамме. Оператор может рассыпать колоду перфокарт и пытаться восстановить первоначальный порядок их следования. Если не ну- меровать карты, трудно определить, перемешана колода или нет. Конечно, если программа находится не на перфокартах, то после- довательная нумерация не так существенна. Нумерация операторов исходной программы помогает при от- ладке, так как номер используется для быстрого обнаружения карты или строки в большой программе. Следовательно, програм- мист должен проставлять номера с самого начала.. При наличии последовательной нумерации и идентификации программа выгля- дит более четкой и легко читаемой. Чтобы поддерживать нумерацию исходных программ, каждая вычислительная система должна обеспечить нумерацию и репроду- цирование исходных колод. В таком случае для получения новой исходной колоды можно использовать интерпретирующий перфо- ратор. На рис. 1.1 показана программа с идентификацией и после- довательной нумерацией. 1.6. ВЫБОР ИМЕН ПЕРЕМЕННЫХ Имена переменных должны быть выбраны так, чтобы наилуч- шим образом определять те величины, которые они представляют. Если ограничения на размер имени отсутствуют, используйте име- на настолько длинные, насколько это нужно, но не длиннее, чем „необходимо. Например, в операторе X = Y + Z -имена переменных выбраны неудачно, поскольку совсем не ис- пользована мнемоника. Такая запись оператора PRICE = COST + PROFIT намного лучше. Правильный выбор имен переменных — это залог удобочитае- мости программ. Кроме того, это самый легкий и дешевый метод,
СТИЛЬ ПРОГРАММИРОВАНИЯ 1» так как он требует незначительных умственных усилий от прог- раммиста и столь же небольшого расхода машинного времени. Ти- пы переменных, для которых надлежащим образом выбраны име- на, могут не соответствовать типам, присваиваемым по первой бук- ве имени в соответствии с принципом умолчания. В связи с этим следует давать соответствующее описание переменным, не пользу- ясь принципом умолчания. Действительно, совсем неплохо описы- вать все переменные, чтобы быть уверенным, что тип переменных выбран правильно/ Существуют некоторые запреты, которые необходимо помнить при выборе имен переменных и меток. Избегайте схожих по виду имен, их неестественных написаний (как phone и fone) и подобных по написанию символов '(АХ 10 и АХЮ). Если нужно использовать числа в именах переменных, лучше писать их в конце имени. Име- на должны отличаться чисто психологически. В этом смысле име- на, подобные по звучанию, виду или составу букв, отличаются ма- ло. Когда имя содержит избыточную информацию, это тоже пло- хо. Например, FOUR — 12/5 Здесь переменная FOUR имеет два различных значения: вели- чины 4 и величины, помещенной в ячейку FOUR. Соответствующая мнемоника должна быть использована при выборе имен для программ, параграфов, процедур, функций и под- программ. Программные метки должны соответствовать меткам,, которые использовались в блок-схемах или при анализе задачи, чтобы можно было их связать с более ранними формами алгорит- ма. Все это кажется очевидным, но часто не выполняется. В каче- стве имен переменных должны употребляться термины, используе- мые в данной области. Используйте имена с подходящей мнемоникой. В языках КОБОЛ и ПЛ/1 легко выбрать соответствующие име- на переменных, так как в этих языках допускаются длинные име- на и разделитель в именах. В ФОРТРАНе длина имен ограничена (от 5 до 8 символов в различных версиях языка) и разделитель в именах отсутствует. АЛГОЛ допускает длинные имена перемен- ных. Изощренные имена, которые имеют отдаленное отношение к рассматриваемой задаче, становятся совершенно непонятными, ког- да программа расширяется или когда другой программист должен ее модифицировать. Необоснованный выбор имен переменных и меток может существенно испортить хорошую в остальном про- грамму. При выборе имен переменных старайтесь установить, что обозначает эта переменная на естественном языке, и выбирайте наиболее подходящее слово. 2*
20 ГЛАВА I Некоторые языки программирования не имеют резервирован- ных слов. Следовательно, программист может использовать любое слово как имя переменной, даже те, которые обычно употребляют- ся как служебные. Например, ФОРТРАН: DO 5 16 = 1 . 34 14 FORMAT(16) = I 5 END = K*I ПЛЦ: IF IF = THEN THEN THFN = ELSE; ELSE ELSE = IF; Какие действия выполняют указанные операторы? Здесь нет -ошибки в расположении букв. Все операторы правильные и одно- значно воспринимаемые компилятором, но заметьте, как трудно они интерпретируются из-за того, что имена переменных совпа- дают со служебными словами. Избегайте употребления слов, кото- рые могут ввести в заблуждение читателя. При использовании различных типов переменных: целых,, дей- ствительных, комплексных, символьных — программист часто попа- дает в затруднительное положение. Однако трудности легко прео- долеть, если следовать определенным соглашениям при наимено- вании переменных различных типов. Например, в программе с не- сколькими комплексными переменными все имена этих перемен- ных могут начинаться с буквы С или аббревиатуры СМР. Этот префикс будет напоминать вам, что переменная комплексная. По- добный метод может быть применен для облегчения идентифика- ции файлов. Начинайте имена целых переменных с одной из следующих букв: I, I, К, L, М, N. Использование этих букв для представления целых переменных настолько обычно и общепринято, что полезно следовать этому на практике. Если язык программирования допускает разделитель в именах переменных (например, дефис — в КОБОЛе и черту под строкой— в ПЛ/1), то его следует использовать. Например, имя COSTPLUS RECOIL CRAFTER IDENTRY следует представлять как COST-PLUS следует представлять как 7 REC-OIL следует представлять как CR-AFTER следует представлять как ID-ENTRY Разделитель облегчает чтение имен переменных и уменьшает вероятность их неправильной интерпретации.
СТИЛЬ ПРОГРАММИРОВАНИЯ 21 Некоторые программисты создают целые программы, чтобы можно было вставить так называемые остроумные фразы ADD GIN ТО VERMOUTH GIVING MARTINI. (Добавьте джина к вермуту, чтобы получить мартини) ADD HOT-PEPPER ТО CHILI GIVING HEART-BURN. (Добавьте острого перца в блюдо из перца, чтобы получить изжогу) Пошутить, крнечно, можно, но будет не до шуток при попытке модифицировать программу, если используемые имена не отобра- жают специфики программы. Правильно выбранные имена пере- менных уменьшают необходимость комментариев. Программы могут быть хорошо написаны: IF CONTENTS(PITCHER) < QUART THEN FILL(PITCHER) ELSE POUR(PITCHER) и могут быть написаны плохо: IF XCONT (PTCH ) < QT THEN XFILL ( PTCH ) ELSE XPOUR ( PTCH) 1.7. ИМЕНА ФАЙЛОВ При работе с файлами в таких языках, как КОБОЛ и ПЛ/1, для идентификации каждого файла разумно выбрать какой-либо префикс или суффикс. Этот префикс или суффикс вы сможете ис- пользовать в названии младших единиц этого файла. В рассматри- ваемом^примере MASTER используется как префикс. FILE SECTION. FD MASTER-FILE, 01 MASTER-RECORD. 03 MASTER-NAME PICTURE X(20). 03 MASTER-ADDRESS PICTURE X(40). 03 MASTER-NUMBER PICTURE 9(08). WORKING-STORAGE SECTION. 01 MASTER-WORK-AREA. 05 MASTER-COUNT PICTURE 9(04).
22 ГЛАВА 1 Если каждый файл имеет свой префикс, то намного легче чи- тать программу. Этот префикс помогает определить соответствую- щее поле при распечатке программы и указывает, какие поля и рабочие области связаны логически. Еще одно соглашение' состоит в том, что имена файлов должны содержать слово FILE, а имена записей — слово RECORD. При наименовании файлов используйте определенный префикс или суффикс. Указанный прием позволяет различать по префиксам идентич- ные имена, такие, как поле дат. Например, MASTER-DATE TRANSACTION-DATE REPORT-DATE Здесь каждое из трех различных полей дат идентифицируется по своему префиксу. Если вы не воспользуетесь приведенными здесь рекомендациями, то будете вынуждены применять различ- ные сокращения, например DATE, DTE, DAT, которые позволят различать поля. При выборе имен записей используйте имена, ориентированные на запись, а не на задание. Так, следующие имена указывают на определенное задание: 01 OUTPUT-FILE 05 OUTPUT-NAME 05 OUTPUT-ADDRESS Такие имена были бы полезны только в одном случае — если данный файл выходной. При следующем задании эта запись мо- жет быть входным файлом и предшествующие имена не будут иметь смысла. Более тщательный выбор имен позволит использовать одно и то же имя файла в нескольких связанных программах. Например, 01 MASTER-FILE 05 MASTER-NAME 05 MASTER-ADDRESS Вышеприведенное имя записи можно ввести в любую програм- му, где требуется эта запись. Использование одних и тех же имен для одинаковых файлов в различных программах ведет к быстрой идентификации файла. В тех случаях, когда один файл используется во многих програм- мах, руководители проектов программ, возможно, сочтут целесооб- разным установку стандартных имен файлбв.
СТИЛЬ ПРОГРАММИРОВАНИЯ 23 1.8. СТАНДАРТНЫЕ СОКРАЩЕНИЯ Лица, ответственные за разработку программы, возможно, соч- тут необходимым введение стандартных сокращений — аббревиа- тур. Это существенно облегчит чтение данной программы посто- ронним лицам. Если в данном вопросе не придерживаться принци- па стандартизации, то, например, следующие сокращения: MSTR MAST MST будут применяться для записи имени MASTER. Стандартные со- кращения помогают программистам понимать свои старые про- граммы, в модификации которых возникла необходимость. Ис- пользование стандартных сокращений и стандартных имен пере- менных особенно предпочтительно, когда коллектив программи- стов создает одну большую систему. Для тех, кто хочет сократить длину идентификатора с целью экономии объема перфорирования или вынужден делать это из-за ограничений, накладываемых на длину имени, предлагается набор правил сокращения, который поможет обеспечить удобочитаемость программ. Эти правила, разработанные Микаэлом Джексоном (Datamation, апрель 1967), заключаются в следующем: 1. Каждое значащее слово в имени подлежит сокращению, но не более трех. 2. В аббревиатуру всегда должны включаться начальные бук- вы слов. 3. Согласные важнее гласных. 4. Начало слова важнее его конца. 5. Аббревиатура должна включать в себя от 6 до 15 букв. Эти правила особенно полезны программистам, использующим ФОРТРАН, так как в этом случае особенно часто возникает необ- ходимость в сокращении длины идентификатора. Ниже приведен алгоритм сокращения, разработанный на основе этих правил. Сокращение слова образуется последовательным удалением гласных, начиная с правого конца слова, до тех пор, пока либо все гласные не будут изъяты (кроме первой буквы слова, если она гласная), либо слово не уменьшится до требуемого размера. Если все гласные выброшены, а длина слова все еще превышает заданную величину, то удаляются согласные. Имена Сокращения COST PLUS CST PLS ACCOUNTS RECEIVABLE ACCNTS RECVBL RECORD RCRD TRANSACTION TRNSCTN
24 ГЛАВА 1 1.9. ПЕРЕНОС В некоторых языках программирования допускается перенос имен или литералов на другую строку. Но обычно не возникает необходимости в разбиении слова и редко бывает нужен перенос литерала. Если слово не помещается на текущей строке, начинай- те его со следующей строки. И хотя перенос допускается, это ус- ложняет чтение и использование программы. Если вы записываете оператор на двух строках, делайте пере- нос после знака операции. Вот случай небрежного переноса: А = В — С - (D + 2) Гораздо лучше перенести так: А = В — С — (D + 2) Во втором примере знак «минус», записанный на первой строке, сообщит читателю о том, что оператор продолжается на следую- щей строке. Кроме того, если вторая строка оператора будет слу- чайно выброшена, то в первом примере это пройдет бесследно, тогда как во втором будет обнаружена синтаксическая ошибка. 1.10. РАЗМЕЩЕНИЕ ОПЕРАТОРОВ В некоторых языках программирования допускается размеще- ние нескольких операторов на одной строке. Например, пл//: X = А**3; IF (А < В) THEN CALL FINISH; В = COS (С);, . КОБОЛ: FD CARD-IN, RECORDING MODE IS F, LABEL RECORDS ARE OMITTED, RECORD CONTAINS 80 CHARACTERS, DATA RECORD IS CARD-SALES. Однако запись нескольких операторов на одной строке неудоб- на по двум причинам. Во-первых, затрудняется чтение программы; во-вторых, это мешает использованию таких средств, как деление на параграфы. Предпочтительнее размещать каждый оператор на отдельной строке. ПЛ/1: X = А**3; IF (А < В) THEN CALL" FINISH; В = COS(C); КОБОЛ: FD CARD-IN RECORDING MODE IS F, LABEL RECORDS ARE OMITTED, RECORD CONTAINS 80 CHARACTERS, DATA RECORD IS CARD-SALES.
СТИЛЬ ПРОГРАММИРОВАНИЯ 25 Это не только улучшает удобочитаемость программы, но об- легчает удаление или исправление одного оператора, так как этот процесс не затрагивает другие операторы. > Например, АЛГОЛ W: А = 14.2; FOR I: = 1 UNTIL 10 DO BEGINX(I): =0; К: = I*K; Y(I): = K; END Ниже написан тот же пример, но каждому оператору отведена отдельная строка. АЛГОЛ W; А: « 14.2; FOR I := 1 UNTIL 10 DO BEGIN X(I) := 0; К := I*K; Y(D := K; END; Возможно, вы заметили, что два предшествующих фрагмента программы отличаются. Можете ли вы найти ошибку? Если вы ее обнаружите, обратите внимание на то, как много перебивок потре- буется сделать, чтобы ее исправить. (Ошибка — в первом опера- торе.) Другая причина построчного размещения операторов состоит в том, что в сообщении о синтаксической ошибке всегда указыва- ется номер строки1). Таким образом, если придерживаться этого правила, легче обнаружить синтаксическую ошибку. Одного оператора в строке достаточно.' Это относится и к заголовкам, таким, как название парагра- фа. Если размещать название параграфа на отдельной строке, то не надо будет его переписывать при изменения содержания этого параграфа. 1.11. УПОРЯДОЧЕНИЕ СПИСКОВ ПО АЛФАВИТУ Языки программирования включают много списков имен пе- ременных, причем порядок расположения имен в списке опреде- ляется самим программистом. Рассмотрим два типа списков: спис- ки имен переменных при объявлении типа переменных и списки параметров процедуры. ® ^екоторых трансляторах ошибка указывается стрелкой под строкой.—
26 ГЛАВА I Упорядочивание имен в списке облегчает поиск имени. Приве- дем в качестве примера два списка: ФОРТРАН: 1) INTEGER BETA, Z, КЕР, COST, PRICE, DOBT 2) REAL I, AMOUNT, SIZE, K, BETS Предположим, вы хотите выяснить тип переменной BETS (це- лая или действительная). Тогда вам придется просмотреть оба списка полностью. Если списки очень длинные, как это обычно бывает в больших программах, то процесс поиска может оказать- ся затруднительным и отнимет много времени. Если же имена в списках упорядочены по алфавиту, то задача становится относи- тельно простой. ' INTEGER BETA, COST, DOBT, КЕР, PRICE, Z REAL AMOUNT, BETS, I, K, SIZE Необязательно упорядочивать имена всех переменных одного типа в одном операторе объявления типа. Часто бывает удобнее объединять логически связанные переменные. INTEGER BUFFI, BUFF2, CHAR, TERMTR INTEGER COST, PRICE, TAX Нижеследующая таблица показывает, что можно упорядочить по алфавиту: , /Н ФОРТРАН Списки имен в операторах описания Списки параметров ПЛ/1 Списки имен в операторах описания Списки параметров КОБОЛ Имена файлов Переменные рабочей области Списки параметров АЛГОЛ W Списки имен в операторах описания Списки параметров Упорядочение по -алфавиту можно провести и в списках аргу- ментов операторов вызова процедур. Например, CALL SUB1 (А, В, СТАХ, X, Z) Но так как аргументы оператора вызова должны соответство- вать списку параметров процедуры или подпрограммы, то не всег- да возможно полное упорядочение имен в списке. Кроме того, не- которые программисты предпочитают функциональное уйорядоче-
СТИЛЬ ПРОГРАММИРОВАНИЯ 27 ние, например располагать сначала выходные аргументы, потом входные. Упорядочением по алфавиту можно воспользоваться и при пе- речислении подпрограмм или процедур. Упорядочив подпрограм- мы или процедуры, можно быстрее найти нужное имя. Под- программы на ФОРТРАНе— это отдельные программы, и упо- рядочить их довольно просто. Нетрудно расположить в алфавит- ном порядке и процедуры на языке ПЛ/1. ' Упорядочивайте списки по алфавиту. ФОРТРАН Когда в большой программе, написанной на ФОРТРАНе, опе- раторы не пронумерованы последовательно, трудно найти нужный номер. Так как нумерация операторов произвольна, то после от- ладки программы можно перенумеровать все операторы, чтобы упорядочить их номера. Это может сделать программа. Програм- ма перенумерации должна уметь нумеровать с любого желаемого номера и обеспечивать любое приращение. (См. разд. «Проекты» в конце главы.) В языках ФОРТРАН й БЕЙСИК для меток используются чис- ла. Если программа или подпрограмма, написанная на этих язы- ках, состоит из двух отдельных логических блоков, тО желательно зарезервировать первые одну или две цифры метки для идентифи- кации блока, а остальные — для нумерации операторов внутри бло- ка. Например, пусть имеются три различных блока, тогда для первого блока можно использовать метки, начиная с 1000-й, для второго — начиная с 2000-й, для третьего — с 3000-й. КОБОЛ В КОБОЛе упорядочить по алфавиту имена параграфов нелег- ко, так как порядок следования параграфов влияет на логику про- граммы. Чтобы иметь возможность независимого выбора имени, добавляйте к мнемоническим именам параграфов номера их по- рядка следования. Например, если параграфам присвоены такие имена TEST-LOOP RUN-ERROR REPORT-OUT то для указания относительного расположения параграфа в про- грамме замените эти имена на следующие: TEST-LOOP-600 RUN-ERROR-610 REPORT-OUT-620
28 ГЛАВА I Используемые номера должны предусматривать включение но- вых параграфов. ПЛ/1 В процедурно-ориентированных языках, подобных ПЛ/1, могут быть вложенные процедуры. В таком случае упорядочение по ал-^< фавиту ограничивается порядком вложения. Одно из предложений, облегчающих нахождение вложенных процедур, — это использо- вание имен внешних процедур как префикса или.суффикса имен вложенных процедур. Например, ПЛ/1: SCAN: PROCEDURE SCAN-CHECK: PROCEDURE SCAN.CHECK.TRACE: PROCEDURE END SCAN_CHECK TRACE; SCAN_CHECK-TRAP: PROCEDURE END SCAN_CHECK-TRAP; END SCAN_CHECK; SCAN_DEBIT: PROCEDURE END SCAN; ’ Здесь SCAN — имя внешней процедуры — используется как пре- фикс для имен внутренних процедур. Другой способ упорядочивания, обеспечивающий, однако, мень- ше информации, — нумерация меток. Например, если имя проце- дуры— SCAN, то меткам в этой же процедуре присвоены значе- ния SCAN010, SCAN020 и SCAN030. При использовании для ну- мерации лексикографических меток всегда оставляйте интервалы для внесения изменений, чтобы это не нарушило всей нумерации. Упорядочение по алфавиту — одна из предпосылок- легкой чи- таемости программы, второй является аккуратность. Так, списки должны быть организованы в столбцы. Обратите внимание, на- сколько труднее читать список OCOMMON ALPHA,BETA,CHI,DELTA,EPSIL.ETA,GAMMA, ПОТА,КАРРА,LAMBDA,MU,NU,OMEGA,OMICR,PHI,PI, 2PSI,RHO,SIGMA,TAU
СТИЛЬ ПРОГРАММИРОВАНИЯ 29* по сравнению со следующим: OCOMMON ALPHA, BETA, CHI, DELTA, EPSIL, ETA, 1 GAMMA, IOTA, KAPPA, LAMBDA, MU, NU, 2 OMEGA, OMICR, PHI, PI, PSI, RHO, 3 SIGMA, TAU Чтобы расположить информацию в виде столбцов, программист должен отвести под каждое имя место, соответствующее макси- мальному числу символов в именах переменных списка. Подобное расположение должно использоваться во всех списках данных, включая списки ввода-вывода. 1.12. СКОБКИ Правильное использование скобок существенно улучшает удо- бочитаемость программы. Так как последовательность выполнения операций определяется их приоритетами, то программист зачастую может ограничиться небольшим количеством скобок, но при этом затрудняется чтение и корректировка программы. Следующий при- мер иллюстрирует это: Использование малого Использование дополни- количества скобок тельных скобок A*B*C/(D*E*F) (A*B*C)/(D*E*F) A*B/C*D/E*F (A*B*D*F)/(C*E) А* *В* *С А* * (В* *С) A/B/C/D ((A/B)/C)/D А* *В*С (А* *В)*С X.GT.Y.OR.Q (X.GT.Y) .OR. Q A + B.LT.C (А+В) .LT. С Основное правило таково: в сомнительных случаях ставьте скобки, — это не только сделает программу более понятной, но и предотвратит ошибки. Часто в логических выражениях скобки используют недостаточно, что приводит к ошибкам. Скобки обходятся дешевле, чем ошибки. 1.13. ОТСТУПЫ ОТ НАЧАЛА СТРОКИ При записи операторов для указания связи между ними дела- ют одинаковый отступ от начала строки (это действие также на- зывают введением параграфов). Отступы, не оказывая влияния на логику программы, существенно улучшают ее читаемость. Это аналогично разделению текста на естественном языке на парагра- фы логически связанные группы предложений. Правильно еде-
30 ГЛАВА 1 данные отступы выявляют структуру программы. Такое простое действие, как введение абзаца в текст программы, может суще- ственно повысить способность читателя к ее восприятию. Програм- ма должна быть приятна для глаза. Примеры Текст без отступов: IF (I<=1) THEN FACTORIAL := 1 ELSE BEGIN FACTORIAL : = 1; FOR J := 2 UNTIL I DO FACTORIAL := FACTORIAL * J END; Текст с отступами: IF (I< = 1) THEN FACTORIAL := 1 ELSE BEGIN FACTORIAL := 1; FOR J’:= 2 UNTIL I DO FACTORIAL := FACTORIAL * J END; Прежде всего отступы следует делать тогда, когда группа опе- раторов заключена в одну из следующих пар: ФОРТРАН DO ... CONTINUE АЛГОЛ BEGIN ... END; • ПЛ/1 DO; ... END; BEGIN; ... END; PROCEDURE; ... END; Циклы — типичный случай использования отступов. Примеры таких циклов показаны ниже. ФОРТРАН-. DO 10 I = 1, 16 С = 0.0 DO 8 К = 1, 12 С = С + В(К) D(K) = SQRT(K*1.0) 8 CONTINUE A(I) =.C 10 CONTINUE
СТИЛЬ ПРОГРАММИРОВАНИЯ 31 ПЛ/1: DO I = 1 ТО 16; С = 0.0; DO К = 1 ТО 12; С = С4-В(К); D(K) = SQRT(K); END; A(D = С; END; АЛГОЛ W: FOR I : = 0 UNTIL 4 DO BEGIN X(I) : = 0.0; B(I) := C(I); END; Чтобы выделить символы, формирующие пару, пишите их, на- чиная с одной позиции. Операторы, заключенные между этими символами, обычно записываются со сдвигом на три позиции. При- веденные примеры вложенных циклов показывают, как отступы по- могают определить начало и конец цикла. Отступы делают не только при использовании циклов, их вво- дят и для лучшей группировки команд. ПЛ/1: IF (А<В) THEN DO; С = А; А = В; В = С; END; ФОРТРАН: IF (A .LT. В) GO ТО 16 С = А А =В В =С 16 VAL = TAN(X) Операторы, переход на которые определяется оператором IF, записываются с одинаковым отступом от начала строки. Примеры, приведенные выше, показывают, какие операторы группируются оператором IF. Аналогичная группировка возможна при операторах IF„..THEN...ELSE.
32 ГЛАВА 1 ПЛ[1: — IF (А < В) THEN DO; А = —А; В = А*В; END; ELSE ' DO; А = А*В; В = —В; END; Обычно используются последовательные отступы на три пози- ции. Этого расстояния вполне достаточно, чтобы указать абзац и сделать несколько уровней отступа. Для выявления структуры программы используйте отступы. При написании операторов ввода-вывода также можно делать отступы. КОБОЛ'. READ имя файла AT END оператор. 4 WRITE имя записи BEFORE ADVANCING идентификатор LINES AT END-OF-PAGE оператор ШЦ1'. GET FILE (SYSIN) (PRICE, SALES.NUMBER); PUT FILE (SYSPRINT) EDIT (TASK, NEW.RATE) (SKIP(2), COL(4), A, F(7) ); Сложные операторы IF, которые содержат составные условия, легко читаются, если их надлежащим образом расположить. Один из вариантов — размещение в одной позиции слов IF, AND, OR, THEN и ELSE.
СТИЛЬ ПРОГРАММИРОВАНИЯ 33 КОБОЛ: IF (PARTS-NUMBER-PREFIX IN MASTER-HISTORY-FILE IS LESS THAN CURRENT-NUMBER-PREFIX, OR CURRENT-DATE IS EQUAL TO ZERO), AND CURRENT-COST IN DETAIL-INVOICE-FILE IS GREATER THAN 10.00 THEN (группа операторов, выполняемая, если условие истинно) ELSE (группа операторов, выполняемая, если условие ложно) В языках, где описываются переменные, атрибуты следует рас* полагать точно по позициям. КОБОЛ: 05 TOTAL-RECORD-COUNT PICTURE 9(03) VALUE ZERO. 05 FILLFR PICTURE X(04) VALUE SPACES. 05 TOTAL-PAGE-COUNT PICTURE 9(04). 05 FILLER PICTURE X(23) VALUE 'TOTAL COUNT OF j RECORDS'. r ПЛЦ DECLARE COST TAXES HEADINGS FIXED(5) FIXED(5,2), CHARACTERS) INITIAL (O), INITIAL('TOTAL COUNT OF INSERTS'); При описании файлов для указания соподчинения используйте отступы. КОБОЛ: 01 NEW-PARTS-AREA. 05 NEW-SEQUENCE-NUMBER PICTURE 9(03). 05 NEW-PAY-NUMBER. 10 NEW-DEPT-CODE PICTURE 9(02). 10 NEW-EMPLOYEE-CODE PICTURE 9(03). 05 NEW-CHARGE-DEPT PICTURE X(05). Для выявления структуры данных используйте отступы. Аналогичным приемом можно воспользоваться при написании арифметических операторов: располагать в одной позиции все зна* ки равенства: А =В-|-С SIGN — BASE*COUNT COST = PAY-{-BONUS 3-899
34 ГЛАВА 1 Это также улучшает читаемость программы. Если операторы занимают несколько строк, то строки, начиная со второй, должны иметь такой отступ, чтобы находиться справа> от знака равенства. Например, SIG = В*В 4- C/DES — COS(A) + PLAT/TEST Такая запись оператора лучше, чем следующая: SIG = В*В + C/DES — COS(A) + PLAT/TEST A: PROCEDURE в: PROCEDURE С: BEGIN; END С; е END В; О: PROCEDURE END D; END A; E: PROCEDURE F: BEGIN; G: BEGIN; ENDG; END F; END E; PL/1 Indentation Рис. 1.2. Использование отступов в ПЛ/1.
СТИЛЬ ПРОГРАММИРОВАНИЯ 3S Языки АЛГОЛ W и ПЛ/1 допускают вложенные процедуры. Для понимания структуры программы нужно, чтобы уровни вло- жения процедур были аккуратно указаны. Один из способов сде- лать это — использование отступов. На рис. 1.2—1.4 показаны со- ответствующие примеры. ВВС IN BEAL X,Y; BEAD (X) ; 1 :=X; IT X=0 THEN WRITE (0) /else IF X<0 THEN WRITE ("ОШИБКА, INPOT ВНЕ 06ЛАСТИ ОПРЕДЕЛЕНИЯ") ELSE BEGIN ' WHILE ABS ( (X-I»T)/X) >.000001 DO Y:=.5* (I+X/Y) ; WRITE (I) END END. Рис. 1.3. Пример программы без комментариев и отступов. 1.14. ВЫБОР ИМЕН РАЗДЕЛОВ Так же как разделение большого произведения на главы и па- раграфы облегчает чтение, так и разбиение большой программы на параграфы, разделы или подпрограммы путем выделения логи- ческих единиц улучшает восприятие программы, помогает избе- жать однообразия и хорошо организовать материал. Название раздела должно отражать цель данного раздела, т. е. действия, которые в нем производятся. Кроме того, использование разделов обеспечивает удобное место для комментариев. В начале каждого раздела следует помещать комментарии с объяснением назначения этого раздела. 1.15. НЕЧИТАЕМЫЕ ПРОГРАММЫ В инженерном конструировании чертежи обычно подписывают- ся ответственным лицом, если последний считает, что они хорошо сделаны и соответствуют принятым стандартам. По окончании раз- работки программ их не визирует ни ответственное лицо, ни непо- средственный разработчик. Игнорируются требования к ясности и стандартам стиля; интересуются лишь тем, работает ли програм- ма, а если нет, то когда заработает. По-видимому, целесообразно, чтобы достаточно сложные про- граммы подписывались двумя лицами, которые бы отвечали, та- ким образом, за приемлемость этой программы по стилю и раз- меру. Это несложное требование со стороны руководителя проекта привело бы к повышению качества программ без дополнительных затрат. Если бы программисты знали, что программы будут читать и проверять, они более четко писали бы свои программы. Если второе лицо, которое должно ставить свою подпись, не может про- 3*
36 ГЛАВА I BEGIN COMMENT ЭТА ПРОГРАММА СЧИТЫВАЕТ ОДНО ДЕЙСТВИТЕЛЬНОЕ ЧИСЛО С ВХОДНОЙ КАРТЫ И ПЕЧАТАЕТ ЗНАЧЕНИЕ КВАДРАТ- НОГО КОРНЯ ИЗ ЭТОГО ЧИСЛА. ДЛЯ ВЫЧИСЛЕНИЯ КВАДРАТ- НОГО КОРНЯ ПРИМЕНЯЕТСЯ МЕТОД ИТЕРАЦИЙ НЬЮТОНА ДО ПОЛУЧЕНИЯ ТОЧНОСТИ 10** (—6). ОСНОВНАЯ ИДЕЯ ЭТОГО МЕТОДА ЗАКЛЮЧАЕТСЯ В СЛЕДУЮЩЕМ: ЕСЛИ X—НЕКОЕ ПРИБЛИЖЕНИЕ КВАДРАТНОГО КОРНЯ ИЗ Y, ТО (X+Y/X)/2 — ЛУЧШЕЕ ПРИБЛИЖЕНИЕ. МЫ ПОВТОРЯЕМ ВЫЧИСЛЕНИЕ ПРИ- БЛИЖЕНИЯ ДО ТЕХ ПОР, ПОКА НЕ ПОЛУЧИМ ЖЕЛАЕМОЙ ТОЧНОСТИ, ТО ЕСТЬ ПОКА (Х**2—Y)/Y НЕ СТАНЕТ МЕНЬШЕ 10’** (—6) ОПИСАНИЕ ПЕРЕМЕННЫХ INPUT —ЧИСЛО, СЧИТАННОЕ С ВХОДНОЙ КАРТЫ, ТО ЕСТЬ ЧИСЛО, КВАДРАТНЫЙ КОРЕНЬ ИЗ КОТОРОГО МЫ ДОЛЖНЫ НАЙТИ APPROXIMATION —ТЕКУЩЕЕ ПРИБЛИЖЕНИЕ КВАДРАТНОГО КОРНЯ ИЗ ВХОДНОГО ЧИСЛА; REAL INPUT, APPROXIMATION; COMMENT УСТАНОВКА НАЧАЛЬНОГО ЗНАЧЕНИЯ — ПУСТЬ INPUT ЯВЛЯЕТСЯ НЕ ОЧЕНЬ ХОРОШИМ ПРИБЛИЖЕНИЕМ КВАДРАТ- НОГО КОРНЯ ИЗ INPUT, НО ДЛЯ НАШИХ ЦЕЛЕЙ ЭТОГО ПРИ- БЛИЖЕНИЯ ДОСТАТОЧНО; READ (INPUT); APPROXIMATION:=INPUT; COMMENT МЕТОД НЬЮТОНА ПРИМЕНИМ ТОЛЬКО К ПОЛОЖИТЕЛЬ- НЫМ ЧИСЛАМ, ПОЭТОМУ СНАЧАЛА МЫ ДОЛЖНЫ УБЕДИТЬСЯ В ТОМ, ЧТО ВХОДНОЕ ЧИСЛО ПОЛОЖИТЕЛЬНОЕ; IF INPUT=0 THEN WRITE (0) ELSE IF INPUTCO THEN WRITE («ОШИБКА, INPUT ВНЕ ОБЛАСТИ ОПРЕДЕЛЕНИЯ») ELSE BEGIN WHILE ABS ((INPUT —APPROXIMATION* APPROXIMATION)/INPUT) >.000001 DO APPROXIMATION : = . 5* APPROXIMATION+ INPUT/APPROXIMATION); WRITE (APPROXIMATION) END END. Рис. 1.4. Программа, приведенная на рис. 1.3, с комментариями, отступами и пра- вильно подобранными именами переменных,
СТИЛЬ ПРОГРАММИРОВАНИЯ 37 честь программу и понять, что она делает, то как быть людям из группы поддержки программного обеспечения, когда придет время неизбежных изменений и эксплуатации программы? Начинающие программисты думают, что они составляют про- граммы для машин. Опытные программисты знают, что програм- мы пишут для людей. Длинная цепочка людей будет читать боль- шую часть этих программ. Вначале сам автор будет много раз чи- тать и перечитывать программу, в то время пока пишет, тестирует и отлаживает ее. Затем все, кто должны использовать программу, будут перечитывать ее много раз. Мы учимся писать, читая. По-ви- димому, причина того, что мы таким же образом не учимся про- граммировать (т. е. читая чужие программы), — недоступность большинства программ. Существование таких никому непонятных программ объясня- ется тем, что программист, писавший ее, а также те, кто ее моди- фицировал, по-настоящему не разобрались в ней. Такие программы подобны «черному ящику», воспринимающе- му входную информацию и выдающему выходную, однако никто не знает, как он работает. Поэтому эти программы нужны, пока не наступает пора неизбежных изменений; тогда программа выбра- сывается и пишется новая, поскольку это легче, чем разобраться в старой. 1.16. ЗАКЛЮЧЕНИЕ Надеюсь, мне удалось показать, что небольшие усилия, кото- рые необходимо приложить для того, чтобы сделать программу удобочитаемой, обходятся дешевле, чем издержки по пересмотру, обнаружению ошибок или переделке плохо написанной програм- мы. Скорость разработки программного обеспечения — около де- сяти полностью отлаженных команд в день; небольшие затраты на улучшение читаемости программы были бы при этом очень по- лезны. Хорошего программиста отличает способность писать удобочи- таемые программы. В оправдание плохо составленных программ обычно приводят два аргумента: 1. Это должна быть сделанная на скорую руку и черновая про- грамма ограниченного использования. 2. Это провалившийся проект, который нет смысла дорабаты- вать. Но программы имеют тенденцию жить дольше и использовать- ся шире, чем запланировано, поэтому следует учиться сразу писать хорошую программу. Кроме того, это экономит время при тести- ровании и модификации программы. Много говорилось о необходимости полной документации. Если программы удобочитаемы, то они сами становятся существенной составляющей документации, Таким образом, имеется еще один
38 ГЛАВА I аргумент в пользу удобочитаемости программ: программа — это часть документации, точная и отражающая произведенные измене- ния. Таким образом, в случае легкочитаемого входного языка мож- но сказать, что документация программы — это есть сама про- грамма. 1.17. СОВЕТЫ ПРОГРАММИСТУ Помните, программы читаются людьми. Делайте комментариев больше, чем это кажется необходимым. Используйте вводные комментарии. Делайте оглавление в больших программах. Комментарии должны содержать дополнительную информацию, а не перефразировать программу. Располагайте комментарии таким образом, чтобы это не делало ее менее наглядной. Неправильные комментарии хуже, чем их отсутствие. Делайте пробелы для улучшения читаемости программы. Используйте имена с подходящей мнемоникой. При наименовании файлов используйте определенный префикс или суффикс. Одного оператора в строке достаточно. Упорядочивайте списки по алфавиту. Скобки обходятся дешевле, чем ошибки. Для выявления структуры программы используйте отступы. Для выявления структуры данных используйте отступы. 1.18. УПРАЖНЕНИЯ ПОВТОРЕНИЕ ПРОЙДЕННОГО 1. Дайте определения следующих терминов: а) вводные ком- ментарии, б) оглавление, в) пояснительные комментарии и г) от- ступы. , 2. Почему программы должны быть удобочитаемыми? 3. Как часто следует давать комментарии? 4. Назовите обязательные случаи употребления комментариев; 5. В каждой программе должны быть некоторые общие коммен- тарии. Какие элементы должны включать эти вводные коммента- рии? \ 6. Как следует выбирать имена переменных?
СТИЛЬ ПРОГРАММИРОВАНИЯ 39 7. Какими правилами нужно руководствоваться, чтобы облег- чить чтение аббревиатур? 8. Что следует учитывать при наименовании файлов? 9. Каковы преимущества использования стандартных сокраще- ний? 10. Когда целесообразно пропускать строки? 11. Почему не следует экономить место при размещении опера- торов? 12. Зачем надо пробивать на картах имя программы? 13. Почему нумерация операторов должна быть с некоторым приращением? 14. Почему не рекомендуется располагать несколько операто- ров на одной строке? 15. В каких случаях можно использовать упорядоченные по ал- фавиту списки в вашем языке программирование 16. Найдите несколько примеров в какой-либо программе, где скобки улучшают ее читаемость. 17. Введите отступы в одну из ваших программ. Улучшило ли это читаемость? 18. Можете ли вы предложить какие-либо другие приемы улуч- шения читаемости программ? 19. Имеется ли в вашем языке программирования оператор, который делает пропуск строк в исходном листинге? ЗАДАНИЯ 20. Как работает следующая программа? Проанализируйте ее с точки зрения стиля и затем перепишите. 45 READ(5,32)X, Y IF(X.GT.40)GO ТОЮ 32 FORMAT(2F8.2) GO ТО21 10 R=Y*40+Y*1.5*(X— 40) 1 WRITE(6,32)R GO TO45 21 R=Y*X GO TO1 END 21. Для чего служит следующая программа? ФОРТРАН-. DO 51= 1, N DO 5 J = 1, N 5 XMAT(I, J) = (I/J) * (J/I)
40 ГЛАВА ! Выяснив назначение этого фрагмента программы, предложите свой, более понятный вариант записи. Замечание: I и J — целые переменные, результат действий над которыми всегда приводится к целому значению путем отбрасывания дробной части. Предложите три примера кодирования, требующие сообрази- тельности. 22. Если в вашем языке программирования не резервируются слова (как, например, в ФОРТРАНе или ПЛ/1), напишите не- большую программу, в которой служебные слова используются как имена переменных. Выясните, может ли кто-нибудь еще разо- браться в вашей программе? 23. Попытайтесь прочесть несколько программ. Вы можете взять их из библиотеки программ, у друзей или из мусорной корзины. Классифицируйте их (от А до F) по стилю и удобочитаемости. Научились ли вы ^ему-нибудь, читая эти программы? 24. Является программирование для вычислительных машин ис- кусством или наукой? Одни считают^ что это всегда будет искус- ством; другие полагают, что это наука. Попробуйте написать ре- ферат с доводами всех «за» и «против». Вначале уясните, в чем состоит различие между искусством и наукой. При исследовании этого вопроса полезно обратиться к статье Кнута «Программиро- вание как искусство»1*. ПРОГРАММЫ2) 25. Выберите любое задание из гл. 6. а) Напишите программу для данного задания наихудшим образом в смысле стиля. б) Напишите аналогичную программу наилучшим образом, в) Попытайтесь избежать использования оператора GO ТО. 26. Составьте программу, которая читает число N, затем N чи- сел, расставляет числа в порядке возрастания и печатает их. 27. Какие положительные целые числа, меньшие 20, удовлетво- ряют следующему равенству: /3+Л + КЭ=ЛЭ. 28. Напишите программу, входными данными которой является возраст ста человек. Программа подсчитывает число людей, воз- раст которых находится в интервале 10 лет, а именно: 0—9 лет 10—19 лет 20—29 лет и т. д. *> Knuth D., Computer Programming as an Art, Communications of the ACM (December 1974). 2> Наилучший способ работы над программами состоит в том, чтобы несколь- ко человек писали программы для одной задачи. Затем результаты сравнивают и оценивают стиль написания программ.
СТИЛЬ ПРОГРАММИРОВАНИЯ 41 Напечатайте результаты ваших расчетов в удобочитаемой форме. 29. Напишите программу для следующих числовых задач: а) Используйте цифры от 1 до 9 в различных комбинациях и операции сложения и вычитания, чтобы получить в сумме 100. Найдите некоторые решения, не прибегая к помощи ЭВМ. б) Выполните п. а) при условии, что цифры появляются в возрастающем или убывающем порядке. Ниже даны два примера:. 123 + 4— 5 + 67— 89 = 100 9—8+76—5 + 4 + 3 + 21 = 100 в) Выполните п. б) с шестнадцатеричными цифрами (123... ...EF), получив при этом число 100 в шестнадцатеричном пред- ставлении. 30. Напишите программу, которая читает исходную программу и перфорирует ее с последовательной нумерацией в позициях 73— 80. Программа должна уметь начинать нумерацию с любого задан- ного числа и обеспечивать любое заданное приращение. 31. Напишите программу, которая читает целое положительное число в десятичном представлении, а на выходе выдает это же число в десятичном представлении и на естественном языке. На- пример, если на вход поступает число 7204 520, то на выходе по- явится сообщение: 7 семь 204 двести четыре 52 пятьдесят два Последнее число должно быть нулем, ни одно из чисел не дол- жно превышать миллиард. ПРОЕКТЫ 32. Разработайте набор директив для введения параграфов в текст, написанный на языке программирования, с которым вы ра- ботаете. Затем обсудите эти директивы и выясните, сколько чело- век за и сколько против. Модифицируйте директивы, пока не бу- дет достигнуто полного единодушия. 33. Напишите программу, которая, получая на входе исходную программу, не разделенную на параграфы, выдает текст с выде- ленными параграфами (см. упр. 32). Это сделать нелегко, но воз- можно. Начните с выделения циклов. 34. Напишите программу, которая перенумеровывает операторы исходной программы на ФОРТРАНе или БЕИСИКе. Програм- ма должна допускать любой начальный номер и любое прираще- ние. Хорошим дополнительным свойством была бы возможность указывать начальный номер оператора для нескольких точек ис- ходной программы. Таким образом, вы можете начать нумерацию
42 ГЛАВА 1 программы с 10 и указать область программы, для которой нуме- рацию следует начать с 1000. 35. Напишите программу, которая будет читать исходную про- грамму на КОБО Ле или ПЛ/1 и выдавать новую программу, где -все параграфы и процедуры имеют численный суффикс. 36. Более простой и остроумный способ выполнения предыду- щего упражнения — изменение порядка следования операторов. Затем проведите отладку программы, составьте документацию и сделайте программу общедоступной. 37. Найдите приятеля, который прочтет ваши программы, а вы в свою очередь прочтете его программы. Затем каждый из вас может сделать замечания и предложения, по программе другого. Разработайте набор директив по чужой 'программе, так чтобы сделанные замечания оказались полезными, но не обескураживаю- щими. Одно традиционное предложение: читатель всегда должен найти в программе другого что-то положительное. (Изучите ме- тод коллективного программирования по книге Вейнберга, которая упоминается в списке литературы к гл. 2.) 38. Для любого языка программирования можно разработать список рекомендаций, обеспечивающих хороший стиль програм- мирования. Разработайте такой список, используя материал дан- ной главы. Согласуйте этот список со своими сотрудниками. 39. Можно написать программу, заставляющую выполнять ре- комендации хорошего стиля (см. упр. 38). Эта программа читает исходный текст и сигнализирует о любом нарушении стиля. Раз- работайте такую программу. 40. Составьте программу, которая читает массивы X и Y, мас- штабирует и строит график по данным либо на построчно печата- ющем устройстве, либо на выходном терминале. Если в вашем распоряжении имеется графопостроитель, воспользуйтесь им. 41. Напишите программу, которая читает исходную программу и выдает некоторые или все из нижеперечисленных результатов: а) Сколько и какой процент операторов каждого типа: комментарии, GO ТО, IF, DO и т. д. б) Какова длина имен переменных? Сколько одно-, двух-, трехсимвольных имен и т. д. Можете ли вы применить эту информацию для выработки некото- рого критерия определения хороших программ? Воспользуйтесь работой Кнута, посвященной практике программи- рования на ФОРТРАНе15. 42. Напишите программу, генерирующую непустые последова- тельности нулей, единиц и двоек без непустых поэлементно совпа- дающих соседних подпоследовательностей и упорядочивающую эти последовательности в лексикографическом порядке, пока не будет сгенерирована последовательность длиной 100. Вот приме- *> Knuth D. Е., An Empirical Study of FORTRAN Programs, Software — Prac- tice and Experience (April — June 1971).
СТИЛЬ ПРОГРАММИРОВАНИЯ 43 ры некоторых последовательностей: О 00 * 01 010 0100 * 0101 * 0102 01020 010200 * 010201 Звездочками отмечены недопустимые последовательности. Если для вас это затруднительно, то изящное решение этой задачи вы можете найти в книге У. Дала, Э. Дейкстры и К. Хоора1). 43. Палиндромы, Палиндром — это сочетания символов, которые читаются одинаково туда и обратно. Элементом палиндрома мо- жет быть буква (например, W0W или МОМ), цифра (4884, 121) или слово (STRAP ON—NO PARTS). а) Найдите целые числа, которые при возведении в квадрат дают палиндромы, например 26=676. б) Найдите целые числа — палиндромы, которые при возведе- нии в квадрат также дают палиндромы (22=484). в) Обобщите п. а) и б) на степени, большие 2. г) Напишите программу-палиндром. д) Начните с любого положительного числа. Если это не палин- дром, реверсируйте его цифры и сложите исходное число с чис- лом, полученным в результате реверсирования. Если сумма не па- линдром, то повторите те же действия и выполняйте их до тех пор, пока не получите палиндром. Ниже приведен пример для исход- ного числа 78: 165 561 "726 627 1353 3531 ___________ 4884 1®7О) RF11’ D4kstra' Hoare, Structured Programming, Academic Press, New York, рование — ’ ^е®кст₽а X°°P K-, Структурное программа-
44 ГЛАВА 1 Проверьте этот алгоритм для первых 100 целых чисел. Замечание: 196 — первое число, для которого не известно, ра- ботает ли этот алгоритм. Для справок смотрите книги Г. Бергер- сона1) и М. Гарднера2). 44. Составьте программу, которая будет выполнять одно из сле- дующих преобразований: а) ФОРТРАН в БЕЙСИК, в) АЛГОЛ в ФОРТРАН, б) БЕЙСИК в ФОРТРАН, г) ФОРТРАН в АЛГОЛ. Можете по желанию реализовать только часть предложенного пре- образования. 45. Напишите программу-стихотворение. 46. Напишите программу пятистопным ямбом. ЛИТЕРАТУРА 1. Conrow К., NEATER: A PL/I Source Statement Formatter, Program 360D- 03.6.0.18. SHARE Program Library, Triangle Universities Computation Center, P. O. Box 12076, Research Triangle Park, N. C., 27709. 2. Conrow K, Smith R. G., NEATER2: A PL/I Source Statement Reformatter, Communications of the ACM (November 1970). 3r „Jackson M., Mnemonics, Datamation (April 1967). ^A Rernighan B. W., Plauger P. J., The Elements of Programming Style, New-York, McGraw-Hill, 1974. 5. Ledgard H.;F., Programming Proverbs, Rochelle Park, N. J.: Hayden Book Com- pany, 1975.’ 6. Scowens R. S., Wichmann B. A., The Definition of Comments in Programming Languages, Software — Practice and Experience (April-—June 1974). Bergerson H. W., Palindromes and Anagrams, Dover Publications, New York, 1973. 2> Gardner M., Mathematical Games, Scientific American (August 1970). Эта статья включена в сборник Гарднер М., Математические новеллы. — М.: Мир, 1974.
Большие программы подобны спагетти на тарелке: тянешь с одной стороны — что-то движется с другой. Хорошее правило — ожидать всегда наихудшего; это относится к програм- мам. Я не программирую и наполовину своих возможностей. Глава 2 ПРОЕКТИРОВАНИЕ ПРОГРАММ Этап проектирования программ оказывает влияние на стиль программирования, надежность, эффективность, отладку, тестиро- вание и эксплуатационные свойства программ. Таким образом, это важнейшая часть любой программной разработки. Однако здесь рекомендации для одного проекта или одного программиста не всегда подходят для другого проекта или программиста. Вот одно очевидное утверждение: проектируйте до этапа коди- рования. Желание сделать хоть что-нибудь приводит к тому, что кодирование начинают, не окончив стадию проектирования, а иног- да даже не начав ее. Небольшие программы, конечно, не вызывают таких трудно- стей, как большие, так как с их составлением легко справится один человек. Но как организовать проектирование программ, которые так значительны по объему, что при их разработке одним челове- ком не обойдешься? Над этой проблемой работают давно, но и сейчас мы действуем как любители, когда возникает вопрос о соз- дании больших программ. В данной главе приводятся некоторые соображения по поводу проектирования программ. 2.1. СТРЕМЛЕНИЕ К ПРОСТОТЕ Простота при проектировании программы — это первый шаг, ведущий к получению легко читаемой программы. Кодирование должно быть простым. Должен соблюдаться так называемый KISS-принцип: Keep It Simple, Stupid (делай проще, глупец!). Изощренное программирование может обойтись слишком дорого при отладке и модификации программы. Необычное кодирование (например, использование скрытых возможностей машины) часто препятствует отладке программы и, конечно, затрудняет ее исполь- зование другими программистами. Стремитесь к простоте.
46 ГЛАВА 2 Было даже предложено увольнять программиста, который слишком изощренно программирует. Действительно, для такого предположения есть основания,’ так как если написана программа, которая так сложна и непонятна, что только автор может в ней ра- зобраться, то она никому не нужна. Программа должна отлажи- ваться и использоваться как автором, так и другими лицами, а ее сложность существенно затрудняет эти процессы. Иногда пишут сложные программы с целью доказать, что без автора этих программ обойтись нельзя. Такая цель может быть оправдана с точки зрения программиста, но никак не руководите- ля проекта, который заинтересован в получении максимально про- стой программы. Сложные программы требуют больших усилий и являются показателем плохого качества программирования. Та- кие программы трудно отладить или модифицировать даже их ав- торам, и последние часто просто не в состоянии получить правиль- но работающие программы. Структура программы должна раскрывать ее логику. Напри- мер, если тестовая программа имеет несколько ветвей, было бы удобно расположить части программы, соответствующие каждой ветви, в определенном порядке, например по возрастанию значе- ния параметра теста. Этот метод наиболее целесообразен, по- скольку именно такого решения и ожидает тот, кто читает про- грамму. Существенна простота и при написании отдельных операторов. При синтаксическом контроле компилятор может выдать такое со- общение: СИНТАКСИЧЕСКАЯ ошибка НАХОДИТСЯ НА 37-и строке. Если 37-я строка выглядит следующим образом: А = В/*С то ошибку обнаружить легко. Но если оператор слишком длинный или в этой строке содержится несколько операторов, то нахожде- ние ошибки становится затруднительным. Простота операторов помогает обнаружить ошибку и в процес- се вычисления, если дан номер оператора. Например ПЕРЕПОЛНЕНИЕ С ФИКСИРОВАННОЙ ТОЧКОЙ В СТРОКЕ 56. Если оператор в строке 56 короткий, то ошибку обнаружить проще. Но не следует впадать в другую крайность. Так, например, этот оператор ROOT1 = (—В + SGRT(B*B — 4* А*С)) / (2* А)
ПРОЕКТИРОВАНИЕ ПРОГРАММ 47 легче для восприятия, чем группа операторов ВВ = В*В А1 = А*С А2 = 4*А1 В1 = ВВ — А2 В2 = SQRT(B1) BOTTOM = 2* А ТОР = —В + В2 ROOT1 = ТОР/ВОТТОМ Последняя запись не только неэффективна, но и порождает но- вые ошибки; более того, она трудна для понимания. Возникает проблема: как решить, что такое простой и что та- кое сложный оператор? Ответ зависит от пользователей и их зна- комства с языком программирования, а также от программируе- мой задачи. Одна из очевидных рекомендаций: при программиро- вании числовых выражений оператор не должен превышать одной строки. Психологи считают, что семь элементов — оптимальная длина оператора. Это длина телефонного номера. По-видимому, это чис- ло является подходящим ограничением. Каждый программист мо- жет варьировать эту границу в зависимости от своих индивидуаль- ных потребностей. Кроме того, он должен решить, что считать эле- ментом в операторе. По мере знакомства программиста с языком программирования длина оператора, как правило, увеличивается. Еще один путь упрощения программ — это постоянство приемов кодирования. Например, переключатели программы, должны всег- да использоваться одинаковым образом: значение «О» для обозна- чения состояния «Выключено», значение «1» для обозначения со- стояния «Включено». Кроме того, желательно для всех таблиц в одной программе применять один и тот же метод просмотра. При вводе используйте один и тот же размер поля для всех веществен- ных переменных и одинаковый размер поля для всех целых пере- менных. Это поможет вам избежать большого количества ошибок. Кроме того, лучше, не допускать смешения целых и вещественных значений при записи входных данных, если это возможно. 2.2. ЧТЕНИЕ ПРОГРАММ Кроме автора программу должен читать кто-нибудь еще. Если программист знает, что кто-то другой будет читать его програм- му, то отношение к кодированию резко изменяется. Программу снабжают комментариями, делят на параграфы, она становится ясной и точной.
48 ГЛАВА 2 В некоторых разработках использовался метод, гарантирующий простоту программирования, — так называемая контролируемая система программирования. В этой системе над составлением каждой программы работают по крайней мере два программиста. Первый пишет программу, вто- рой должен просмотреть и понять ее. Эта система имеет два пре- имущества. Во-первых, она гарантирует получение простых и по- нятных программ. Во-вторых, если первый программист по какой- либо причине не может довести программу до конца, другой в со- стоянии заменить его. Аналогичный метод — это оценка программы на выполнение стандартов. Для данной вычислительной машины устанавливают- ся определенные стандарты программирования. Затем каждая программа проверяется на выполнение этих стандартов, прежде чем она будет записана в библиотеку программ. Обычно все про- граммисты по очереди делают такую оценку. 2.3. ОПИСАНИЕ ЗАДАЧИ Нет ничего хуже, чем неполное или неправильное определение требований к программному обеспечению. Если заказчики не мо- гут определить свои запросы или если программист не может по- нять, чего хочет заказчик (под программистом здесь подразуме- вается тот, кто пишет спецификации проекта), то при разработке проекта можно ожидать серьезных затруднений. Говорят, что иде- альная программа — это такая программа, которая дает точный желаемый выход при неясно сформулированных требованиях пользователя. Однако редко нечеткое описание требований приво- дит к желаемому результату. Один из путей уточнения описания требований заказчика — это проведение регулярных просмотров для координации установлен- ных требований и действительных намерений заказчика. В резуль- тате этих проверок могут быть обнаружены несоответствия, кото- рые при отсутствии таких проверок выявились бы значительно позже. Следует отметить важность полноты и непротиворечивости предварительного проекта. Лучше проявить прагматизм в начале проектирования, чем вносить изменения, когда система уже го- това. 2.3.1. ПОСТАНОВКА ЗАДАЧИ Задача может быть определена хорошо или плохо. Для плохо сформулированной задачи нельзя составить четкого проекта про- граммы. К сожалению, большинство задач, подлежащих програм- мированию, определено плохо («Напишите программу, выдающую платежную ведомость»). Но если вам удастся четко определить задачу до этапа программирования, вы сэкономите много времени я избегните лишней работы.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 49 Если в проекте программы имеются пробелы, это обнаружится или во время программирования, или после передачи «завершен- ной» программы пользователю. Впоследствии программу надо- будет переделать в соответствии с правильным проектом. Очень- много переделок в программе связано с тем, что никто не забо- тится о правильности проекта с самого начала. Задачи, подлежащие программированию, обычно ставятся не теми, кто будет программировать. Имеются две причины, по ко- торым программист приступает к работе без полного понимания задачи. Во-первых, заказчик, ставящий задачу, может не иметь- четкого представления о ней; во-вторых, программист может не понять, чего хочет заказчик. Когда программист и постановщик, задачи —не одно лицо, первый вынужден работать с представле- нием второго о данной задаче. Заказчик должен подготовить описание задачи. Это необходи- мо даже в том случае, если программист является одновременна и постановщиком задачи. Такое описание заставит заказчика серьезно обдумать задачу и четко определить ее постановку. Да- лее должно состояться обсуждение задачи программистом и заказ- чиком. Если задача достаточно сложна, то обсуждение лучше провести до того, как заказчик попытается описать задачу. Эта поможет ему лучше оценить возможности вычислительной маши- ны, а также программиста. Такое обсуждение часто помогает за- казчику разобраться в своем представлении задачи. Далее программист должен переписать спецификации задачи,, ориентируясь на вычислительную машину. Необходимо сжатое, но полное описание задачи, объем которого может составлять от нескольких предложений для небольшой задачи до целой книги в случае большой системы. Это описание является основой для большого количества документации. Затем программист и заказчик должны вместе тщательно изу- чить написанные спецификации задачи, чтобы быть уверенными в их правильном понимании. Возможно, понадобится повторить эта несколько раз. Необходимо, чтобы заказчик уяснил для себя, чта он собирается получить именно то, что описано в спецификациях. Более поздние изменения или добавления не только приводят к удорожанию проекта, но и задерживают его завершение. Любая небрежность, допущенная на стадии определения задачи, вызовет впоследствии осложнения. Добивайтесь точности при определении задачи. После того как задача определена, лучше всего заморозить спецификации и в дальнейшем не вносить в них каких-либо из- менений или дополнений. Любой пересмотр откладывается на бо- лее позднее время. В том случае, если изменения вносятся после начала программирования, следует увеличить расходы и выделить 4—899
50 ГЛАВА 2 дополнительное время на проектирование. Более того, эти допол- нительные расходы должны быть достаточно высоки. Это один из способов контролировать изменения и предусматривать дополни- тельное время на их внесение. Многие проекты невозможно пла- нировать из-за постоянно изменяющихся спецификаций. Заказчи- ки, считающие, что ничего не стоит внести незначительное изме- нение в выходные результаты, не поймут также и то, почему нель- зя сделать небольшое изменение, например, в главном входном файле, не вызвав дополнительных издержек и расхода времени. Отсюда следует, что надо приучить заказчиков дорого платить за все изменения. Это заставит их более тщательно составлять спе- цификации. 2.4. ВЫБОР АЛГОРИТМА Важнейшим шагом для получения эффективной и правильной программы является составление алгоритма. При этом предпола- гаются правильный выбор языка и полнота спецификаций про- граммы. Таким образом, хороший алгоритм — необходимое, но не достаточное условие хорошей программы. Если пользователь фор- мулирует задачу в виде четкого алгоритма, то процесс проектиро- вания существенно облегчается. Вот простой пример вычисления значения Y: Y=Ax*+B#+Cx-]-D Прямое решение выглядит так: Y=A*X**3 + В*Х**2 4- С*Х 4- D . Но повторное умножение более эффективно, чем возведение в сте- пень при небольшом целом показателе степени, поэтому алгоритм может быть изменен следующим образом: Y==A»X«X*X 4- В*Х*Х 4- С*Х 4- D При таком алгоритме требуется выполнить шесть действий ум- ножения и три действия сложения. Казалось бы, больше ничего нельзя улучшить. В действительности этот алгоритм можно упро- стить, если воспользоваться, например, разложением полинома {методом Горнера): Y=Ax3+Bx?+Cx + D =x(Ax* + Bx + C) + D =х (х (Ах В) О') -f- D Теперь алгоритм вычисления имеет вид Y = Х*(Х*(А*Х 4- В) + С) 4- D В этом случае требуется выполнить по три действия умножения и сложения. Таким образом, мы уменьшили число операций, уве-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 5> Рис. 2.1. Блок-схема алгоритма 1. личив при этом точность. Это выражение называется вложенной формой полинома. Этот пример улучшения алгоритма иллюстрирует такое прави- ло: час, потраченный на выбор алгоритма, стоит пяти часов про- граммирования. Очень часто пренебрегают тщательным выбором алгоритма и программируют первый пришедший в голову алго- ритм. 4*
Рис. 2.2. Блок-схема алгоритма 2.
Чтение N Рис. 2.3. Блок-схема алгоритма 3.
54 ГЛАВА 2 Еще один пример, заслуживающий обсуждения, — задача опреде- ления, является ли число N простым. Простое число — это число, которое делится без остатка только на единицу и на само себя. Например, 3, 5, 7, 11, 13 и 17 — простые числа, а числа 4, 9, 21 и 35 не являются простыми. На рис. 2.1 показана блок-схема одно- го из алгоритмов определения того, является ли число N (W>1) простым. В соответствии с этим алгоритмом число N делят на 2, 3, 4,... .... N—1 до тех пор, пока N не разделится без остатка или пока в качестве делителя не будет испытано число N. Это не самый эф- фективный путь решения данной задачи, но наиболее очевидный. Нетрудно сообразить, что нет необходимости проверять, явля- ются ли все четные числа, меньшие N, его делителями. Достаточно проверить число 2, так как 2 — делитель, если любое другое чет- ное число является делителем. Значит, надо проверять только нечетные числа 3, 5, 7 и т. д. Это уменьшит объем вычислений почти наполовину. Тогда можно составить новую блок-схему ал- горитма (рис. 2.2). Этот алгоритм делит N на 2 и на все нечет- ные числа до тех пор, пока N не разделится на одно из них или не будет достигнуто N. Большинство программистов, освободив- шись почти от половины вычислений, сразу же приступят к про- граммированию. Но мы, чтобы подчеркнуть важность тщательно- го выбора алгоритма до начала программирования, проведем дальнейший анализ. Заметим, что достаточно проверить, являются ли делителями числа, меньшие или равные квадратному корню из N. Если су- ществует делитель, больший корня квадратного из N, то должен существовать и делитель, меньший квадратного корня из N. Если это утверждение не является для вас очевидным, разберите не- сколько примеров. Таким образом, мы получим еще одну блок- схему, показанную на рис. 2.3. Этот алгоритм делит N на 2 и нечетные числа, меньшие или равные квадратному корню из N, до тех пор, пока N не разде- лится без остатка или пока не будет испытано в качестве делите- ля число N. Подсчитаем количество, чисел, которое мы должны проверить в качестве делителя в каждом из трех алгоритмов: Алгоритм 1 | | Алгоритм 2 | | Алгоритм 3 Проверке подлежат все числа <# 2 и все нечетные 2 и все нечетные <V N <N 10 8 5 2 100 98 50 5 1000 998 500 16
ПРОЕКТИРОВАНИЕ ПРОГРАММ 55 Сравнивая второй и четвертый элементы последней строки таблицы, мы видим, что первый алгоритм проверяет более чем в 60 раз больше чисел, чем третий. С точки зрения эффективности выбор правильного алгоритма — наиболее ответственный шаг. Теперь мы имеем эффективный алгоритм, определяющий, яв- ляется ли данное число простым. Пусть нам надо не определить, является ли данное число простым, а сгенерировать все простые числа. В этом случае рассмотренный алгоритм был бы неэффек- тивным. Составить алгоритм решения такой задачи можно, если использовать решето Эратосфена. Возникает вопрос: как выбрать хороший алгоритм? Первое правило — не начинайте программировать первый пришедший в голову алгоритм, просмотрите по крайней мере несколько вариан- тов. Выберите лучший из них. Если вы рассмотрели только один алгоритм для решения вашей задачи, вряд ли он будет наилуч- шим. Выбирайте алгоритм задачи самым тщательным образом. Имеется немало литературных источников, где рассматрива- ются вычислительные алгоритмы. В трехтомнике Д. Кнута1) со- держится большое количество основных вычислительных алгорит- мов. Все профессиональные программисты должны быть знакомы с этим полезным изданием. Можно обратиться также к книге Ахо и др.* 2), а также к сборнику алгоритмов САСМ3). Кроме того, озна- комьтесь с литературными источниками, где рассматривается ва- ша задача, подлежащая программированию. Возможно, вы обна- ружите свой алгоритм в готовых программах своих коллег. Если алгоритм взят из литературного источника, то его разработка и использование легче и надежнее. Итак, хороший алгорит — необ- ходимое, но не достаточное условие хорошей программы. 2.5. ОПИСАНИЕ ДАННЫХ Другим фактором, сравнимым по значению с выбором алгорит- ма, является описание данных. Хорошо продуманное описание данных существенно сокращает программу. Так, полезно исполь- зовать массив данных в том случае, когда это наиболее очевид- ный способ их организации. Другой пример такого рода — воз- ’> Knuth D. Е., The Art of Computer Programming, Vol. 1—3, Addison-Wesley, Reading, Mass., 1969. [Имеется перевод: Кнут Д., Искусство программирования, тома 1—3, —М.: Мир, 1976—1978.] 2) Aho A. et al., The Design and Analysis of Computer Algorithms, Addison- Wesley, 1974г [Имеется перевод: Ахо А. и др., Построение и анализ вычислитель- ных алгоритмов. — М.: Мир, 1979.] 3* Collected Algorithms from the CACM (Association for Computing Machi-
56 ГЛАВА 2 можность использовать ссылки и указатели. Если нужно просле- дить отношения между родителями и их потомками на протяже- нии нескольких поколений, то легче всего это сделать с помощью ссылок и указателей. Следует выбирать такое представление дан- ных, которое наилучшим образом соответствует рассматриваемой задаче. Выбирайте представление данных, соответствующее задаче. Для знакомства с различными типами данных можно восполь- зоваться несколькими учебниками1). Программисты, использую- щие ФОРТРАН, которых интересуют нечисловые приложения, мо- гут обратиться к книге А. Дэя2). Почти все структуры данных мо- гут быть смоделированы на любом языке, и эти методы обсужда- ются в указанных здесь книгах, но лучше использовать тот язык программирования, в конструкциях которого предусмотрена нуж- ная вам структура данных. 2.6. ВЫБОР ЯЗЫКА ПРОГРАММИРОВАНИЯ Часто выбор языка программирования предопределен данной вычислительной системой или подготовкой программиста. Суще- ствуют серьезные основания для установления языковых стандар- тов для системы. Если применяют много разных языков для на- писания программ, то использование последних становится затруд- нительным. С другой стороны, соблазну писать на языке просто, потому, что он лучше знаком, следует также противостоять. Если программисту предоставляется возможность выбрать язык программирования, то подходящий для задачи язык высо- кого уровня — зто, как правило, наилучший выбор. Оператор язы- ка высокого уровня заменяет несколько операторов языка низкого- уровня. Следовательно, меньше вероятность сделать ошибки. Ис- пользование библиотеки заранее написанных и отлаженных под- программ и функций дает подобный эффект. Если язык программирования не подходит для данной задачи, то возникают проблемы при программировании и отладке. Оста- ется удивляться, что многие программы и сейчас составляют на ассемблере. Более того, зачастую пытаются использовать ФОРТ- РАН для экономических задач, а КОБОЛ — для вычислительных. Многих трудностей можно избежать, если использовать подходя- щий для данного случая язык программирования. Выбранный язык программирования оказывает влияние и на разработку про- екта. *> Harrison М. С., Data Structures and Programming, Scott, Foresmen and Co., 1973; Horowitz E., Sahni S., Fundamentals of Data Structures, Computer Scien- ce Press, 1976 и Knuth D. E., The Art of Computer Programming, Vol. 1. [Имеется перевод: Кнут Д., Искусство программирования, том 1. — М.: Мир, 1976.] ’> Day А. С., FORTRAN Techniques, Cambridge University Press, 1972.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 57 2.7. УНИВЕРСАЛЬНОСТЬ Универсальностью будем называть независимость программы от конкретного набора данных. Если программа зависит от дан- ных, она не универсальна; ниже приведен простой пример такой программы. ФОРТРАН: READ’(5,12) (А(1), 1=1, 25) SUM = 0.0 DO 20 I = 1, 25 SUM = SUM -f- A(I) 20 CONTINUE Эта программа читает вектор А с 25 элементами и суммирует последние. И если необходимо просуммировать элементы вектора с другим числом элементов, то в данной программе следует най- ти все строки, содержащие число 25, и заменить их на новое зна- чение. Такой подход чреват ошибками, так как требуется найти все места, где 25 используется как число элементов вектора А. Намного лучше такая программа: N =25 READ (5, 12) (A(I), 1= 1, N) SUM = 0.0 DO 20 I = 1, N SUM - SUM + A(I) 20 CONTINUE Изменив число N, мы можем обработать вектор любого размера от 1 до N (пока N не станет слишком большим). Хорошая универсальная программа должна обрабатывать вы- рожденные случаи (например, число элементов вектора равно 0 или 1) и печатать сообщение об ошибке, если N превышает допу- стимое значение. Тдгда программа является не только универсаль- ной, но и защищенной от ошибок. Используйте в качестве параметров переменные, а не константы. Программа может стать универсальной, если в качестве пара- метров вместо констант использовать переменные. Если же приме- нять константы, то при изменении параметров надо измейять в исходной программе каждый оператор, содержащий прежнюю константу. Эта процедура отнимает много времени у программи- ста и часто вызывает ошибки. Использование констант вместо пе- ременных не влияет на эффективность программы; таким обра- зом, нет особых причин для использования констант. В практиче-
58 ГЛАВА 2 ских условиях при изменении ситуации всегда проще изменить параметр программы, а не искать по всей программе фрагменты, на которые влияет происшедшее изменение. При этом ничего не стоит пропустить оператор, в котором встречается данная константа, и получить таким образом непра- вильную программу. Было предложено использовать хороший текстовой редактор, который бы автоматически изменял опреде- ленную константу в тексте. Подход творческий, но идея плохая, потому что одна и та же константа может быть использована для разных целей и поэтому не всегда должна быть изменена. Отсюда очевидна еще одна возможность возникновения ошибок, связан- ных с изменением констант. Вы не только можете пропустить ту константу, которую нужно изменить, но также исправить то, что не нуждается в исправлении. Очень многие изменения в програм- ме связаны с тем, что вместо переменных в качестве параметров используются константы. Укажем некоторые очевидные случаи, когда переменные могут быть использованы вместо констант: 1. Размер таблиц, массивов, списков. 2. Налоги, скидки, физические 1<онстанты и проценты. 3. Обозначения устройств ввода-вывода. Можно, поступить и по-другому, например хранить все подле- жащие изменению параметры в одном массиве. Эти параметры могут быть инициированы в одном месте программы и достаточно полно прокомментированы. Если программа простая, эти пара- метры можно поместить в начале. Если же программа содержит много подпрограмм, иногда полезно иметь одну подпрограмму, задающую начальные значения всех параметров. В противном случае изменение параметра может потребовать изменения несколь- ких подпрограмм, что- увеличит вероятность появления ошибок. Кроме того, параметры могут быть полезны во время отладки. Например, если размер таблицы или число рабочих областей за- давать как параметр, то этот параметр можно уменьшить во вре- мя отладки, чтобы легко получить переполнение таблицы или ра- бочей области. Таким образом вы отладите программу обработки ошибки для этих ошибочных условий. Опытные программисты знают, что универсальность програм- мы экономит время по дальнейшей работе над ней и обеспечива- ет широкие возможности по использованию. Если незначительное изменение или дополнение сделает вашу программу полезной для какого-то другого случая, никогда не пренебрегайте этим. Так создаются универсальные процедуры, которые вносятся в библио- теку программ для использования в других программах. Програм- мист или соответствующий отдел должен начинать свою деятель- ность с создания хорошей библиотеки нужных подпрограмм, на- личие которой существенно сократит случаи повторного програм- мирования.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 59 Создавайте универсальные программы. Еще одним доводом в пользу разработки универсальных про- грамм является тенденция к изменению программ. Разрабатывая универсальные программы, часто можно предвидеть будущие из- менения в спецификациях этой программы. Кроме того, тщатель- ный анализ возможных обобщений может оказать помощь в раз- работке проекта программы. Поиск универсальности обычно помо- гает предвидеть дальнейшие модификации и варианты. 2.8. БИБЛИОТЕКИ Чтобы повысить эффективность разработки программ, облег- чить отладку и тестирование, а следовательно, и сократить рабо- ту по созданию программ, используйте библиотеки программ. Это правильный подход к программированию, плагиат в программиро- вании не преследуется. Первый тип библиотечных подпрограмм представляет собой функции и подпрограммы, имеющиеся в наличии для данного язы- ка программирования. К этой категории относятся все стандарт- ные функции, такие, как квадратный корень, синус, косинус, аб- солютное значение. Для ознакомления с функциями и подпрограм- мами языка загляните в руководство по соответствующему языку программирования. Встроенные функции, которыми снабжен язык, конечно, лучше закодированы и тестированы, чем это сделали бы мы сами. Было бы расточительством программировать процедуры, которые доступны для использования в данном языке. Не перепрограммируйте функцию квадратного корня. Второй источник функций и подпрограмм находится в преде- лах вашей вычислительной системы. Большинство разработанных программ в той или иной степени доступно для использования. Следует лиШь выяснить, что именно и каким образом можно ис- пользовать. С какой целью применяются библиотечные подпрограммы? Одна из причин — увеличение надежности и уменьшение сложно- сти программирования. Программирование не так уже молодо, чтобы, переписывая по нескольку раз одни и те же программы, Добиваться увеличения их эффективности. Это уже признано в отношении таких процедур, как сортировка, слияние или вычисле- ние функций типа квадратного корня или косинуса. Когда заказывается программирование уникального модуля, то один заказчик полностью оплачивает его разработку и тести- рование. При программировании же стандартных модулей рас- ходы несет большая группа заказчиков. Таким образом, гораздо больше внимания уделяется его разработке и тестированию. При
€0 ГЛАВА 2 использовании таких стандартных подпрограмм может быть су- щественно улучшена надежность. Тем не менее имеется тенден- ция не вносить в библиотеку часто используемые части программ. Рассмотрим следующий фрагмент программы: IF А < В THEN GO ТО 10 GO ТО 20 10 IF А < С THEN GO ТО 30 GO ТО 25 i 30 SMALL = А GO ТО 100 20 IF В < С THEN GO ТО 50 25 SMALL = С GO ТО 100 - 50 SMALL = В I 100 .... j Внимательно изучив эту программу, вы, наверное, разберетесь, 1 для чего она предназначена. Но,’ во-первых, этот текст плохо чи- I таем, а, во-вторых, эта часто выполняемая операция обычно обес- | печивается встроенной функцией языка программирования. В дан- | ном случае это есть стандартная процедура по нахождению мини- J мальнбго элемента в группе элементов, которую можно предста- | вить в таком виде: ; 1 SMALL=MIN (А, В, С) | Вы, конечно, знакомы с функциями МАХ и MIN, но скажите, ког- | да вы в последний раз просматривали список стандартных функ- ций вашего языка программирования? Каждый из вас потратил | часы и дни, программируя какую-либо стандартную процедуру | только для того, чтобы обнаружить позже, что есть библиотечная 1 функция, которая делает то же самое. 1| При разработке нового электронного устройства инженеры не | проектируют каждый транзистор и конденсатор. Вместо этого они | выбирают уже имеющиеся надежные блоки питания, транзисторы | и конденсаторы. Но программисты слишком часто начинают с । нуля. | Третий источник библиотечных программ представляют книги | по программированию и применению вычислительных машин. Не- | мало книг содержат программы, которые можно использовать как j библиотечные, но должны быть предприняты некоторые усилия, f. чтобы получить интересующую информацию. Л Обычно каждый изготовитель ЭВМ имеет группу пользовате- ?' лей, которым он сбывает программы по номинальной цене. Этот источник также не следует забывать. Специалисты в области хи-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 6> мии собрали библиотеку прикладных программ, частично перера- ботав их. Хорошие статистические программы имеются в нескольких группах (BMD-программа в Калифорнийском университете в Лос-Анджелесе и SPSS-программа в Исследовательском центре общественного мнения Чикагского университета). И наконец, по- следний источник библиотечных программ — это вы сами. Соблю- дая принципы модульности и универсальности, вы постепенно со- здадите набор нужных подпрограмм. 2.9. ФОРМАТЫ ВВОДА-ВЫВОДА Форматы входных и выходных данных являются частью этапа- проектирования. Входные форматы должны быть разработаны с- учетом максимального удобства для пользователя и минимальной- возможности ошибок. Порядок переменных и форматы данных,, привычные для пользователя, помогут избежать ошибок и облег- чат использование программ. Постоянство входных форматов, как правило, также способст- вует уменьшению ошибок. Например, применение формата (17, 18,. 19, 18) менее предпочтительно, чем работа с постоянным форматом 4110. В первом случае пользователю трудно запомнить все вход- ные форматы, во втором случае общий формат запомнить значи- тельно проще—10 позиций. Выходные спецификации могут сильно различаться. Иногда* даются четкие инструкции и выходные данные подгоняются под определенный стандарт. Однако часто вообще отсутствуют какие- либо указания. Выходные данные подчас представляют собой страницы, заполненные числами, без всякой идентификации. Да- же сам программист без обращения к программе не может по- нять, что означают эти числа. Выходная информация должна идентифицироваться без при- влечения других источников. Выходные данные должны содержать- 1) идентификацию выходной записи, 2) описание или функцию- записи, 3) дату и 4) нумерацию страниц. Кроме того, каждая напечатанная группа элементов должна быть помечена. При табличной форме выдачи должны быть поме- чены строки и столбцы. Данные следует компоновать в функциональные или логические группы, разделенные пустыми строками или страницами. Мне ча- сто приходилось видеть людей, переписывающих листинг для включения его в отчеты. Это не только расточительно по време- ни, но и является источником ошибок. Выходные данные должны- быть такими, чтобы их можно было репродуцировать и публико- вать без какой-либо редакции. Программисты, которым предъявлены нереальные сроки завер- шения работы, не могут уделить достаточно внимания разработке
62 ГЛАВА 2 выходных форматов. Однако имеются серьезные основания зани- маться такой разработкой. Во-первых, хорошо продуманные вы- ходные форматы — помощь программисту при отладке и тестиро- вании. Непомеченные выходные записи требуют постоянного воз- вращения к программе, чтобы понять, что печатается. Кроме того, вид выходных распечаток — часто единственный критерий, по ко- торому оценивается мастерство программиста. Хорошая распечатка результатов создает у заказчиков мне- ние, что программист—мастер высокого класса. 2.10. СОЗДАНИЕ УСЛОВИЙ ДЛЯ РАБОТЫ ОПЕРАТОРА При проектировании программ часто забывают об одном важ- ном обстоятельстве — удобстве работы с программой для опера- тора ЭВМ. Кто-то ведь должен прогнать задачу, разместить вход- ные и выходные ленты, установить диски, изменить формат бу- маги. Конечно, невозможно облечь задачу в форму, удобную для оператора, но часто можно оказать ему хотя бы небольшую по- мощь. В некоторых задачах оператор должен поставить сначала бумагу одного формата, затем другого, потом опять первого. Не- большое изменение выходного потока могло бы устранить это неудобство. Еще один несложный прием: старайтесь использовать помеченные ленты, чтобы операционная система сама проверяла правильность поставленной ленты. Следует интересоваться, как продвигается работа у оператора и не возникает ли каких-либо проблем. Опытные программисты знают, что с операторами желательно поддерживать хорошие от- ношения. В некоторых организациях требуют, чтобы программи- сты сами иногда работали на вычислительной машине. Я также считаю необходимым, чтобы каждый программист работал в ма- шинном зале по крайней мере несколько дней в году. Это всегда очень полезно. 2.11. СКРОМНЫЕ ЦЕЛИ Часто мы строим слишком смелые проекты программ, стремясь обработать все возможные случаи. Это приводит к получению чрезвычайно больших и сложных программ. Вместо этого следует разрабатывать программы, которые обрабатывают почти все слу- чаи, запрограммировать идентификацию остальных случаев и оставить их для ручной обработки. Зачастую большая часть про- граммы используется для обработки чрезвычайно редких собы- тий. Так, в операционной системе OS 360 фирмы IBM постоянно находящаяся в оперативной памяти процедура датирования вклю- чает обработку високосных лет*\ Это событие происходит раз в ’> Brooks F. Р., Jr., The Mythical Man-Month and Other Essays on Software Engineering, Addison-Wesley, 1975. [Имеется перевод: Брукс Ф. П., Как проекти- руются и создаются программные комплексы. Мифический человеко-месяц. Очер- ки по системному программированию. — М.: Наука, 1979.]
ПРОЕКТИРОВАНИЕ ПРОГРАММ 63 четыре года и, по мнению автора указанной книги, вполне может быть доверено оператору. Разработка многих систем математиче- ского обеспечения могла бы быть успешной, но потерпела провал из-за излишне обширных замыслов. Система OS 360 была слиш- ком смелой для своего времени. В программировании, как, впро- чем, и в других областях жизни, можно извлечь больше пользы,, если скромнее оценивать свои возможности. Гораздо предпочти- тельнее программы,.которые правильно обрабатывают 99% Дан- ных и отвергают оставшийся процент, чем те, которые обрабаты- вают все данные, но часть времени ведут себя непредсказуемым образом. Более скромные по целям работающие программы по- лезнее неоконченных грандиозных проектов. 2.12. УСТАНОВЛЕНИЕ ЦЕЛЕЙ Уже на стадии проектирования должны быть установлены оп- ределенные цели. Вот некоторые из них: 1) высокий уровень надежности; 2) выполнение некоторого объема работы к определенной дате; 3) минимальное время разработки или минимальная стои- мость; 4) удобство и простота эксплуатации; 5) эффективность (объем памяти или быстродействие); 6) возможность введения модификаций; 7) универсальность. При разработке каждого проекта по программированию обыч- но ставят несколько целей, однако часто их не фиксируют. Иногда программисты не вполне осознают поставленную цель, а бывает и так, что два программиста, работая над одним и тем же проек- том, имеют в виду различные цели. Один пытается минимизиро- вать любой ценой объем памяти, другой — расходы на разработку. Устанавливайте цели проекта заблаговременно и точно. Предположим, что некоторая программа должна быть закон- чена к январю, использована один раз, а если не будет сделана в срок, то не будет использована совсем. В данном случае ни эф- фективность, ни простота модификации программы не являются целями. Рассмотрим другой случай. Необходимо разработать еже- недельно обновляемую программу, которая будет использоваться в течение длительного времени. Здесь существенны эффективность и простота внесения исправлений и, если программа будет сдела- на неделей позже, это не имеет решающего значения. Установле- ние и описание целей гарантирует то, что все будут работать, ру- ководствуясь едиными принципами. Однажды установленные цели Должны быть упорядочены по степени важности и присвоенным
€4 ГЛАВА 2 «весовым коэффициентам в случае, если необходимо какой-либо мз них отдать предпочтение. В процессе эксплуатации системы программного обеспечения могут возникнуть новые цели. Могут повыситься, например, тре- бования к ее быстродействию, или вообще какие-либо новые тре- бования могут быть наложены на эту систему. Изменение сущест- вующего программного обеспечения в соответствии с новыми целями называется подгонкой (retrofitting). Подгонка проводится, когда добавляются новые функции и упраздняются старые. 2.13. СЛОЖНОСТЬ Каждому известны два наиболее важных параметра программ: память и время. Однако существует еще один параметр, который необходимо учитывать, — сложность. Чем сложнее программа, тем труднее ее контролировать, тестировать, отлаживать и эксплуа- тировать. Ограничив параметр сложности, мы можем получить намного легче поддающийся управлению результат. В настоящее время существуют методы ограничения сложности программ. Программа для вычислительных машин — одна из наиболее сложных когда-либо создаваемых структур. Более того, из-за по- вторных вычислений с различными наборами данных программа -является наиболее испытанной структурой. Некоторые ошибки многих сложных структур так и остаются невыявленными, так как соответствующие ситуации никогда не наступают. Например, только во время землетрясений можно проверить сейсмоустойчи- вость здания. В разные времена истории человечества были созданы очень сложные структуры и выполнены операции впечатляющей слож- ности, но содержащие несколько серьезных ошибок. Под удачной структурой или операцией подразумевается такая, в которой •ошибки проектирования и внедрения могут быть выявлены во вре- мя использования. Что такое методы управления сложностью? Как они действуют? Каким образом можно их применить для программирования? Существуют два подхода к обеспечению качества: проведение исчерпывающего тестирования либо обеспечение высокого каче- ства системы на всех стадиях разработки. Для большинства программ невозможно провести исчерпывающего тестирования, поэтому приходится управлять сложностью программ. Метод, обычно используемый для управления сложностью, за- ключается в том, что процесс или структуру разбивают на не- большие, легко управляемые части, которые комбинируют для получения определенной функции. Такой подход используется в серийном производстве так же, как и во всех организациях — ад- министративных и промышленных. Увеличение сложности програм*
ПРОЕКТИРОВАНИЕ ПРОГРАММ 65 мы не пропорционально увеличению ее размера. При увеличении размера программы вдвое сложность может возрасти в четыре раза. Таким образом, проект программного обеспечения, по разме- рам вдвое превышающий заведомо успешный проект, нельзя полу- чить простым удвоением персонала. Это обстоятельство часто не учитывалось и явилось причиной появления многих неудачных проектов. Управление сложностью — очень важный фактор в программи- ровании. По существу мы занимаемся этим, определяя функцию каждого модуля независимым образом. Каждый модуль, получая входные данные, должен выдать определенные выходные данные. Модульное проектирование дает возможность составлять из моду- лей систему с более сложной структурой. Разбиение на модули и метод проектирования сверху вниз (это понятие объясняется ниже) позволяют нам контролировать сложность системы, определяя ее различные уровни. Детали ниж- него уровня могут игнорироваться, если нет необходимости в соответствующей этому уровню информации. Например, если читается основная программа и встретился вызов подпрограммы, выдающей заголовки некоторых сообщений, то можно пропустить эту подпрограмму и продолжать следовать логике основной про- граммы. Если же заголовки запрограммированы как часть основ- ной программы, то неизбежно выполнение этой части программы. Разбиение на модули — это мощное средство управления пара- метром сложности. Во-первых, модули позволяют разделить мно- жества переменных. Таким образом, уменьшается влияние многих возможных ошибок благодаря их изоляции в пределах каждого модуля. Например, чем больше модуль, тем больше вероятность использования одной и той же переменной для двух разных целей. Кроме того, чем больше модуль, тем труднее определить, что про- изойдет, если внести незначительное изменение в какую-либо часть модуля. И наконец, введение модулей уменьшает число воз- можных путей в программе, что является существенным обстоя- тельством при управлении параметром сложности. Следующий шаг в управлении общей сложностью — управле- ние сложностью в пределах каждого модуля; этот процесс обычно называется структурным программированием. 2.14. СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ Одним из методов, улучшающих программы, является струк- турное программирование. Он служит для организации проекти- рования программ и процесса кодирования таким образом, чтобы предотвратить большинство логических ошибок и обнаружить те, которые допущены. Структурное программирование сосредоточи- вается на одном из наиболее подверженных ошибкам факторов программирования — логике программы — и включает три глав- ные составляющие: 5—899
'66 ГЛАВА 2 1. Проектирование сверху вниз. 2. Модульное программирование. 3. Структурное кодирование. Вначале программа может иметь довольно ясную структуру, но по мере ее расширения становится очевидной необходимость Неструктурированный код Структурированный код IF р GOTO label q IF w GOTO label m ' L function GOTO label k label m M function GOTO label k label q IF q GOTO label t A function В function C function label r IF NOT r GOTO label s D function GOTO label r label s IF s GOTO label f E function label v IF NOT v GOTO label k J function label k К function END function label f F function GOJO label v label t IF t GOTO label a A function В function GOTO label w label a A function В function G function label u IF NOT u GOTO label w H function GOTO label u label w IF NOT t GOTO label у I function label у IF NOT v GOTO label k J function GOTO label k ©IF p THEN . A function В function ©IF q THEN © IF t THEN G function ©DOWHILE u H function ©ENDDO I function ©(ELSE) • ©ENDIF ©ELSE C’ function ©DOWHILE r D function ©ENDDO ©IF s THEN F function ©ELSE E function< ©ENDIF ©ENDIF ©IF v THEN J function ©(ELSE) ©ENDIF ©ELSE ©IF w THEN M function ©ELSE L function ©ENDIF ©ENDIF К function END function Рис. 2.4. Пример структурированного и неструктурированного кодирования. изменений и корректировки. Для этой цели привлекается большое количество операторов GO ТО и меток. Таким образом, после ав- тономной и комплексной отладки, а также после дальнейшей экс- плуатации программы логика последней становится довольно сложной. На рис. 2.4 показаны примеры обычной неструктуриро- ванной и структурированной программ. Наиболее очевидное отлй-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 67 чие состоит в том, что в неструктурированном варианте использу- ются операторы GOTO и метки. Просмотрите обе программы и обратите внимание на сокращение числа' переходов в структури- рованной программе. 2.14.1. ПРОЕКТИРОВАНИЕ СВЕРХУ ВНИЗ Проектирование программ сверху вниз подобно написанию ста- тьи сверху вниз. Процесс написания статьи имеет иерархическую структуру и начинается с вершины иерархии, т. е. с краткого об- зора. Разработку проекта обычно начинают с исследования целей и определения основных задач, ведущих к достижению этих це- лей. Если проект очень большой, то необходимо провести его раз- биение, которое должен выполнить компетентный и квалифици- рованный специалист. Вначале необходимо написать то, что вы хотите сделать, на естественном языке. Этот шаг часто многое раскрывает. Нередко вы обнаруживаете, что не в состоянии записать задачу на есте- ственном языке. В таком случае не надейтесь, что вам удастся составить программу. И потом ведь намного легче переделать опи- сание задачи на естественном языке в период разработки специ- фикаций, чем переписывать потом программу, которую считают уже завершенной. Таким образом, важно сформулировать задачу правильно на стадии проектирования, чтобы не исправлять ее позднее на стадиях программирования и отладки. Сначала напишите программу на естественном языке. Метод проектирования сверху вниз предусматривает вначале определение задачи в общих чертах, а затем постепенное уточне- ние структуры путем внесения более мелких деталей. Проектиро- вание представляет собой последовательность шагов такого уточ- нения. На каждом шаге необходимо выявить основные функции, которые нужно выполнить, т. е. данная задача разбивается на ряд подзадач, пока эти подзадачи не станут настолько простыми, что каждой из них будет соответствовать один модуль. Именно такой традиционный и по существу иерархический под- ход применяется при создании сложных структур в других обла- стях, например в технике, серийном производстве, при написании статей. Затем проект системы может быть проверен и подтверж- ден моделированием или расчетами. Действие каждого модуля может быть описано одной фразой. Если описание модуля зани- мает больше одной строки — перепроектируйте его. Далее следует описать данные, указывая их структуру и ос- новные процессы обработки. Это описание должно включать тща- тельно отобранные примеры, убедительно демонстрирующие функ- ции системы и их наиболее существенные варианты. Такие при- 5*
68 ГЛАВА 2 меры будут полезны позже на стадии тестирования. При описа» нии модуля должны быть описаны его тестовые данные. Тестиро- вание программы неизбежно, поэтому выявление требований к тестированию (слабых и критических мест) заранее, на стадии проектирования является хорошей практикой. Логическая про- верка фрагментов программы должна уменьшить необходимость тестирования конечной программы. Чтобы выполнить это «ручное» тестирование, спецификации системы должны быть достаточно точными. Разрабатывайте тестовые данные заранее. ' Основное преимущество такого метода работы — то, что он обеспечивает создание документации. Документацию всегда на- чинают создавать во время разработки спецификаций, но часто результаты этой начальной работы утеряны к моменту формиро- вания окончательной документации. Лучше всего, когда большая часть документации разработана до начала программирования. Это также способствует улучшению программы. Ведь програм- мист должен более тщательно обдумать структуру, данные и те- стирование своей программы, чтобы записать все это на бумаге. При использовании структурного программирования правиль- ность программы обеспечивается уже самим методом проектиро- вания. Создание полного проекта системы по уровням, начиная с верхнего, приводит к уверенности, что система математического обеспечения будет удовлетворять поставленным целям и любые допущенные ошибки станут очевидными настолько рано, насколь- ко это возможно. Проектирование должно быть завершено до на- чала программирования. Как только приступают к программиро- ванию, создается психологический барьер, препятствующий даль- нейшим улучшениям проекта. Следует сделать несколько итера- ций проекта прежде, чем начать программирование. В небольших проектах, где этап проектирования является менее решающим, часто программисты приступают к программированию слишком рано. Многие программы вообще не проектируются, а создаются сразу в форме кода. 4 Прежде Чём начать программировать, разработайте проект. Теперь, когда разъяснен метод проектирования сверху вниз, остановимся и попытаемся разработать проект, заключающийся в том, чтобы одеть мужчину. Начнем с определения задачи в об- щих чертах, а затем постепенно уточним инструкции, пока не по- лучим полный набор инструкций. Итак, попытаемся сделать это. Очевидный первый шаг мог бы установить цель: Одеть
ПРОЕКТИРОВАНИЕ ПРОГРАММ 69 Первый уровень уточнения мог бы выглядеть так: Одеть нижнюю половину Одеть верхнюю половину Нижнюю половину можно было бы одеть в два этапа: Надеть трусы и брюки ' Надеть ботинки и носки В два этапа можно одеть верхнюю половину: Надеть майку Надеть рубашку Окончательный проект выглядит так: Надеть трусы Надеть брюки Надеть ботинки Надеть носки Надеть майку Надеть рубашку Таким образом, мы имеем довольно подробные спецификации проекта. Следующий шаг заключается в том, чтобы вручную про- моделировать его и проверить ошибки. А ошибка здесь есть! Если вы не можете найти ошибку ручным просчетом, попытайтесь ис- пользовать спецификации проекта, чтобы одеться завтра утром. Большинство современных методов создания математического обеспечения допускает появление ошибок в спецификациях про- екта, которые сделаны на предварительных этапах и остаются необнаруженными до тестирования и эксплуатации. Однако цена обнаружения и корректирования ошибок в системе возрастает по мере приближения программы к завершению. Исправление оши- бок, найденных во время проектирования, относительно недорого (переделать проект) по сравнению с обнаружением и исправлени- ем ошибок на конечном этапе тестирования (перепрограммировать задачу). Кроме того, поздно найденные ошибки затрагивают мно- гих и вызывают необходимость связи между группами (например, между программистами, теми, кто делает документацию, и заказ- чиками). Исключайте ошибки с самого начала. В большинстве случаев ошибки в системе — результат непол- ных или противоречивых спецификаций проекта. В работе [6] по- казано, что на стадии спецификаций проекта вносится до двух третей ошибок. Следовательно, именно здесь должны быть сосре- доточены наши главные усилия, так как ошибки при проектиро- вании дороже всего обходятся. Хорошее программирование не спасет плохой проект.
70 ГЛАВА 2 Плохо спроектированные программы настолько распростране- ны, что введен новый термин для их обозначения: «клудж»1'1— си- стема, представляющая собой «набор плохо совместимых друг с другом частей, формирующих жалкое целое». Этот термин опре- делен в статье Дж. Грэнхолма* 2) и применяется сейчас для обозна- чения плохо спроектированной программы, неважно составленной документации или даже неудовлетворительной работы целого вы- •числительного центра. 2.14.2. МОДУЛЬНОЕ ПРОГРАММИРОВАНИЕ Чтобы преуспеть в структурном программировании, программу следует представить в виде модулей. Модульное программирова- ние— это процес разделения программы на логические части, называемые модулями, и последовательное программирование каждой части. Когда большая единая задача делится на подза- дачи, то значительно проще понять и прочесть программу. Это; обычный способ управления сложной ситуацией: «Разделяй и властвуй!» Если проведено проектирование всей задачи сверху вниз, то она, естественно, разбивается на подзадачи для возмож- ных модулей. При этом преследуют 'две цели: 1. Необходимо добиться того, чтобы программный модуль был правильным и не зависел от контекста, в котором он будет исполь- зоваться. 2. Следует стремиться к тому, чтобы из модулей можно было формировать большие программы без каких-либо предваритель- ных знаний о внутренней работе модуля. Прикладные подпрограммы и стандартные процедуры — при- меры удачных модулей. Мы используем процедуру вычисления функции квадратного корня в любом контексте и не ожидаем по- бочных эффектов в других частях программы. Нам хотелось бы, чтобы наши собственные модули взаимодействовали таким же об- разом, не оказывая непредусмотренного влияния друг на друга. Размер модулей Никто не может точно назвать наилучший размер модуля, но обычно используемое ограничение — около 60 строк. Такая длина ’> Слово «клудж» (kludge — англ.), происходящее от немецкого kluge — ум- ный, в результате курьезов эволюции языка приобрело иронический смысл и ста- ло употребляться в противоположном значении: «не очень остроумно», «довольно нелепо». Именно в этом смысле, став существительным, слово «kludge» использу- ется для характеристики плохо спроектированных систем, схем и программ.— Прим, перев. 2> Granholm J. W., How to Design a Kludge, Datamation (February 1962); по этому вопросу см. также Faith, Hope and Parity, Ed. by J. Moshman, Thompson Book Company, 1966.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 7Г. программы удобна для восприятия, так как она легко охватыва- ется и запоминается. Это число строк помещается на одной стра- нице, кроме того, его легко прочесть на терминале вычислитель- ной машины. Очевидно, существуют исключения, но тем не ме- нее 60 строк — это удобный размер модуля. Иногда делают модули больших размеров и руководствуются другими соображениями при выборе размера модуля. Мы хотим, чтобы модуль соответствовал одной легко охватываемой задаче. В некоторых организациях допускаются модули до 100, 200 или даже 300 строк. Нам кажется, что верхний предел 300 строк ред- ко используется и необходимость в нем сомнительна. При выпол- нении нескольких больших проектов по программированию (ра- боты [3, 4]) придерживались предела в 60 строк и получили вполне работоспособные системы. Если вы не можете ограничить подзадачу 60 строками, но справитесь с составлением более чем одной подзадачи, тогда вам следует разбить подзадачу на не- сколько модулей. Хотя мы не можем установить точный макси- мальный размер модуля, существует общее соглашение, что ма- лый модуль лучше большого. Короткие модули предпочтительнее длинных. В некоторых системах размер модуля ограничивается 4096 байт, или 1024 словами. Но это кажется менее удобным, чем ис- пользование предела в 60 строк. Ведь нетрудно установить, где оканчиваются 60 строк, но не ясно, где кончаются 4096 байт. Независимость Следует стремиться к независимости между модулями, или подпрограммами. Для достижения этого требуется, чтобы модуль не зависел от а) источника входных данных, б) места назначения выходных данных и в) от предыстории, иначе сложность системы резко возрастает. При разбиении программы на функциональные блоки, называемые модулями, можно обеспечить некоторую са- мостоятельность последних, хотя определенная зависимость все- таки существует, например от параметра и последовательности вызова. Каждый модуль должен иметь свое назначение, отличающееся от назначения других модулей. Это должен быть замкнутый блок, вход и выход которого точно определены. Стремление к функцио- нальной независимости подпрограмм хорошо аргументировано: в этом случае менее вероятно, что изменение в одной подпрограм- ме воздействует на остальную часть программы. Воздействие из- менения в одном модуле (или части программы) на другую часть программы называется волновым эффектом (ripple effect). Этот эффект можно уменьшить, сведя к минимуму связь между моду-
ГЛАВА 2 72 -лями, т. е. сократив количество путей, вдоль которых изменения ;или ошибки могут проникнуть в другие части системы. Простой путь уменьшения волнового эффекта — избегать использования глобальных переменных (COMMON-переменных) и делать моду- ли небольшими. Минимизация взаимосвязей между модулями — модульное сцепление (module coupling) — происходит за счет усиления связей между элементами одного модуля (модульная прочность — module strength). Таким образом, тесно связанные элементы мы стремимся поместить в один модуль, а несвязан- ные— в разные. Высокая степень независимости, характерная для структурного программирования, и минимальная связь модулей приводят к ограничению влияния дефектов средств программного обеспечения на модуль. Использование модулей приводит к уменьшению сложности. Фактор сложности при этом включает три составляющие: функцио- нальную, распределенную и связи. Функциональная сложность обусловлена тем, что один модуль выполняет слишком много функ- ций. В этом случае для их различения требуется очень сложное те- стирование. Распределенная сложность — это сложность идентифи- кации общей функции, распределенной между несколькими моду- лями, за счет чего утрачивается возможность уменьшения слож- ности всей программы при модульном программировании. Слож- ность связи определяется сложностью взаимодействия модулей при использовании общих данных. Наложение ограничений на размер подпрограмм не гаранти- рует использования метода модульного программирования при на- писании программ. Некоторые программисты, наслышанные про модульность, пишут программу, делят ее на четыре-шесть частей и говорят, что они используют принцип модульности. Излишне го- ворить, что это неправильный подход. Следует делать естествен- ное разбиение программы на модули — разбиение, соответствую- щее полученному на стадии проектирования сверху вниз. Разработка любой достаточно сложной программы состоит из нескольких различных этапов и начинается с исследования об- щих целей проекта. Это приводит к определению основных задач, предусматривающих осуществление этих целей. Определение ос- новных задач — обычно первая фаза проектирования программы, в течение которой и должно выявиться разделение на модули. Чтобы оценить независимость модулей, мы должны принять во внимание следующие обстоятельства. Если что-то в программе изменить, как это отразится на остальной части программы? Если любой программный модуль можно заменить новым, воспринима- ющим те же входные данные и выдающим такие же выходные, а на программе это не отразится, это означает, что модули незави- симы. Если существуют какие-то величины, подверженные изме- нениям (например, налоговые удержания в программе начисле- ния зарплаты), то их следует выделить в отдельный модуль, что-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 73 бы облегчить внесение изменений. Небольшие усилия по планиро- ванию будущих изменений позже, во время эксплуатации про- граммы могут «обернуться прибылью». Определение модуля Хорошие модули должны быть подобны математическим функ- циям. В математике имеется функция квадратного корня. Мы на- зываем ее SQRT (X) Функция квадратного корня может быть вычислена только для неотрицательных чисел. Таким образом, неотрицательные числа являются областью определения этой функции. Область определе- ния— это набор возможных входных значений. Функция не опре- делена для значений, выходящих за пределы этой области. Каж- дому допустимому аргументу X функция ставит в соответствие определенное значение. Набор возможных выходных значений на- зывается областью значений. Хотелось бы, чтобы модули были определены таким же обра- зом. Модуль должен также проверять аргументы на их принад- лежность области определения, как это делается для функции квадратного рорня. Когда аргумент функции квадратного корня получает значение, выходящее за пределы области определения, обычно выдается сообщение об ошибке. Это называется побоч- ным эффектом. Иногда побочный эффект включает код ошибки, который может быть обработан. Таким образом, для модуля должны быть определены 1) алгоритм решения задачи; 2) область допустимых входных значений; 3) область возможных выходных значений; 4) возможные побочные эффекты. Если эти четыре объекта надлежащим образом определены для каждого модуля, то результатом будут четко составленные модули, а следовательно, и хорошие программы. Однако часто область определения не проверяют, а побочные эффекты не опре- деляют. Следует сделать одно дополнительное замечание по поводу по- бочных эффектов. Лишь в редких случаях получение побочных эффектов должно прекращать выполнение программы. Вместо этого должна выявляться ошибка и должен происходить возврат к вызывающему модулю. Таким образом, нежелательно, чтобы процедура вычисления функции квадратного корня прекращала; вычисления при получении значения, выходящего за пределы об-, ласти определения (т. е. отрицательного значения). В этом слу- чае должно быть возвращено некоторое значение (часто нуль) вызывающему модулю, предупреждающее об ошибке. Затем вы-
74 ГЛАВА 2 зывающий модуль должен решить, прекращать или продолжать вычисления. Это гарантирует универсальность программы, так как ошибка идентифицируется и вызывающая программа может делать то, что необходимо. Именно из-за побочных эффектов затрудняется использование программы. Побочный эффект можно определить следующим об- разом: если модуль Мх не может быть заменен модулем Mz, где Мх и Mz — модули с одинаковыми входом и выходом, это значит, что модуль Мх имеет побочные эффекты. Приведем пример моду- ля с побочными эффектами: IF (MAP (I) .LT. 0) STOP FUNCTION MAP (N) COMMON I, K, L 1 = 0 K = K+ 1 L = K*L MAP = MAP — К RETURN END He вызывающий на первый взгляд подозрений оператор IF обусловливает ряд побочных эффектов, что можно обнаружить, если тщательно изучить программу. Данный пример также пока- зывает, почему пропадает желание использовать глобальные пе- ременные, или COMMON-переменные. При использовании гло- бальных переменных побочные эффекты могут легко остаться не- замеченными. Метод кодирования Сегменты программ должны быть составлены следующим об- разом. Каждый сегмент должен иметь один вход в начале и один выход в конце. Если другие сегменты вызываются внутри какого- либо сегмента, то в них входят через начало, выходят через ко- нец и возвращаются обратно в вызывающий сегмент. Главная процедура должна принимать все решения по управ- лению потоком данных для соответствующих обрабатывающих процедур. Переменные, общие для всех модулей, должны быть определены как часть главной процедуры в области COMMON. Локальные переменные должны использоваться только своими модулями. Каждый сегмент должен иметь как можно меньше ветвей вы- числений. Чем меньше размер модуля, тем, как правило, меньше
ПРОЕКТИРОВАНИЕ ПРОГРАММ 75 ветвей, которые надо тестировать. Чем проще структура, тем луч- ше. Каждая процедура должна выполнять только одну логическую задачу. Следует обеспечить невмешательство других сегментов в «кухню» вычислений каждой процедуры. Таким образом, обраба- тывающая процедура будет управляться только своим сегментом. Ни одно решение, принятое за пределами сегмента, не дблжно оп- ределять какое-либо действие внутри сегмента, и, наоборот, ни одно решение внутри сегмента не должно сказываться на действи- ях вне сегмента. Четкость программы в целом в большой степе- ни зависит от четкости структуры каждого модуля. Таким образом, каждая процедура является замкнутой. Управление передается от главной процедуры к вызываемой, и; когда последняя выполнит свою функцию, она возвратит управле- ние вызвавшей ее главной процедуре. Вызываемая процедура мо- жет в свою очередь вызвать другую процедуру, однако возврат- всегда будет происходить в процедуру, являющуюся вызывающей для данной процедуры. Задача об изображении шахматной доски Задач#: напечатайте изображение шахматной доски;- ее образец приведен на рис. 2.5. Эта задача может быть использована для демонстрации метода проектирования сверху вниз и принципа мо- дульности. Попробуйте сделать ее самостоятельно. Вероятно, су- ществует много хороших решений, и ваше решение может ока- заться даже лучше моего. Кроме того, это единственный способ выяснить, усвоили ли вы методы, описанные в данной главе. Проведите проектирование этой задачи сверху вниз, разбив ее на модули. Потом посмотрите предлагаемое здесь, решение. Если интерес у вас не пропадет, составьте программу для этой задачи и доведите ее до конца. Первый шаг при проектировании сверху вниз — описание того', что мы хотим делать. Задача заключается в том, чтобы напеча- тать изображение шахматной доски. Наша программа могла быть записана следующим образом: CALL PRINT-CHESSBOARD Так как не существует команды, которая бы делала это, мн долж-- ны разделить нашу задачу на подзадачи. На следующем шаге> произведем деление на верхнюю полосу (ТОР), середину? (MIDDLE) и нижнюю полосу (BOTTOM): CALL TOP-MARGIN CALL MIDDLE CALL BOTTOM-MARGIN
76 ГЛАВА 2 Рис. 2.5. Шахматная доска. Далее” в подзадаче MIDDLE выделим две составные подзада- чи, так как существуют два типа рядов шахматной доски. Попро- буем это сделать: CALL FIRST-ROW-TYPE CALL SECOND-ROW-TYPE В этой записи FIRS'!-ROW-TYPE — это модуль, формирующий ряд первого типа, a SECOND-ROW-TYPE — соответственно второ- го типа. И если мы повторим вызовы этих модулей по четыре ра- за, то получим все восемь рядов шахматной доски: DO 4 TIMES CALL FIRST-ROW-TYPE CALL SECOND-ROW-TYPE DOEND
ПРОЕКТИРОВАНИЕ ПРОГРАММ 77 Теперь рассмотрим задачу FIRST-ROW-TYPE. Нам нужно на- печатать строку определенного типа шесть раз: FIRST-ROW-TYPE DO 6 TIMES PRINT FIRST-ROW DOEND Аналогично поступим с рядами второго типа. Теперь мы имеем достаточно хороший проект всей задачи, разработанный с по- мощью метода сверху вниз и разбиения на модули. Вы, видимо, заметили, что верхняя и нижняя горизонтальные полосы модели доски одинаковы. Итак, предлагаем один из вариантов програм- мы на некотором метаязыке: CHESSBOARD CALL MARGIN DO 4 TIMES CALL FIRST-ROW-TYPE CALL SECOND-ROW-TYPE DOEND CALL MARGIN Заметьте, что пока мы не рассматриваем вопрос о программи- ровании этих процедур. 2.14.3. СТРУКТУРНОЕ КОДИРОВАНИЕ Человек имеет склонность к последовательным процессам. Мы предпочитаем последовательные чтение и обработку. Поэтому, чем больше разрывов в последовательности, тем труднее нам про- слеживать логику программы. После нескольких условных пере- ходов мы может запутаться в программе со сложным ветвлением. Структурное кодирование — это метод написания хорошо структурированных программ, который позволяет получать про- граммы, более удобные для тестирования, модификации и исполь- зования. Программы произвольных размера и сложности могут быть написаны на основе ограниченного множества базисных структур. Этот принцип положен в основу проектирования схем, где лю- бая логическая структура может быть создана из элементарных структур И, ИЛИ и НЕ; в булевой алгебре имеется соответствую- щая теорема. Так как отмеченное положение обосновано теорети- чески, нет необходимости доказывать его в каждом отдельном случае. Задача по проектированию логической схемы из базисных составляющих относится к полю деятельности инженера. Если он не в состоянии сделать это, подлежит сомнению его профессио- нальная компетенция.
78 ГЛАВА 2 Теория структурного кодирования Структурное кодирование состоит в получении правильной про- граммы из некоторых простых логических структур. Оно базиру- ется на строго доказанной теореме о структурировании (разд. «Про- екты» в конце главы), которая утверждает, что любую правиль- ную программу (с одним входом и одним, выходом, без зацикли- Рис. 2.6. Основные логические структуры (а — последовательности, б — выбора и в — повторения). ваний и недостижимых команд) можно написать с использовани- ем только следующих логических структур: 1) последовательности двух или более операторов (MOVE, ADD, ...) 2) выбора одного из двух операторов (IF THEN ELSE) 3) повторения (или управления циклом) оператора, пока вы- полняется некоторое условие (DO WHILE) На рис. 2.6 показаны эти основные логические структуры. Ком- бинации правильных программ, полученные с использованием трех основных управляющих структур (последовательности, вы-
ПРОЕКТИРОВАНИЕ ПРОГРАММ бора и повторения), также являются правильными программами. Заметьте, что каждая структура имеет один вход и один выход. Можно получить программу любого размера и сложности, приме- няя итерацию и вложение этих основных структур. При исполь- зовании только указанных трех структур отпадает необходимость в безусловных переходах и метках. Каждая структура прослежи- вается от начала до конца без каких-либо ветвлений. Применение структурного программирования в значительной степени уменьша- ет сложность программ. Рис. 2.7. Структура последовательности. Структура последовательности — формализация того, что опе- раторы программы выполняются в порядке их появления в про- грамме, пока что-то не изменит эту последовательность. Тот факт, что последовательность является управляющей логической структурой, иногда упускается из виду. Структура выбора — это выбор одного из двух действий, исходя из выполнения некоторо- го условия; ей соответствует оператор IF THEN ELSE. Структура повторения используется для повторного выполнения группы ко- манд до тех пор, пока выполняется некоторое условие. Итератив- ная управляющая структура приводится в действие оператором DO WHILE или итеративным DO. Существенным моментом является то, что при замене любого функционального прямоугольника в схеме правильной программы на любую из трех основных структур программа остается в классе правильных программ. На рис. 2.7 приведена схема программы, состоящей из последовательности трех функций. Если мы хотим заменить функцию В и использовать структуру выбора, то полу- чим схему, изображенную на рис. 2.8. Получение правильной про- граммы путем замены операторов программы на управляющие логические структуры называется вложением структур. Хотя теоретически возможно написать все правильные про- граммы, используя только три перечисленные выше основные ло- гические структуры, мы увидим, что небольшое расширение этого базиса облегчит программирование. Введенные структуры должны иметь один вход и один выход. Обычно добавляют операторы DO UNTIL и CASE. На рис. 2.9 показаны эти две логические струк- туры. Оператор DO UNTIL организует цикл, подобный циклу опе- ратора DO WHILE. Но существуют различия между этими опера- торами.
80 ГЛАВА 2 1. Цикл DO WHILE оканчивается, когда условие ложно, а цикл DO UNTIL — когда условие истинно. 2. В цикле DO UNTIL условие проверяется после выполнения операторов цикла, так что операторы тела цикла выполнятся по крайней мере один раз. Структура CASE — это схема разветвления и соединения вет- вей, которая используется для выбора одной из ветвей вычисле- но UNTIL CAS£ Рис. 2.9. Операторы DO UNTIL и CASE. ния в зависимости от значения целого выражения. Оператор CASE имеется только в АЛГОЛе. Несколько уже рассмотренных ранее понятий необходимо для поддержания структурного программирования. Во-первых, логи- ческая структура должна указываться отступами так, чтобы ло- гические отношения в программе соответствовали позициям в ли- стинге. Далее, программа должна быть разделена на группы команд (модули), которые легко охватываются. И наконец, про- грамма должна быть организована таким образом, чтобы ее мож- но было читать от начала до конца без прерываний, обусловлен- ных переходами. Руководствуясь этими принципами при написании программ, можно существенно уменьшить их сложность, так как программы можно будет читать сверху вниз как печатный текст. Перескаки-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 8t ваний, типичных для программ с операторами GO ТО, здесь не- будет. И действительно, при правильном использовании основных логических структур почти отпадает необходимость в использова- нии операторов GO ТО. Следует отметить еще одну характерную черту: структурированные программы требуют более • детального- проектирования до программирования. Иначе невозможно остать- ся в пределах требуемой структуры, и мы должны прибегать к переходам, чтобы реализовать непредусмотренные случаи. Чтобы получить простую структуру для каждого сегмента про- граммы, следует избегать операторов GO ТО. Каждый сегмент должен состоять из прямой последовательности операторов IF" THEN ELSE, циклов операторов CASE или таблиц решений. Та- кие языки, как ФОРТРАН или КОБОЛ, не имеют достаточного* количества языковых конструкций для структурного программиро- вания, поэтому при работе с ними трудно обойтись без операторов^ GO ТО. Для случаев программирования на языке, в котором нельзя исключить операторы GO ТО, и для тех, кто не может отказаться от использования GO ТО в силу многолетней привычки, предлага- ем два совета, которые помогут ограничить использование этого оператора: 1. Не прибегайте к GO ТО там, где можно заменить его опера- торами CALL или PERFORM. 2. Применяйте GO ТО только для переходов вперед. Переход: назад подразумевает цикл. Используйте в этом случае соответст- вующую конструкцию цикла. Как правило, следует избегать операторов GO ТО, кроме осо- бых случаев, таких, как моделирование управляющей логической: структуры в языке программирования, в котором таковая отсутст- вует. Качество программы часто обратно пропорционально числу включенных в нее операторов GO ТО. Эти операторы затрудняют чтение больших программ. В программе, написанной с помощью- операторов GO ТО, читатель, просмотрев несколько строк, пере- скакивает через определенное число строк или страниц, чтобы" прочесть еще несколько строк, опять перейти на другую страницу и т. д. Усложняя ситуацию, многие переходы являются или услов- ными, или разветвлениями, так что мы часто сомневаемся в пра- вильности выбранной ветви или забываем исходную задачу. Цель структурного программирования — обеспечить возможность чте- ния программы от начала до конца, следуя ее логике. Использование конструкций структурного кодирования В языках ПЛ/1 и АЛГОЛ имеется большая часть рекомендо- ванных выше операторов, поэтому на этих языках легко писать структурированные программы. Кроме того, оба языка имеют 6-899
S2 ГЛАВА 2 блочную структуру, так что нетрудно разбить программу на бло- ки, поскольку языки позволяют это. Так как ни в одном из ос- новных языков программирования нет всех необходимых для структурного программирования операторов, рассмотрим, как смо- делировать их в любом языке. ПЛЦ Язык ПЛ/1 располагает такими структурами, как последова- тельность, IF THEN ELSE, итеративный DO и DO WHILE. Этот язык обладает также возможностью группировки, так что в про- грамме могут быть выделены блоки, а процедуры или подпрограм- мы можно активировать с помощью операторов CALL или % INCLUDE. Оператор DO UNTIL не входит в состав набора операторов стандартного ПЛ/1, но его можно легко смоделировать следующим образом: /* DO UNTIL */ MORE.TO.DO = ТВ; DO WHILE (MORE_TO_DO); IF (условие) THEN MORE.TO.DO = 'O'B; END; Переменная MORE_TO_DO должна быть объявлена как BIT(l). Вышеприведенная модель оператора DO UNTIL всегда будет вы- полнена по крайней мере один раз, так как проверочное условие изменяется только в конце группы DO WHILE. Оператор CASE также не входит в состав стандартного ПЛ/1. Вот как можно представить этот оператор: /* CASE */ CASEJNDEX = выражение; IF (CASE INDEX < 11 CASE-INDEX > CASE_max) THEN CASE-INDEX =n; GO TO CASE (CASE-INDEX); CASE1: DO; GO TO END-CASE; END; CASE2:
ПРОЕКТИРОВАНИЕ ПРОГРАММ 83 DO; GO ТО END_CASE; END; CASE3: : CASEn: DO; • Это соответствует выбору ошибочного случая GO ТО END-CASE; END; END-CASE: ; При таком представлении оператора CASE требуется следую- щее описание: DECLARE CASE (l:n) LABEL INITIAL (CASE1, CASE2, CASE3, .... CaSEn); Этот пример подходит для любого числа разветвлений. Нужно лишь проверить, не превышает ли значение переменной CASE- INDEX допустимого значения. Оператор CASE особенно удобен, когда предстоит выбор из большого числа вариантов. Предполо- жим, нужно выбрать одну из 50 процедур, используя двузнач- ные целые числа. Можно, конечно, написать 50 операторов IF, но здравый смысл подсказывает, что оператор CASE сделал бы это намного лучше. В случае пустого тела CASE можно оставить толь- ко оператор GO ТО. Выбор одной и той же процедуры в несколь- ких случаях можно было бы осуществить, поставив более одной метки перед одним CASE. Если возможностей для выбора немного или нет подходящего численного кода, то для моделирования структуры CASE можно использовать вложенные операторы IF: IF (CODE = 'А') THEN CALL CASE-A.PROC; ELSE IF (CODE = 'C') THEN CALL CASE_C_PROC; 6*
«4 ГЛАВА 2 •ELSE IF (CODE='3') THEN CALL CASE_3_PR0C; ELSE CALL CODE.ERROR.PROC; Программа может быть сделана более эффективной, если упоря- дочить варианты с учетом наибольшей вероятности выбора. АЛГОЛ Большинство версий АЛГОЛа имеет все или почти все реко- мендованные структуры. Обычно включается и оператор CASE. Если оператора DO UNTIL нет, его можно смоделировать так же, жак в ПЛ/1. КОБОЛ В КОБОЛе структурное кодирование не встречает трудно- стей. КОБОЛ имеет структуры последовательности, IF THEN .ELSE, а также и оператор CALL — для подпрограмм. Предусмот- рен также оператор PERFORM, который может быть использован •как итеративный DO. .КОБОЛ располагает возможностью группи- ровки операторов, но при этом ни один оператор, кроме последне- го, не оканчивается точкой. Это ограниченная блочная структура, так как здесь не допускается вложенных блоков и требуется, что- бы ни один оператор в группе не оканчивался точкой. Когда IF THEN ELSE используется для условного выполнения •единичных операторов, то не возникает никаких проблем. Струк- туру IF THEN ELSE можно применять также для выполнения .групп операторов, если внутри этих групп нет точки: IF (условие) THEN группа операторов 1 ELSE группа операторов 2. Трудности возникают, когда внутри первой группы операторов требуется IF THEN (без ELSE): IF (условие р) THEN IF (условие г) THEN группа операторов 1 ELSE группа операторов 2.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 65 Сложности связаны с тем, что оператор ELSE будет спарен не с тем THEN, с которым нужно. В КОБОЛе ELSE должен со- ответствовать верхнему THEN, т. е. в рассматриваемом примере необходимо спарить ELSE с первым THEN. Но тогда в группе операторов, расположенной до ELSE, будет сделана синтаксиче- ская ошибка. Эту трудность можно разрешить, если добавить еще один оператор ELSE и сочетание NEXT SENTENCE: IF (условие р) THEN IF (условие г) THEN группа операторов 1 ELSE NEXT SENTENCE ELSE группа операторов 2. При использовании вложенных операторов IF добавление ELSE NEXT SENTENCE — наилучший способ избежать непра- вильного соответствия THEN и ELSE. Оператор PERFORM вместе с оператором КОБОЛа UNTIL действует так же, как традиционный оператор DO WHILE, по- скольку условие проверяется до выполнения параграфа. Это вы- глядит, так: PERFORM параграф UNTIL (условие). Моделируя оператор DO UNTIL, мы хотим, чтобы параграф вычислялся по крайней мере 1 раз. Мы можем сделать это так: PERFORM параграф. PERFORM параграф UNTIL (условие). Оператор CASE можно получить, применив оператор КОБОЛа GO TO...DEPENDING: GO ТО case-1, case-2...case-n DEPENDING ON IDENTIFIER. Каждый именованный параграф должен оканчиваться операто- ром GO ТО, ведущим к общей собирающей точке. Только один параграф будет переходить к вышеописанному GO ТО — тот, кото- рый обрабатывает значение п, выходящее за пределы запланиро- ванной области. Оператор CASE подобен описанному в разделе
86 ГЛАВА 2 ФОРТРАН На ФОРТРАНе труднее написать структурированную программ му, так как в этом языке нет группирующих структур типа IF THEN ELSE. Существует несколько способов решения этой проб* лемы. ФОРТРАН имеет оператор IF, таким образом, часть проб- лемы уже решена. В случае единичных операторов лучше всего применить последовательно два оператора IF: IF (условие) оператор 1 IF (.NOT. условие) оператор 2 Первый оператор IF обрабатывает часть THEN, второй опера- тор IF — часть ELSE. В ФОРТРАНе нет блоков. Чтобы выполнить блок операторов, определяемый оператором IF, мы можем сделать так: С IF THEN ELSE. IF (условие) GO TO 50 выполняется, если условие ложно GO ТО 75 50 ••• выполняется, если условие истинно 75 CONTINUE С END IF THEN ELSE. Если часть ELSE не нужна, то этот фрагмент можно сократить: С IF THEN. IF (NOT условие) GO TO 75 часть THEN 75 CONTINUE C END IF THEN. Это, конечно, менее наглядно, чем при наличии оператора IF THEN ELSE, но делает то же самое. Здесь соблюдается правило использования GO ТО в структурном программировании (т. е. GO ТО следует применять только для перехода вперед). ФОРТ- РАН обладает свойством модульности, которое можно использо- вать для формирования блоков программы. Оператор DO WHILE в ФОРТРАНе можно реализовать не- сколькими способами. Во-первых, есть оператор цикла DO, кото- рого часто бывает достаточно. Простой путь моделирования DO*
ПРОЕКТИРОВАНИЕ ПРОГРАММ 87 WHILE — использование стандартного цикла ФОРТРАНа с очень большим конечным значением параметра: С DO WHILE. DO 100 К= 1, 100000 IF (условие) GO ТО 150 100 CONTINUE 150 CONTINUE C END DO WHILE. При моделировании оператора DO UNTIL в конце тела цикла делается проверка на невыполнение условия: С DO UNTIL. DO 100 K=l, ЮОООО IF (NOT условие) GO TO 150 100 CONTINUE 150 CONTINUE C END DO UNTIL. И наконец, можно прибегнуть к конструкции CASE, которая реа- лизуется в ФОРТРАНе с использованием вычисляемого GO ТО следующим образом: С CASE STATEMENT. IF (KASE .LT. 1 . OR.KASE .GT. 4) KASE=5 GO TO (10, 20, 30, 40, 50), KASE C CASE 1. 10 CONTINUE GO TO 200 C CASE 2. 20 CONTINUE GO TO 200 C CASE 3 C CASE n-H, ERROR CASE. 50 CONTINUE 200 CONTINUE C END CASE.
88 ГЛАВА 2 Этот метод может быть применен для обработки любого числа разветвлений. Предшествующий сегмент будет выполнять одну из частей программы в зависимости от выполнения условия. Опе- раторы GO ТО используются для перехода вперед. Необходимо- проверить, чтобы метки вычисляемого GO ТО не выходили за пре- делы допустимой области. 2.14.4. ВЫВОДЫ ПО СТРУКТУРНОМУ ПРОГРАММИРОВАНИЮ Одно из преимуществ структурного программирования заклю- чается в том, что программа может быть проанализирована путем проверки структуры, когда проектировщик или программист зна- комит с программой своих коллег. Таким образом, становится воз- можным обнаружить ошибки уже на стадии программирования^ поэтому они обходятся сравнительно недорого. Процесс проверки структур подробно рассмотрен в гл. 5. GO ТО: PROCEDURE OPTIONS (MAIN); /* ЭТО ПРОГРАММА, В КОТОРОЙ СЛИШКОМ МНОГО ОПЕРА. ТОРОВ GO ТО. ЧТО ПЕЧАТАЕТ ЭТА ПРОГРАММА? */ ' К = 0; GO ТО L4; L2: PUT LIST ('D') ; GO ТО L3; L7: PUT LIST ('E') ; GO TO L5; L4: PUT LIST ('H') ; GO TO L7; L3: PUT LIST ('O') ; GO TO LI; L5: PUT LIST ('L') ; К = К +1; IF K<2 THEN GO TO L5; GO TO L3; LI: /* ИСПОЛЬЗУЕТ ЛИ ВАША ПРОГРАММА ОПЕРАТОРЫ GO ТО ПОДОБНЫМ ОБРАЗОМ? */ /* УКАЗАНИЕ: ГЛАВНЫЙ ПРОГРАММИСТ СЧИТАЕТ, ЧТО ИС- ПОЛЬЗОВАНИЕ ОПЕРАТОРОВ GO ТО В ВАШЕЙ ПРОГРАММЕ НЕЖЕЛАТЕЛЬНО */ END GO ТО; Рис. 2.10. Многократное использование операторов GO ТО в программе. При использовании структурного программирования сущест- венно улучшается читаемость программы. Большие программы с самого начала разбивают на логические блоки подобно тому, как книгу делят на главы, а главы — на разделы. Каждый логический сегмент ограничен одной страницей программы с простой струк- турой, так что читателю несложно понять, что делает каждый сег- мент. Сегмент имеет только один вход в начале и один выход в конце. И наконец, отсутствуют операторы GO ТО, поэтому про-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 89 грамма может быть прочитана с начала до конца последователь- но. На рис. 2.10 приведен пример программы, в которой насчиты- вается слишком большое число операторов GO ТО. Стремитесь к минимальному использованию операторов GO ТО. В КОБОЛе и ФОРТРАНе трудно обойтись совсем без опера- торов GO ТО. Кроме того, программисты с большим опытом про- граммирования на этих языках привыкли мыслить «в терминах <Ю ТО». Таким образом, стремясь избежать их использования, про- граммист может оказаться в затруднительном положении. Ве- роятно, при работе с этими языками лучше всего ориентироваться на экономное применение указанных операторов. Для небольших программ можно допустить использование одного оператора GO ТО, для программ большего размера—двух-трех. Руководствуясь принципами структурного программирования, можно перепроекти- ровать программу таким образом, чтобы убрать большую часть операторов GO ТО. 2.15. РАЗМЫШЛЕНИЯ О СТРУКТУРНОМ ПРОГРАММИРОВАНИИ Распространено мнение, что если лицо, ответственное за раз- работку программы, или преподаватель запретит использование операторов GO ТО, то будут получаться хорошие программы. Я не могу с этим согласиться. Я знаю программистов, которые созна- тельно искажают программу только с той целью, чтобы избежать использования единственного оператора GO ТО. Они пытаются получить программу без операторов GO ТО только ради того, что- бы сказать, что в их программе этих операторов нет. Из некото- рых программ, составленных на определенных языках, можно пол- ностью исключить операторы GO ТО; в других же задачах и на других языках без них просто нельзя обойтись. Структурное программирование часто приводит к «пуристско- му синдрому», который состоит в стремлении в любой ситуации написать полностью структурированную программу. Следует по- нять, что структурное программирование не является самоцелью, его назначение — получение хорошей программы. Даже в самой лучшей программе иногда необходимы операторы GO ТО (напри- мер, для выхода из группы внутренних циклов по ошибочному условию). Однако чтобы это не выглядело как исключение из пра- вил, лучше иметь письменное подтверждение и согласие руко- водства для каждого нарушения стандартов структурного про- граммирования. Использование оператора GO ТО может оказаться уместным в лучших структурированных программах1). Желательно не только Более подробно этот вопрос рассматривается в статье Knuth D. Е., Struc- tured Programming with GO TO Statements, ACM Computing Surveys (Decem- ber 1974).
90 ГЛАВА 2 s==f отсутствие операторов GO ТО, но и наличие структуры. Хорошо разработанная структура автоматически приводит к использова- нию небольшого количества операторов GO ТО. Повышенный интерес к структурному программированию спо- собствовал возникновению мнения, что при его применении будут получаться правильные и надежные программы. Хотя, возможно, это и так, было бы наивно ожидать в этом случае получения со- вершенных программ. Даже в области прикладных наук, где су- ществуют более проверенные методы проектирования, чем в про- граммировании, мы до сих пор слышим о мосте или плотине, раз- валившихся по окончании их строительства. Хотя в математике обязателен строгий подход, история науки дает много примеров ошибок. В интересной статье П. Дэвиса1) обсуждена частота оши- бок в математических работах. Несколько сот ошибок было обна- ружено в «Справочнике математических функций». Редактор жур- нала Mathematical Reviews подсчитал, что 50% всех математиче- ских работ содержит ошибки. Издана книга объемом 130 страниц, где перечислены ошибки, допущенные первоклассными матема- тиками. Хотя структурное программирование широко используется, а применение операторов GO ТО не рекомендуется, необходимо сделать вывод, что нет таких правил, следуя которым можно со- здавать понятные и проверенные программы, не содержащие оши- бок. Существуют основные принципы, но индивидуальный стиль программиста, четкость и творческий подход (или отсутствие этих качеств) будут существенно влиять на полученную программу. 2.16. КОДИРОВАНИЕ СВЕРХУ ВНИЗ Кодирование задачи следует выполнять сверху вниз, что озна- чает кодирование в первую очередь главной (управляющей) про- граммы, так называемого скелета программы. Это лучше всего выполнять независимо от машины и языка. Для отображения за- дачи следует использовать систему обозначений, а не машину и язык программирования. Вместо создания выходной программы вначале нужно оценить размеры критических модулей, их слож- ность и адекватность. После того как каждый уровень закодиро- ван, следует его промоделировать, не прибегая к помощи машины, чтобы посмотреть, корректен ли он и может ли в принципе рабо- тать. Очень важно до начала разработки нижних уровней полно- стью «протестировать» верхние уровни. Важность завершенности (и определенности) каждого верхнего уровня разработки часто не учитывается при использовании метода сверху вниз. По мере того как «скелет» программы просматривается и про- веряется на каждом более низком уровне, формируется фактиче- Davis Р. J., Fidelity in Mathematical Discourse: Is One and One Really Two?, American Mathematical Monthly (March 1972).
ПРОЕКТИРОВАНИЕ ПРОГРАММ 91 £кая программа. Когда написан каждый уровень, очень важно обеспечить, чтобы он работал, потому что ошибки на любом уров- не гибельны для следующих уровней. Кодирование сверху вниз дспользуется для того, чтобы гарантировать работу проекта с самого начала. Кодирование «скелета» помогает избежать огромной работы по переписыванию программы. Например, большинство значи- тельных по объему программ сильно модифицируют на этапе ко- дирования, в это время становятся очевидными некоторые труд- ности, ограничения и желаемые изменения. Из-за такой спеш- ной переработки, кодирования и тестирования выходная програм- ма обычно получается плохой. Выполняя же вначале кодирование «скелета» программы, можно предвидеть некоторые трудности до того, как будут затрачены большие усилия на программирова- ние. Кодирование «скелета» программы следует выполнять, в псевдокодах. 2.16.1. ПСЕВДОКОДЫ Псевдокоды пишут на языке, близком к естественному, что поз- воляет четко выразить логику программы. Псевдокоды очень ча- сто заменяют блок-схемы и подобны языкам программирования. Идея их использования состоит в том, чтобы четко представить логику программы, игнорируя машинные ограничения. Псевдо- коды нельзя компилировать и выполнять. Вы можете создать свои собственные псевдокоды или воспользоваться моими предложе- ниями. г) DO UNTIL д) CASE е) CALL операторов DO используйте END обозначения конца операторов IF — соответственно 2.16.2. СОГЛАШЕНИЯ ПО ПСЕВДОКОДАМ 1. Применяйте только структурные конструкции: а) последовательность б) IF THEN ELSE в) DO WHILE 2. Для обозначения конца DO, для ENDIF. 3. Вводите абзацы для обозначения блочной структуры. Если в какой-либо организации придерживаются установлен- ных соглашений по псевдокодам — любой . может их прочесть. Псевдокоды должны быть частью документации и могут исполь- зоваться вместо блок-схем или являться их дополнением. Напи- шите предлагаемую ниже задачу в псевдокодах. Задача. Составьте программу вывода на печать сдачи с дол- лара, если цена покупки меньше доллара. Примите во внимание Желание покупателя получить минимальное число монет.
92 ГЛАВА 2 Попытайтесь решить эту задачу сами, прежде чем посмотри- те мое решение. Чтобы не вводить вас в искушение, я помещаю свое решение на следующей странице, а здесь предлагаю вам еще одну задачу. После того как вы найдете решение, тщательно проверьте его, при этом убедитесь в том, дает ли оно нужные ре- зультаты. Итак, вторая задача: Задача. Считайте последовательность единиц, двоек и троек и подсчитайте их количество по отдельности. Прочитав число 9, на- печатайте результаты. Решение. . j Считать число. DO WHILE (не 9) IF 1 THEN прибавить 1 к содержимому счетчика единиц ELSE IF 2 THEN прибавить 1 к содержимому счетчика двоек ELSE IF 3 THEN прибавить 1 к содержимому счетчика троек ENDIF ENDIF Считать следующее число. ENDDO Вывести на печать результаты. Обратите внимание на то, что в примере используются только» структурные конструкции и как абзацы выделяют структуру. По- пытайтесь выполнить такую же задачу, используя операторы DO UNTIL, CASE или массивы. В дальнейшем псевдокоды должны быть переведены на язык программирования. Псевдокоды исполь- зуют для получения логического построения программы до начала фактического программирования. Псевдокоды имеют два преимущества: 1. Логика программы может быть записана полностью неза- висимо от языка программирования и машины. 2. Логика программы изображается таким образом, что ее мо- гут понять даже непрограммисты.. Таким образом, псевдокоды позволяют написать программу в простой структурной форме до того, как выяснятся все слож- ности машины или языка программирования. Прежде чем программировать, запишите программу в псевдокодах. Попытайтесь решить еще одну, более сложную задачу.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 9% Задача. Прочитайте какой-нибудь текст, поместите в таблицу все входящие в него слова и подсчитайте, сколько раз встречает- ся каждое слово. Напечатайте таблицу, указывая- каждое най- денное слово и число его появлений в тексте. DO WHILE (количество > 0) IF количество >=50 THEN вычесть 50 из количества Напечатать 50 ENDIF IF количество > =25 THEN вычесть 25 из количества Напечатать 25 ENDIF IF количество > = 10 THEN вычесть 10 из количества Напечатать 10 ELSE IF количество > =5 THEN вычесть 5 из количества Напечатать 5 ELSE Напечатать количество устанрвцть количество равным нулю» ENDIF ENDIF ENDDO Рис. 2.11. Решение задачи о получении сдачи с доллара. 2.16.3 . ФАКТИЧЕСКОЕ ПРОГРАММИРОВАНИЕ Фактическое программирование также следует выполнять* сверху вниз, причем вначале пишут операторы языка управления заданиями (ЯУЗ), за которыми следует главная (управляющая)- программа. На рис. 2.12 показана сущность подхода сверху вниз.- Обычно разработка «скелета» проекта может выполняться одним! человеком, что обеспечивает целостность разработки. Так как
54 ГЛАВА 2 применяется принцип модульности, главная программа должна быть короткой и вызывать модули и подпрограммы, которые мож- но моделировать, создавая подыгрывающие подпрограммы. По- дыгрывающая программа (stub) *> — очень короткая последователь- ность команд, которая используется как замена, пока не будет- создана фактическая программа. Подыгрывающие программы мо- а 'б в Рис. 2.12. Иллюстрация подхода сверху вниз. — запись операторов языка управления заданиями ЯУЗ и их выполнение; б — добавление операторов языка редактора связей (ЯРС) и их выполнение; в — добавление программного сегмента (написанного на ПЛ/1) с подыгрывающими подпрограммами, его выполнение и продолжение программирования. гут быть двух видов: фиктивные или замещающие модули. Фик- тивные модули (dummy module) не выполняют никакой работы, а только возвращают управление вызывающему модулю. Заме- щающий модуль (substitution module) выполняет простую обра- ботку до тех пор, пока не окажется возможным программировать более сложный модуль. Необходимо, чтобы вызывающий модуль не мог продолжать работу до тех пор, пока замещающий модуль не выдал результат. Примером могла бы быть ситуация, когда требуется подпрограмма усложненного ввода. Замещающий мо- дуль может выдавать какие-либо простые данные. Эти подыгры- вающие программы могут, например, читать фиктивные файлы, где хранятся тестовые данные. Использование обращений к фик- тивным подпрограммам позволяет производить компилирование и отладку на более ранней стадии программирования. Подыгрывающие программы используют и для того, чтобы можно было начать тестирование других сегментов программы. *> В отечественной литературе используется также термин «заглушка». — Прим, перев.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 95 Подыгрывающая программа может быть любой программой, обеспечивающей возвращение в вызывающий модуль для про- должения его выполнения. Хорошо, если подыгрывающая програм- ма настолько удовлетворяет любым требованиям интерфейса, что в последующем никаких изменений интерфейса не потребуется. Часто такие программы имеют только один или два оператора. Иногда они просто печатают текст: «ВЫПОЛНЕНА ПОДПРО- ГРАММА XY», показывая тестирующему, что подпрограмма как будто выполнена, и проверяя таким образом логику модуля более высокого уровня. В других случаях бывает необходимо вернуть некоторые значения вызывающему модулю с тем, чтобы можно было проверить самый высокий уровень программы, т. е. глав- ную программу. После того как она отлажена, таким же образом программируют и проверяют следующий по порядку более низ- кий уровень. Части программы более низкого уровня последова- тельно добавляют аналогичным образом. Очередной модуль про- граммируют только после того, как составлена программа и от- лажен модуль, его вызывающий. Этот процесс приводит к тому, что модули самого высокого уровня, обычно более критичные, проверяют наиболее тщательно. Когда проект разрабатывают сверху вниз, то для програм- мирования модулей самоГо низкого уровня можно использовать дополнительно лиц с невысокой квалификацией. Очень важно, чтобы опытные программисты выполняли первоначальную разра- ботку «скелета» программы сверху вниз й его фактическое про- граммирование. Кроме возможности для профессионального про- граммиста показать свое искусство такой подход позволяет менее опытным программистам непосредственно участвовать в работе и иногда даже реализовать свои идеи. Во всяком случае, програм- ма должна быть прочитана и проверена по крайней мере двумя лицами. В повседневной практике программирования разработка свер- ху вниз фактически означает, что система строится таким обра- зом, чтобы исключить или минимизировать написание любых про- грамм, тестирование которых зависит от еще не написанных про- грамм или от данных, которых пока нет. При создании больших проектов необходимо тщательное планирование, поскольку неко- торые программы должны быть завершены в определенной степе- ни, прежде чем может начаться разработка других. Система про- грамм тестируется по мере их создания, тем самым сокращается про- цесс тестирования, который обычно происходит в конце проекта. Программирование сверху вниз не означает, что создание про- грамм должно непременно происходить вниз по дереву, уровень за Уровнем. Некоторые ветви намеренно программируют раньше дру- гих. Ветви, которые подвержены изменениям, могут быть заверше- ны раньше, для того чтобы потенциальный пользователь мог оценить результат их работы.
S6 ГЛАВА 2 2.16.4 . ВЫВОДЫ Расширение задачи при проектировании сверху вниз всегда затруднительно. Примеры из учебников изображают этот процесс просто перечислением множества расширений сверху вниз. Да- же имея дело с этими простыми задачами, я совершенно запу- тался, пытаясь получить хорошее решение. А у меня были такие преимущества, как четко определенная задача и хороший алго- ритм, чего в реальных условиях обычно нет. Поэтому, если вы де- лаете расширение сверху вниз, нужно быть готовым к большим трудностям, преодолеть которые не просто. После выбора алго- ритма вторым основным условием получения успешной програм- мы является хороший проект. Плохой алгоритм или плохой проект нельзя спасти искусным программированием. Всегда стремятся побыстрее начать программирование, однако от таких порывов следует удерживаться, пока не завершено проектирование. Коди- рование— минимальная часть задачи. Большинство решений сле- дует принять до того, как начнется программирование. Как толь- ко вы приступили к программированию, трудно изменить направ- ление работы. Поэтому очень важно стать на правильный путь в самом начале. Чаще всего существует более одного способа ре- шения задачи. Описанная ситуация подобна путешествию по стране в автомо- биле. Если вы отправляетесь в поездку, не наметив маршрут, то первоначальное направление, в котором вы начали движение, мо- жет определить оставшуюся часть пути. Поэтому важно сплани- ровать заранее, куда и как вы отправитесь сначала. 2.17. БРИГАДА ГЛАВНОГО ПРОГРАММИСТА Оправдывает ли она себя? Сегодня многие программные про- екты используют методы, рассматриваемые в этой главе. Посту- пают сообщения о больших успехах, достигнутых в разработке больших, не содержащих ошибок программ. Кроме того, многие 'специалисты, освоившие методы структурного программирования, сообщают о качественном скачке производительности создания 'программ. При разработке двух успешных проектов были объединены все "методы структурного программирования и создана так называе- мая бригада главного программиста (БГП). Структура такой бри- гады впервые описана Дж. Эроном0. Первый проект, выполненный ио методу БГП, назывался Super-Programmer Project. Соединение методов структурного программирования с новой организацией талантов дало возможность создавать программы, насчитывающие до 50000 команд, в пять раз быстрее, чем обычно, и почти без ’*> Aron J. Software Engineering Techniques, Ed. by Buxton, Randell.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 97 ошибок. После такого успеха организация БГП была усовершен- ствована в проекте The New York Times. Этот проект был пред- назначен для создания информационного банка для газеты «Нью- Йорк тайме». Работа заняла 22 мес, на создание проекта было затрачено более 11 человёко-лет. Результатом явилось написание более 83 000 строк программы. Система обработки файлов успешно прошла недельное при- емное тестирование, и только по истечении 20 мес была обнару- жена первая ошибка. В первые 13 мес работы только одна про- граммная ошибка привела к сбою в системе. Программисты си- стемы выдавали в процессе ее создания около 10 000 строк ис- ходной программы и одну ошибку за один человеко-год.. Главным программистом обоих проектов был X. Миле. Если вы хотите бо- лее подробно изучить проект, обратитесь к работам [3, 4]. Эта ситуация противоположна той, которая имела место, ког- да проектировалась операционная система OS 360. Система OS 360 разрабатывалась на год дольше намеченного срока, за вре- мя создания в ней было обнаружено 1000 ошибок даже после того, как был создан 21 вариант этой системы. Поскольку в OS 360 бы- ло так много неполадок и при разработке других больших проек- тов были подобные затруднения, некоторые группы, создававшие эти проекты, начали анализировать и изучать, что же в действи- тельности делали программисты. Поразительный факт, обнару- женный при анализе нескольких проектов, состоял в том, что в этих больших программных проектах средняя скорость получения отлаженных программ составляла 5—10 команд за человеко-день. Очевидно, это не означает, что программист за весь день закоди- ровал только 5—10 команд. Ведь тогда возникает естественный вопрос: что же он делал оставшуюся часть дня? (Читателям, ин- тересующимся этой проблемой, следует обратиться к книге Брук- са, ссылка на которую дана на стр. 62). К сожалению, большая часть времени в проектах значительного объема тратится на свя- зи, переписи программ, тестирование и отладку. И задача состо- ит в том, чтобы сразу получить правильную программу, так как отладка, очевидно, слишком дорого стоит. Ядро бригады программистов составляют главный програм- мист, его помощник и библиотекарь программ. Главный програм- мист решает, когда и сколько дополнительных программистов мо- жет понадобиться. Если в брагаду нужно включить много про- граммистов, возможно, следует предусмотреть должность служа- щего, который займется административными, финансовыми и пра- вовыми вопросами, чтобы главный программист мог сосредото- читься только на техническом руководстве. Очень важно, чтобы основная задача главного программиста состояла именно в тех- нической разработке программ. 7—899
98 ГЛАВА 2 2.17.1. ОБЯЗАННОСТИ ЧЛЕНОВ БРИГАДЫ Основная обязанность главного программиста заключается в разработке и составлении программ, и все члены бригады долж- ны сообщать о ходе своей работы непосредственно ему. Главный программист разрабатывает и программирует основные, критиче- ские сегменты системы программ и поручает работу над другими сегментами членам бригады. Как только разработка этих сегмен- тов закончена и они включены в систему, главный программист проверяет сегменты, чтобы убедиться в том, что они работают пра- вильно и соответствуют стандартам оформления и структурного программирования. Главный программист принимает все окон- чательные решения и отвечает за успех проекта. Помощник главного программиста не уступает по мастерству руководителю и помогает ему в разработке программ. Необходи- мо участие помощника на каждом уровне работы, так как он дол- жен быть в состоянии осуществить руководство проектом, если это потребуется. В обязанности помощника главного програм- миста может также входить разработка стратегии и тактики, ко- торых следует придерживаться при создании программ проекта. Это позволяет главному программисту и его помощнику обсуждать основную структуру и проектирование задачи. В то время как главный программист программирует основные критические сег- менты, его помощник может готовить данные для тестирования. Главный программист и его помощник проверяют программы друг друга, а также программы, составленные другими членами брига- ды. Последние в свою очередь читают и разбирают программы, написанные главным программистом, чтобы составить подыгрыва- ющие программы. При такой организации работы по крайней мере два программиста полностью разберут каждую строку раз- рабатываемой программы. Библиотекарь программ хранит все записи проекта в библио- теке поддержки разработки (которая описана ниже). В библиоте- ке хранятся записи, показывающие текущее состояние программ и тестирования. Если в систему добавлены новые или модифици- рованные модули, библиотекарь поместит новые листинги в под- шивку программ и отметит новое текущее состояние системы. Библиотекарь отвечает за хранение всех записей, прогоны про- грамм с тестовыми данными и замену старых подыгрывающих модулей или сегментов новыми. Хотя его значение заключается в том, чтобы другие программисты меньше занимались канцеляр- ской работой, библиотекаря нельзя считать вспомогательным зве- ном бригады — он ее полноправный член. Трудно определить положение библиотекаря. Мастерство тре- буется не только программистам. Нужен кто-то, умеющий взаи- модействовать с машиной, инициируя действия системы и обнов- ляя файлы данных. Кроме того, библиотекарь должен справлять-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 99 ся с такими секретарскими обязанностями, как типовая докумен- тация. Поэтому часто на эту должность назначают человека, вла- деющего навыками секретарской работы, а затем его обучают не- обходимой работе на машине. Назначение библиотекаря — выпол- нение большей части канцелярской работы, входящей в обязанно- сти программистов, что позволяет более эффективно использовать последних. Эта несложная помощь значительно повышает произ- водительность труда бригады. 2.17.2. ДРУГИЕ ЧЛЕНЫ БРИГАДЫ В зависимости от размера и типа создаваемой системы по ме- ре развития разработки в бригаду программистов включают до- полнительных сотрудников. Так как главный программист яв- ляется основным разработчиком системы, он начинает выполнять свои обязанности раньше всех. Несколько позже приступают к работе два других основных сотрудника — помощник главного программиста и библиотекарь. Однако, если проект большой и необходимы специальные знания по применению аппаратных средств или программного обеспечения, в бригаду вводят допол- нительных сотрудников. Как правило, в бригаде не должно быть более семи человек. Большие программы являются очень сложными структурами. С созданием БГП опытных специалистов используют таким обра- зом, чтобы они принесли максимальную пользу. Использование опытных людей в детальном кодировании программ необходимо при работе с современной операционной системой. Язык управле- ния заданиями, методы доступа к данным, средства системного программирования и языки высокого уровня настолько сложны, что необходимо, насколько это возможно, использовать квалифи- цированйых специалистов при кодировании критических участков проекта. Современное программное обеспечение так сложно и об- ширно, что для его успешного применения требуются его тщатель- ное изучение и некоторый опыт. Бригадный подход дает много преимуществ. Во-первых, менее квалифицированные программисты получают возможность участ- вовать в больших проектах и наблюдать разработку всего проек- та. Сотрудничество с квалифицированными специалистами — тре- мя основными членами бригады — позволяет молодым сотрудни- кам овладеть профессиональными и техническими навыками. Все выполненные работы проверяются опытными программистами. Кроме того, так как молодые члены бригады будут читать глав- ную программу, она будет тщательно проверена. Таким образом, программирование переходит от индивидуального умения к кол- лективному сотрудничеству, когда каждый анализирует успехи и ошибки как свои собственные, так и других. Среда, в которую попадают молодые сотрудники, способствует овладению профес- сиональным мастерством и дальнейшему продвижению.
100 ГЛАВА 2 Дж. Вейнберг1) высказывает мысль, что коллективное про- граммирование приведет к разработке лучших программ. Кол- лективное программирование означает, что все программы явля- ются общим достоянием и проверяются другими членами бригады. Бригадный метод работы обеспечивает высокую степень контро- ля. Библиотекарь отвечает за получение всех машинных распеча- ток, хороших и плохих, и за подшивку их в библиотеку поддержки системы, где они становятся частью общего протокола. В отличие от такого метода работы при небригадном программировании пло- хо составленные программы выбрасывают в мусорную корзину. Это владение всеми программными данными и результатами ра- боты на машине как общественным достоянием, а не личной соб- ственностью отдельных программистов приводит к принципу обез- личивания программирования. 2.17.3. НЕКОТОРЫЕ РЕКОМЕНДАЦИИ ПО БРИГАДНОМУ ПРОГРАММИРОВАНИЮ Очень важно, чтобы главный программист располагал време- нем и соответствующими полномочиями для осуществления тех- нического руководства проектом. Главные обязанности главного программиста заключаются в следующем: 1) в техническом руководстве — наблюдении за техническим аспектом разработки проекта; 2) в руководстве персоналом — контроле за отчетностью лю- дей и 3) в соблюдении условий контракта — регулировании отноше- ний с заказчиками. Наиболее важной для успешной разработки проекта является исполнение первой обязанности. Если количество лиц, участвую- щих в создании системы, больше семи, вопросы взаимоотношения людей отнимают у главного программиста много времени. Этой ситуации можно избежать, либо ограничив число сотрудников, ли- бо введя должность помощника по административно-финансовым вопросам. Если число людей более семи, можно, кроме того, сфор- мировать две бригады. Аналогично этому, если отношения с за- казчиком отнимают слишком много времени, следует ввести долж- ность управляющего. Иногда помощник главного программиста становится вторым лидером. Такой ситуации следует избегать — лидер должен быть один. Если помощник так же опытен, как главный программист, то в следующем проекте можно поменять их ролями. Очень важ- но, чтобы главный программист имел доверительные отношения со своим помощником, чтобы все проблемы можно было обсуж- дать в атмосфере взаимного доброжелательства. Существенно *> Weinberg G., Psychology of Computer Programming.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 1,01 также, чтобы у главного программиста была возможность отка- заться от помощника, если сложившиеся у них отношения тормо- зят развитие проекта. 2.17.4. ВОЗМОЖНЫЕ ТРУДНОСТИ Не вполне ясно, можно ли применять организацию БГП при создании очень больших проектов. По-видимому, БГП имеет смысл использовать при разработке проектов средних размеров, когда один человек в состоянии охватить весь проект. Но если мы рассматриваем проект создания системы, подобной OS 360, стоившей 50 млн. долл, и занявшей 5000 человеко-лет, становится ясно, что один программист не может контролировать разработку всего проекта. В этом случае можно использовать уровни БГП, но если количество последних велико и если они должны взаимо- действовать друг с другом, эффективность работы может пони- зиться. Бригады программистов работали успешно над проектами, которые были более чем на два порядка меньше по величине, чем OS 360. Методы, хорошо себя проявившие в проектах одного раз- мера, не гарантируют получения хороших результатов при разра- ботке проектов, в сто раз больших по объему. Эта трудная задача для создателей программного обеспечения, но такая же проблема стоит и во многих других областях. С другой стороны, старые ме- тоды организации больших программных проектов приводили к таким плохим результатам, что сомнительно, чтобы метод бригад- ного программирования не дал какого-либо улучшения. Неясно также, насколько хорошо будут работать БГП при ме- нее благоприятных условиях, чем в этих двух часто цитируемых успешных примерах. Эти два примера не типичны по некоторым причинам. Во-первых, руководитель программистов исключитель- но квалифицированный специалист. Д-р X. Миле, сотрудник фир- мы IBM, имеет большой опыт работы на машинах. Производитель- ность труда программистов может отличаться в 25 раз, поэтому мастерство каждого программиста может стать решающим фак- тором в успешной разработке проектов. Во-вторых, БГП поддер- живалась колоссальными ресурсами IBM. Другие БГП не будут иметь таких преимуществ. В-третьих, организация БГП может не представлять интереса для некоторых лиц или организаций. Итак, главному программисту необходимо уметь управлять бригадой, рационально расходовать свое время и обеспечивать техническое представительство перед заказчиком. Он должен ве- сти проект, а также критически оценивать работу других сотруд- ников бригады. Специалистов, обладающих таким мастерством, наити нелегко. Однако полученные результаты весьма впечатля- ющи и вызывают большой интерес.
102 ГЛАВА 2 2.18. БИБЛИОТЕКА ПОДДЕРЖКИ РАЗРАБОТКИ Библиотека поддержки разработки (БПР) служит для хране- ния и использования всех (читаемых машиной и людьми) струк- турированных программ, тестированных и объединенных в систему. Эта система поддерживается библиотекарем и используется для хранения документов таким образом, чтобы они были доступны всем членам проекта. БПР предназначена 1) для организации хра- нения текущего состояния проекта и доступа к нему в любое время (каждый интересующийся проектом может выяснить его состоя- ние или изучать проект самостоятельно); 2) для освобождения остальных работников проекта от канцелярской работы. Такая организация повышает производительность программирования. Все читаемые машиной данные, входящие в систему, следует хранить в машинной библиотеке. Включению в библиотеку подле- жат программа на входном языке, текст на языке управления за- даниями и тестовые данные. Библиотекарь отвечает за то, что рабочая копия сделана и обкатана. Эта внутренняя (читаемая машиной) библиотека содержит либо только один текущий на- бор данных каждого типа, либо в ней могут находиться различ- ные версии работающей системы. Внешняя (читаемая человеком) библиотека отражает содер- жимое внутренней библиотеки. Для каждого типа данных, храни- мых во внутренней библиотеке, должна быть подшивка текущего состояния. Таким образом, должны быть свои подшивки для ис- ходной программы, для языка управления заданиями и для тес- товых данных. Должны быть также подшивки тестовых прогонов, которые отражают состояние проекта и доступны всем, принима- ющим участие в проекте. Каждая новая выдача подлежит хране- нию в архивном файле и может использоваться для восстановле- ния при нарушениях и для просмотра, если в этом возникает не- обходимость. Так как главный программист непосредственно уча- ствует в проекте и состояние проекта ему понятно, администрация получает возможность знакомиться с состоянием проекта быстрее и эффективнее, чем в случае, когда бригадный метод не исполь- зуется. Любые добавления и изменения вносятся непосредственно в подшивки либо записываются на бланках. Затем библиотекарь переносит эти изменения во внутреннюю библиотеку и выполняет прогон задания. Таким образом, библиотекарь значительно со- кращает объем канцелярской работы и потери времени програм- мистов (рис. 2.13). Лучший критерий определения правильного использования БПР заключается в степени использования БПР во время раз- работки проекта. Программисты должны применять программы на входном языке, находящиеся в БПР, при создании системы. Главный программист при помощи программ в БПР может еле-
ПРОЕКТИРОВАНИЕ ПРОГРАММ 103 дить за ходом работ. Пользователи также имеют.такую возмож- ность, кроме того, БПР позволяет работать независимо друг от друга. Если не все программисты выполняют работу на машине, пользуясь библиотекой, то целостность БПР нарушается и идея. Рис. 2.13. Процедура работы с библиотекой поддержки разработки. обесценивается. Реальное значение БПР состоит в том, что в ней находятся все данные. БПР свойственны следующие характерные черты: 1. Хранит как внутреннюю, так и внешнюю машинную доку- ментацию. 2. Используется на протяжении всего проекта — от стадии про- ектирования до заключительной стадии тестирования и составле- ния отчета по проекту. 3. Подшивка внешней библиотеки с прогонами программ со- храняет как предысторию, так и текущее состояние проекта.
104 ГЛАВА 2 4. GaMH программы должны быть лучшей справкой для вопро- сов о форматах данных, работе программы, алгоритмах и т. д. 5. Желательно, чтобы только библиотекарь выполнял все про- гоны и регистрировал их. Это позволит сохранять целостность и обозримость проекта. Наличие архива полезно по нескольким причинам. Становится возможным прослеживать эволюцию программы или просматри- вать ее более ранние версии, причем программы на выходном языке или результаты прогонов менее ценны, чем программы на входном языке или тестовые данные. Одно из преимуществ БПР заключается в том, что с ее помощью можно легко восстановить любой файл в случае сбоя. 2.19. ИСПОЛЬЗОВАНИЕ ПРОГРАММ Программисты часто забывают о том, как долго работают не- которые программы. Имеются работающие программы, которые написаны в начале 60-х годов. Некоторые из них созданы для машины IBM 1401 и теперь используются на современных маши- нах при помощи эмулятора машины 1401. Изданный в октябре 1972 г. выпуск EDP Analyzer посвящен эксплуатации программ. Авторы выпуска пришли к выводу, что ор- ганизации по обработке данных обычно тратят 50% времени прог- раммирования на поддержку программ и что программа средней производительности используется по крайней мере десятью разны- ми лицами, прежде чем ее перепишут или выбросят. Первоначаль- ная стоимость программы обычно составляет лишь небольшую часть стоимости программирования. Э. Тофлер в книге Future Shock описывает затруднения, испы- тываемые людьми и организациями, когда изменения происходят слишком быстро. То же самое свойственно и программированию. Изменения возникают раньше, чем закончена разработка програм- мы. Может измениться состав администрации, могут измениться требования, может измениться том на дисках. Кроме того, меняет- ся состав программистов и вполне возможна ситуация, когда про- граммист, создавший программу, больше не работает. Система трансформируется даже в том случае, когда не нужно делать до- бавления или улучшения в работающей программе. Изменяются машина и периферийные устройства. Если модернизируется опера- ционная система, то часто возникает необходимость в обновлении первоначальной программы в соответствии ,с новыми условиями работы. Программы, которые можно использовать в разных условиях эксплуатации (в разных операционных системах), называют мо- бильными. Это важно, когда вы намерены снабдить программным обеспечением сразу многих пользователей или хотите, чтобы ваши программы могли работать в новых условиях. Самый лучший
ПРОЕКТИРОВАНИЕ ПРОГРАММ 105 метод, используемый для поддержки мобильной программы, состо- ит в применении ANSI COBOL и ANSI FORTRAN1). Аналогичным понятием является приспособляемость. Приспособляемость — это легкость, с которой можно изменять программу, чтобы приспосо- бить ее к требованиям различных пользователей и к ограничениям системы. Главное отличие между этими двумя понятиями состоит в том, что мобильность связана с изменениями условий работы, в то время как под приспособляемостью подразумевают изменения в структуре алгоритма. Эти понятия подробно обсуждаются в ста- тье Пула и Вейта2). Если предполагается использовать программное обеспечение в течение большого периода времени, его следует создавать таким образом, чтобы в него можно было постоянно вносить изменения, сообразуясь с новыми условиями эксплуатации. В операционной системе OS 360 фирмы IBM на поддержку программного обеспе- чения было израсходовано примерно в четыре раза больше, чем на разработку. Поэтому в этом случае, как и во многих других, спо- собность поддерживать программное обеспечение системы более важна, чем первоначальные затраты. Хорошо известно, что программисту трудно добиться успеха, если он модифицирует работающую программу. Боэм [6] устано- вил, что если изменяется менее 10 операторов, то шанс написать работающую программу с первой попытки составляет 50%. Но если изменению подлежит до 50 операторов, то вероятность ус- пешного решения снижается до 20%. Трудность внесения измене- ний или корректировки программы во время ее использования со- стоит в том, что изменение в программе может быть правильным локально, но по отношению к системе неверным. Программа, труд- ная для модификации, называется недолговечной (fragile). Так как расходы на эксплуатацию часто превышают расходы на первоначальное программирование, следует сразу создавать хо- рошо эксплуатируемые программы. Расходы на эксплуатацию обычно являются неявными расходами. Чтобы сделать эти рас- ходы более очевидными, вычислительные центры используют ме- тод, заключающийся в прибавлении стоимости эксплуатации к стоимости ^каждой полученной программы. То есть если на созда- ние программы требуется 3 человеко-года, то на эксплуатацию программы следует предусмотреть по одному или по половине че- ловеко-года на время работы программы. Иногда приходят к выводу, что для сокращения расходов на эксплуатацию нужно разрешить программистам полностью перепи- сать некоторые подпрограммы. Было установлено, что обычно не- сколько подпрограмм или программ используют чаще других. Ес- ’’ Стандартный КОБОЛ и стандартный ФОРТРАН.—Прим, перев. •• Advanced Course in Software Engineering, Ed. by M. Beckman et al., Sprin- ger-Verlag, New York, 1973,
106 ГЛАВА 2 ли такие подпрограммы можно обнаружить и переписать, это мо- жет привести к снижению расходов на эксплуатацию. Один из способов внушить программистам важность создания хорошо эксплуатируемых программ состоит в том, чтобы каждый программист занялся эксплуатацией программ в течение некоторо- го периода времени. Ни один программист не должен отлынивать от этой работы. Действительно, всех новых программистов следует назначать на эксплуатацию программ на первые шесть месяцев их работы, чтобы они оценили значение хорошо документирован- ной программы. Чтобы запланировать изменения, которые могут возникнуть при эксплуатации программы, их следует определить еще на ста- дии ее разработки. Если в процессе разработки помнить о них, то программу можно сразу написать так, что эти изменения не будут для нее слишком разрушительными по своим последствиям. Планируйте возможные изменения в программе. 2.2( 5. ДОКУМЕНТИРОВАНИЕ Документирование не должно начинаться тогда, когда разра- ботка программы закончена. Оно должно выполняться одновре- менно с разработкой программы, начиная с этапа постановки за- дачи. Независимо от того, насколько хороша программа, она будет работать только в том случае, если для ее использования имеются надлежащие инструкции. Даже если программа предназначена для индивидуального пользования, следует написать инструкцию, что- бы не забыть некоторых деталей, касающихся ее работы. Если же программа предназначена для непрограммистов, необходимы хо- рошо составленные подробные инструкции. Начинайте документирование на стадии разработки программы. Программы не являются физическими объектами, которые мож- но понять, дотрагиваясь до них руками. Более того, если програм- ма находится внутри машины, ее нельзя даже увидеть. Для боль- ших программ бесполезно пытаться использовать листинг в качест- ве документации. Листинг программы либо слишком длинный, чтобы его можно было прочитать, либо требуется приложить чрезмерные усилия даже в случае коротких программ, чтобы пред- ставить, что же делает программа. Плохо документированная программа должна быть исследова- на группой специалистов, состоящей из врача, детектива и архео- лога. Детектив соединяет неполную документацию, врач ставит диагноз, а археолог изучает исторические документы в виде ста- рых распечаток. Так как для понимания плохо документированных
ПРОЕКТИРОВАНИЕ ПРОГРАММ 107 программ необходимо владеть перечисленными специальностями, нет ничего удивительного в том, что программу обычно выбрасы- вают и переписывают. 2.21. ПЕРЕПИСЫВАНИЕ ПРОГРАММ Программисты не должны бояться переписывать программу. Переписывание программы — творческая задача, и, как и при соз- дании других произведений, при этом часто требуется более чем один черновик. При написании программы нередко становится очевидным, что первая попытка была неверной. Поэтому перепи- сывание помогает создать эффективную и легко отлаживаемую программу. Не бойтесь начинать программирование сначала. При работе над большими программами целесообразно напи- сать короткую программу, чтобы промоделировать основные час- ти большой программы. Для большой программы могут понадо- биться обширный ввод-вывод и значительное время на програм- мирование или тестирование. Модель может не содержать ввода и иметь упрощенный вывод. Эта небольшая программа требует по крайней мере на два порядка меньше работы, чем реально дейст- вующая программа. Даже если получено только приближение окончательного решения, модель должна оказать неоценимую по- мощь в проверке задачи, подлежащей программированию. Можно проверить алгоритмы и проект. Модель можно закодировать в псевдокодах или можно написать ее программу в интерактивной системе на каком-нибудь языке, подобном БЕЙСИКУ или АПЛ, и затем использовать для проверки проекта и алгоритма. Один из способов проверки, следует ли переписывать програм- му, заключается в применении контролируемой (buddy) системы, т. е. каждая программа должна быть понятна и ее автору, и по крайней мере еще одному программисту. Если программирование выполняет бригада, то не менее двух человек знакомятся с про- граммой. Однако при разработке небольших проектов, выполняе- мых одним человеком, нет никого, кто проверил бы программу. Такая контролируемая система выгодна руководству. Если один из программистов уходит, остается другой программист, который знаком с программой. 2.22. СОВЕТЫ ПРОГРАММИСТУ Стремитесь к простоте. Добивайтесь точности при определении задачи. Выбирайте алгоритм задачи самым тщательным образом.
108 ГЛАВА 2 Выбирайте представление данных, соответствующее задаче. Используйте в качестве параметров переменные, а не константы. Создавайте универсальные программы. Не перепрограммируйте функцию квадратного корня. Устанавливайте цели проекта заблаговременно и точно. Сначала напишите программу на естественном языке. Разрабатывайте тестовые данные заранее. Прежде чем начать программировать, разработайте проект. Исключайте ошибки с самого начала. Короткие модули предпочтительнее длинных. Стремитесь к минимальному использованию операторов GO ТО. Прежде чем программировать, запишите программу в псевдокодах. Планируйте возможные изменения в программе. Начинайте документирование на стадии разработки программы. Не бойтесь начинать программирование сначала. 2.23. УПРАЖНЕНИЯ ПОВТОРЕНИЕ ПРОЙДЕННОГО 1. Определите следующие понятия: а) KISS-принцип б) универсальность в) сложность г) независимость д) клудж е) правильная программа ж) проектирование сверху вниз з) разбиение на модули и) структурное программирование к) БГП л) подыгрывающая программа м) псевдокод н) БПР о) мобильная программа п) приспособляемость р) недолговечность с) побочный эффект т) перестройка 2. Каковы обязанности а) главного программиста, б) помощ- ника главного программиста и в) библиотекаря? 3. Какие свойства вашего языка программирования затрудня- ют/облегчают структурное программирование? 4. Почему труднее программировать большие программы? 5. Приведите примеры простого и сложного программирования. 6. Как вы полагаете, будете ли вы программировать по-друго- му в случае использования контролируемой системы? 7. Перечислите стандартные приемы для вашего языка про- граммирования, которыми следует воспользоваться.
ПРОЕКТИРОВАНИЕ ПРОГРАММ 109 8. В чем заключаются некоторые преимущества модульного программирования? Каковы его недостатки? 9. Приведите несколько примеров универсальных и неунивер- сальных приемов программирования. 10. Дайте определение структурного программирования, пере- числите все известные вам особенности структурного программи- рования. Просмотрите другие работы и попытайтесь дать свое оп- ределение структурного программирования. 11. Найдите книгу, содержащую вычислительные алгоритмы или программы, представляющие интерес для вашей работы. 12. Многие выступают против использования операторов EQUIVALENCE в ФОРТРАНе, REDEFINES в КОБОЛе или DEFINED в ПЛ/1. Эти операторы позволяют присвоить несколько имен одной ячейке памяти. В каких случаях вы считаете оправ- данным их применение? Ошибки' какого типа могут возникнуть при модификации программы вследствие использования этих опе- раторов? Приведите аргументы в пользу и против применения ука- занных операторов. 13. Большинство языков программирования разрешает приме- нять глобальные переменные при помощи операторов COMMON в ФОРТРАНе, LINKAGE SECTION в КОБОЛе и EXTERNAL в ПЛ/1. Возникновению каких ошибок способствует использование глобальных переменных? Когда их следует использовать и когда нет? Приведите аргументы в подтверждение обеих точек зрения. 14. Найдите в вашем языке программирования оператор, ис- пользование которого вы считаете опасным и который желательно исключить из языка. Почему вы хотели бы это сделать? В каких случаях используется этот оператор и как вы могли бы его за- менить? Приведите соответствующие аргументы. 15. Многие задачи не следует решать на вычислительной ма- шине, так как получается незначительный выигрыш от ее приме- нения. Проклассифицируйте перечисленные задачи с точки зрения целесообразности решения их на машине, давая один из трех от- ветов: «Да», «Нет» и «Сомнительно». а) Платежная ведомость 250 человек. б) Платежная ведомость 12 человек. в) Платежная ведомость 3 человек. г) Программа грамматического разбора предложений на английском языке. । д) Просуммируйте 100 первых целых чисел арифметической прогрессии. е) Просуммируйте числа 2, 4, 8, 16, 32, ..., 2*»20 геометри- . ческой прогрессии. ж) Программа нахождения трех последовательных пятниц, которые попадают на 13-е число. з) Возьмите шахматную доску и удалите противоположные углы. Напишите программу размещения шашек на шах-
по ГЛАВА 2 матной доске таким образом, чтобы каждая шашка зани- мала точно две клетки и все клетки были заняты. и) Напишите программу определения максимального коли- чества коней, которые можно расположить на шахматной доске так, чтобы ни один конь не мог угрожать другому. к) Составьте таблицу из 1000 случайных чисел. Восполь- зуйтесь главой «Что такое машинная задача?» из книги Ф. Грюенбергера0. ПРОГРАММЫ 16. Целое число можно представить как сумму его частей, на- зываемых разбиениями. Так, число 4 можно представить как 4; 34-1; 2-|-1 + 1’> 2-|-2; 14-1 + 14-1. Р(п)'— количество разбиений, Р(4)=5. Напишите программу, которая читает число п и печатает разбиенця числа п и количество Р(п). 17. Напишите программу, которая читает некоторый текст и печатает частоту появления в нем различных слов. 18. Напишите программу, которая печатает текст какой-нибудь песни. 19. Получите первые 1000 простых чисел, используя алгоритм, приведенный на рис. 2.1. Затем улучшите программу, как пред- ложено в разд. 2.4, и определите различие во времени выполнения. 20. Решето Эратосфена. Составьте программу задачи получе- ния простых чисел, используя следующий метод. Создайте после- довательность положительных целых чисел от 1 до N. Начните с числа 2 и вычеркните все числа, кратные 2. Следующее за 2 не вычеркнутое число является очередным простым числом, т. е. 3. Теперь начните с 3. Вычеркните все числа, кратные 3. Следующее не вычеркнутое число 5 является очередным простым числом. Вы- черкните все числа, кратные 5, и т. д. Все оставшиеся числа и бу- дут простыми. 21. Составьте детальный план нижеследующей программы и затем используйте его для написания самой программы. Програм- ма предназначена для чтения целых чисел и выбора второго по величине значения. Последнее число является нулем. Затем обоб- щите задачу, то есть выберите 2V-e по величине значение, где N считывается каждый раз. 22. Банковский вклад. Разработайте план решения следующей задачи и затем напишите программу. Ежемесячно начисляется 6% вклада. Если вклады сделаны до десятого числа, то начисляются проценты за весь месяц. Для того чтобы вкладчики не изымали часто вклады, приняты следующие меры: с вкладчика с мини- мальным балансом менее 1000 долл., делающего более пяти изъ- ятий в месяц, удерживают 50 центов с каждого изъятия после ’> Gruenberger F., Computing: A Second Course, Canfield Press, San Francisco, 1971.
ПРОЕКТИРОВАНИЕ ПРОГРАММ Ш пяти изъятий. Процент начисляется ежемесячно по минимальному месячному балансу. 23. Создайте нижеследующие подпрограммы, работающие со строкой символов: а) Напишите подпрограмму, которая читает целые числа как строку символов, затем преобразует их в целые чис- ла. Сделайте то же самое для вещественных чисел. б) Напишите подпрограмму, которая читает целое число и выдает ее как строку символов с запятыми, отделяющи- ми группы по три цифры, начиная справа, например 1567842-* 1,567,842 Напишите программу обратного действия. Составьте ана- логичные программы для вещественных чисел. в) Напишите подпрограмму, которая читает вещественные числа, а выдает строку символов со знаком доллара и тремя звездочками перед числом. ПРОЕКТЫ 24.. Коммивояжер. Напишите детальный план решения следу- ющей задачи. Коммивояжер обслуживает 16 городов. Так как он сам покупает бензин, то предпочитает ездить самым коротким пу- тем. Каждую пятницу он получает список городов, которые дол- жен посетить на следующей неделе. Он может объезжать их в лю- бом порядке и не всегда посещать все города. 25. Лабиринт Минотавра. Напишите детальный план решения следующей задачи. План должен быть понятен любому, кто захо- чет использовать его для написания программы. Определите все модули и интерфейсы. Введите массив 21X21, состоящий из нулей (не путь) и единиц (путь). Напишите программу, которая читает вышеуказанный массив и находит выход из лабиринта. Минотавр начинает двигаться из центра массива. Если он находится в неко- тором квадрате, то может переместиться в любой из четырех смеж- ных квадратов, если последний содержит единицу. Нуль блокирует путь. После того как вы нашли путь из массива, напечатайте мас- сив, отмечая путь крестиком. Покажите только правильный путь, т. е. не печатайте возврат. 26. Напишите программу печати точной копии самой себя. Опе- раторы ввода не допускаются. 27. Напишите программу печати 100-символьной последователь- ности цифр 0, 1 и 2, в которой нет двух смежных идентичных под- последовательностей. Решение этой задачи есть в книге [8]. 28. Гипотеза Симона о факториале. Только четыре факториала могут быть определены как произведение трех последовательных Целых чисел. Вот два из них: 41=2*3*4=24 51=4*5*6=120
112 ГЛАВА 2 Можете вы найти еще два факториала? Не смогли бы вы найти ко- личество таких факториалов и доказать неправильность предпо- ложения, что их только четыре? 29. Напишите программу нахождения всех способов разреза- ния шахматной доски с числом клеток на Две равные части (не считая вращений и отражений). Если п нечетно, то центр на- ходится слева. (Указание', попытайтесь вначале составить про- грамму для доски 4X4. Затем добавьте ограничение, что две раз- резанные части должны быть одинаковой формы. При использова- нии этого ограничения существует 15 различных путей разрезания доски 5X5.) 30. Выясните, сколько лет используются наиболее старые прог- раммы на вашей машине. Можете ли вы определить, сколько че- ловек работало с этими программами? Располагаете ли вы в на- стоящее время программами, которые выполняются на современ- ных машинах при помощи эмуляции? Как модифицировать эти программы? 31. Декабрьский выпуск журнала ACM Computing Surveys за 1974 г. посвящен структурному программированию. Просмотрите его. 32. Сложность программ трудно оценить. Один метод измере- ния сложности заключается в подсчете количества непоследова- тельных частей в программе. Представляется ли вам это хорошим критерием? Оцените некоторые программы, используя это прави- ло. Сложность программ обсуждается в работе [2]. Этот вопрос также излагается в книге Милса по структурному программирова- нию. Ознакомьтесь с этими книгами и подумайте, можете ли вы разработать некоторые принципы для измерения сложности ма- шинных программ. 33. Просмотрите несколько известных статей, касающихся опе- ратора GO ТО. Начните со статьи Д. Кнута0. 34. Создайте предварительный проект программы начисления зарплаты. Посмотрите реальные ведомости расчета зарплаты и определите, какие производятся удержания. Затем перечислите все изменения, которые нужно внести в программу начисления зар- платы. Если это задание выполняли другие, соберите все сведе- ния об изменениях и переделайте программу начисления зарплаты таким образом, чтобы она могла легко приспособиться к внесению новых изменений. Это упражнение можно также проверить на многих других программах. 35. Теорема о структурировании утверждает, что любая пра- вильная программа эквивалентна программе, которая содержит в качестве логических структур только ’> Knuth D. Е., Structured Programming with GOTO Statements, ACM Com* puting Surveys (December 1974).
ПРОЕКТИРОВАНИЕ ПРОГРАММ ИЗ а) последовательность двух или более операторов, б) условный переход к одному из двух операторов и воз- врат (IF THEN ELSE); в) повторение операторов, пока условие истинно (DO> WHILE). Приведите несколько примеров для того, чтобы убедиться в спра- ведливости этой теоремы. Насколько целесообразно было бы при- вести все программы к вышеуказанной форме? Просмотрите соот- ветствующую литературу0. 36. Сделайте колоду перфокарт или перфоленту, содержащую программу, которая выполняется одинаково независимо от того, с какого конца она загружена в машину. В более сложном вари- анте программа перфорирует свою копию. 37. Попытайтесь разработать некоторые правила разделения программ на модули2). 38. Каждая система программирования, поставляемая фирмой, содержит обычно стандартные языки (ANSI COBOL или ANSI FORTRAN) и некоторые их расширения. Эти расширения отража- ют торговую политику фирмы, заключающуюся в том, что вы не можете использовать другой компилятор и ваши программы пере- стают быть мобильными. Если вы хотите этого избежать и иметь, мобильные программы, вы должны по возможности не пользовать- ся расширениями языка, что часто бывает трудно, так как расши- рения предоставляют хорошие возможности. Определите расшире- ния и стандартные конструкции, имеющиеся в одном из ваших ком- пиляторов. Затем разработайте программу, которая читает исход- ную программу и печатает ее, отмечая все нестандартные свойства языка. 39. Напишите статью о будущем программирования (но не ма- шин). Областями вашего исследования являются будущее исполь- зования языков КОБОЛ, ФОРТРАН, ПЛ/1 и БЕЙСИК, роль структурного программирования, доказательства правильности программ, автоматическое программирование и т. д. Некоторые сведения вы можете почерпнуть из работы Э. Дейкстры3>. 40. Напишите статью об истории программирования. Для изу- чения этой темы хорошо ознакомиться с работой Дж. Сэммета4>. ° Например, Bohm С., Jacopinf G., Flow Diagrams, Turing Machines, and* Languages with Only Two Formation Rules, Communications of the ACM (May 1966) и Mills H. D., Mathematical Foundations for Structured Programming, IBM Report No. FSC 72-6012, 1972. 2> По этому вопросу см. Pamas D. L., Communications of the ACM (May 1972) (December 1972). s> Dijkstra E. W., The Humble Programmer, Communications of the ACM (Oc- tober 1972). 4) Sammet J. E., Programming Languages, Prentice-Hall, Englewood Cliffs,. 1УОУ. 8—899
ГЛАВА 2 '--——--------------'----------------------------------------------------- ' ЛИТЕРАТУРА 1 1. Armstrong R. М„ Modular Programming in COBOL, New York, Wiley, 1973. 2. Aron J. D., The Program Development Process, Reading, Mass., Addison-Wes- ley, 1974. 3. Baker F. T., Mills H. D., Chief Programmer Teams, Datamation (December 1973). 4. Baker F. T., Structured Programming in a Production Programming Environ- ment, IEEE Transactions on Software Engineering (June 1975). 5. Beckmann M., et al., Advanced Course on Software Engineering, New York, Springer-Verlag, 1973. 6. Boehm B. W., Software and Its Impact: A Quantitative Assessment, Datamation (May 1973). J 7. Buxton J. N., Randell B., Software Engineering Techniques. NATO Science t Committee 1969. Available from Scientific Affairs Division, NATO, Brussels 39, Belgium. 3. Dahl 0. J., Dijkstra E. W., Hoare C. A. R., Structured Programming, New York, Academic Press, 1972. [Имеется перевод: Дал У., Дейкстра Э., Хоор К., Струк- турное программирование. — М.: Мир, 1975.] 9. Dijkstra Е. W., GO ТО Statement Considered Harmful, Communications of the ACM (March 1968). 10. Improved Program Technologies — An Overview, IBM Corporation, GC20-1850, 1974. ! 11. An Introduction to Structured Programming in COBOL, IBM Corporation, GC20-1776, 1975. 12. An Introduction to Structured Programming in PL/I, IBM Corporation, GC20- 1777, 1975. 13. Maynard J., Modular Programming, Philadelphia, Pa., Auerbach Publishers, 1-972. 14. McGowan C. L., Kelly J. R., Top-Down Structured Programming Techniques,. New York, Petrocelli/Charter, 1975. 15. Naur P., Randell B., Software Engineering, NATO Science Committee, 1968. ( Available from Scientific Affairs Division, NATO, Brussels 39, Belgium. 16. Special Issue: Programming, ACM Computing Surveys (December 1974). 17. Stevenson H. P. (Ed.), Structured Programming in COBOL, New York, Asso- ciation of Computing Machinery (April 1975). 18. That Maintenance Iceberg, EDP Analyzer (October 1972). 19. Wirth N., Program Development by Stepwise Refinement, Communications of i the ACM (April 1971). I 20. Wirth N., Systematic Programming: An Introduction, Englewood Cliffs, N. J., I Prentice-Hall, 1973. * 21. Yourdon E., Techniques of Program Structure and Design, Englewood Cliffs, N. J.., Prentice-Hall, 1975.
Отчего у нас никогда нет времени сде- лать что-либо хорошо, но всегда нахо- дится время на переделку. Высокая эффективность программ сни- жает расходы по эксплуатации и делаег возможным то, что нельзя сделать, если> программы неэффективны. Глава 3 ЭФФЕКТИВНОСТЬ ПРОГРАММ Основной задачей программирования является создание пра- вильных, а не эффективных программ. Эффективная программа’ не нужна, если она не обеспечивает правильных результатов. Это' правило Ван Тассела. Если бы программе не нужно было быть, правильной, она могла бы быть эффективной, насколько это нуж- но. Но программы создаются для решения некоторой задачи, а не для выполнения ее за некоторый короткий промежуток времени. Если задача не выполнена, время растрачено попусту. Другой вопрос, требующий рассмотрения, состоит в том, что правильность не является дополнительной характеристикой прог- раммы в отличие от эффективности. Эффективная, но неправиль- ная программа редко может быть сделана правильной, в то время как правильную, хотя и неэффективную программу можно опти- мизировать и сделать эффективной. Поэтому оптимизация являет- ся вторым этапом программирования. Первый этап — получение правильной программы. Нет смысла повышать быстродействие не- правильной программы. Неправильное программное обеспечение бесполезно независимо от его эффективности. Если программа неправильна, не имеет значения, какова ее эффективность. Наиболее разумный подход к программированию заключается в создании программы наилучшим возможным способом, не уделяя особого внимания эффективности. Затем, если программа в таком виде пригодна, если она нужна для работы, если ее будут выпол- нять многократно и если статус проекта и фирмы позволяет, тогда и только тогда следует рассмотреть возможность ее оптимизации. Исходя из этого, материал данной главы разделен на две части: способы нахождения того, что подлежит оптимизации, и методы оптимизации, которые могут быть применены к программам. Обыч- но большая часть времени расходуется на выполнение очень не- 8*
116 ГЛАВА 3 •большой части программы (~5% ее объема), называемой крити- ческой областью. Как правило, только критическая область объ- ектной программы оптимизируется программистом вручную. Для •обнаружения этих критических областей программы используются •средства, подобные профилировщикам (профилировщики обсуж- даются в гл. 5). Погоня за эффективностью часто ведет к злоупотреблению. За- мечено, что программисты тратят огромное количество времени, думая и беспокоясь о работе некритических областей программы. Помните, что современные машины отличаются высоким быстро- действием и очень мала разница во времени выполнения програм- мы, если некоторые, редко выполняемые операторы удается сде- лать эффективными. Экономию можно получить только за счет многократно выполняемых циклов. Однако неэффективные программы подобны больному зубу без пломбы: вы никогда не можете оставить его в покое, изучаете, ковыряете, давите на него, думаете о нем, и не потому, что это необходимо, а потому, что он есть. Некоторые программисты считают архаичной задачу написания эффективной программы. Это справедливо только в отношении не- больших программ, для выполнения которых используются ма- шины с высоким быстродействием и большим объемом памяти. Что же касается больших программ, то еще на стадии проектиро- вания определяются требуемые параметры, включающие время и емкость памяти. Например, мы можем сказать, что программа дол- жна обрабатывать некоторое число единиц информации в секунду и что для этого требуется объем памяти не более 150К. Мы можем предъявлять также такое требование, как обработ- ка компилятором некоторого числа команд в минуту. Для эконо- мических задач емкость памяти часто более критичный параметр, чем время. Если вы выбрасываете на рынок программу, для кото- рой требуется 200К памяти, тем самым вы исключаете возможность ее использования для машин с емкостью памяти менее 200К. Если же программа может разместиться в 150К, ваш рынок может рас- шириться в два раза (и соответственно увеличится прибыль). Создатель математического обеспечения должен установить не- обходимые объем памяти и производительность каждого модуля. Гл. 9 «Десять фунтов в пятифунтовом мешке» в книге Брукса, (см. ссылку к гл. 2 на стр. 62) содержит заслуживающий внимания со- вет по решению этой проблемы, возникающей при создании боль- ших программных проектов. На одной из стадий разработки было установлено, что компилятор с языка ФОРТРАН в системе OS 360 обрабатывал только 5 операторов в минуту; поэтому потребова- лись большие переделки. Определяйте требования к эффективности программы на стадии проектирования.
ЭФФЕКТИВНОСТЬ ПРОГРАММ П7 3.1. ОТНОШЕНИЕ К ЭФФЕКТИВНОСТИ Существуют три типа программ, и для каждого из них эффек- тивность должна быть различной. К первому типу относятся часто используемые программы. Это операционные системы, компиляторы, прикладные подпрограммы и системы резервирования авиабилетов. Для этих программ эф- фективность является первостепенной задачей вследствие их ча- стого использования и специфического выполнения. Второй тип составляют производственные программы, исполь- зуемые длительное время. Этот тип программ пишут профессио- нальные программисты. Хотя эффективность таких программ су- щественна, обычно еще больше внимания уделяют их эксплуатаци- онным характеристикам. Третий тип программ —программы, написанные не программи- стами, а научными работниками или аминистраторами. Время для этих людей важнее всего. Здесь эффективность имеет значение только для программ, которые должны уместиться в заданном объеме памяти и выполняться за приемлемое время. Следовательно, еще до написания программы необходимо уста- новить, насколько эффективной она должна быть. Очевидно, что следует модифицировать только те программы, которые выполня- ются многократно. Программисты, «экономящие на спичках», со- кращают на 10 мкс время выполнения редко используемой прог- раммы, затрачивая при этом 2 ч на программирование и много минут на компилирование и тестирование. Очевидно, что в этом случае вы ничего не сэкономите. Зато, как и при любом изменении программы, можете добавить в нее ошибки. Однако человеческая натура такова, что эффективность программ всегда будет вызы- вать интерес. 3.2. ЭФФЕКТИВНОСТЬ ИЛИ УДОБОЧИТАЕМОСТЬ? Многие методы, делающие программу эффективной, не наносят ущерба ее удобочитаемости. Эти методы следует использовать всегда. Но так как я намереваюсь сделать полный обзор методов эффективного программирования, замечу, что некоторые меры по повышению эффективности могут быть просто вредными для по- лучения удобочитаемой программы. Удобочитаемость программы более существенна, чем ее эффек- тивность. Дело в том, что удобочитаемую программу легче отла- живать, модифицировать и использовать. А всякую большую про- грамму обычно изменяет, модифицирует и применяет совсем не тот человек, который ее писал. Лишь в особых случаях программу следует делать более эф- фективной: программа либо не помещается в памяти, либо слиш- ком долго выполняется. Или же программа должна быть включена
118 ГЛАВА 3 в библиотеку и часто использоваться. В этом случае эффективность становится очень важным фактором и ей отдают предпочтение в ущерб удобочитаемости. Удобочитаемость программы обычно более важна, чем эффективность. 3.3. ОПТИМИЗИРУЮЩИЕ КОМПИЛЯТОРЫ Эффективность важна на двух стадиях разработки програм- мы: компилирования и выполнения. Если компилятор работает быстро, то он обычно составляет программу, которая выполняется медленно. Компиляторы, создающие эффективную объектную про- грамму, обычно бывают большими и работают медленно, так как оптимизируют объектную программу. В связи с этим в настоящее время на одной машине обычно используют по два компилятора для каждого входного языка. Пер- вый компилятор работает быстро, но создает неэффективную объ- ектную программу. Этот компилятор используется для отладки программ. Второй компилятор работает медленнее, однако произ- водит эффективную объектную программу, оптимизируя ее. Этот компилятор используют для создания объектных модулей. WATFIV — быстро работающий компилятор для языка ФОРТ- РАН, который располагает хорошими средствами отладки, но соз- дает относительно медленную объектную программу. WATBOL —- быстро работающий отладочный -компилятор для программ, написанных на КОБОЛе. Оба компилятора имеются в университете г. Ватерлоо (Канада, Онтарио, Ватерлоо, N2L 3G1). ALGOL W—быстро работающий компилятор для языка АЛГОЛ, которым располагает Стэнфордский университет. Существует не- сколько компиляторов для языка ПЛ/1. ПЛ/С — быстро работаю- щий отладочный компилятор, имеющийся в Корнеллском универ- ситете, ПЛ/1-оптимизатор — оптимизирующий компилятор вычис- лительных систем фирмы IBM. Оптимизаторы для языка КОБОЛ обсуждаются в книге М. Стэнли и др?) Однако многие машины имеют только один компилятор для каждого языка. В этом случае правильный выбор режима работы компилятора может сократить как время компилирования, так и время выполнения программ. На стадии отладки обычно не ис- пользуют листинг ассемблера и выходные колоды объектных про- грамм, поэтому они не должны выдаваться по умолчанию. Кроме того, такая информация, как дампы, планы распределения памяти, списки перекрестных ссылок, также не используется, если про- граммист не знает, как ее употребить, или к ней . обращаются очень редко. Тщательное изучение режимов работы компилятора и •> Stanley М. N. et al., COBOL Support Packages, Wiley, New York, 1972;
ЭФФЕКТИВНОСТЬ ПРОГРАММ 119 соответствующее их использование значительно сэкономят время. Можно ввести две процедуры для вызова одного и того же компи- лятора: первая из них будет вызывать рабочую версию, которая работает быстро и имеет мало дополнительных режимов; вторая предназначена для вызова отладочной версии, предоставляющей много дополнительных возможностей, например печать плана рас- пределения памяти и списка перекрестных ссылок. Некоторые современные компиляторы позволяют пользователю выбрать ресурс, который нужно оптимизировать. Пользователь может потребовать, чтобы был минимизирован размер памяти, не- обходимый для выполнения программы, либо время выполнения. Оптимизация одного ресурса выполняется за счет другого. К сожалению, об увеличении скорости компилирования можно сказать немного. Некоторые программные ухищрения могут со- кратить время компилирования, но они либо тривиальны, либо сильно зависят от компилятора. Методы, дающие положительный результат при использовании в одном компиляторе, не дают тех же улучшений в другом компиляторе этой же машины. Некоторые компиляторы работают более эффективно, если дли- ны имен переменных распределены равномерно. Другие компиля- торы более эффективны, если метки операторов равномерно рас- пределены по последнему символу. Естественно, что исключение неиспользуемых меток и выражений также уменьшает время ком- пилирования любого компилятора. Наличие меток препятствует некоторым типам оптимизации, поэтому неиспользуемые метки ухудшают эффективность объектной программы. При повторном прогоне программы следует исправить все ошибки в исходной про- грамме, а число предупреждающих диагностических сообщений должно минимизироваться каждый раз, когда это возможно. Работа эффективной программы стоит дешевле, и это всегда важно. Если программе требуется слишком много времени или памяти, ее нельзя выполнить на малой машине. В последние годы наблюдается феноменальный рост использо- вания мини-ЭВМ; поэтому весьма актуален вопрос о создании эф- фективных программ, нужных большинству пользователей таких машин. Компиляторы малых машин хуже выполняют оптимизацию объектной программы, чем компиляторы больших машин. Оптими- зация больше всего нужна там, где ее почти нет. Если для большой машины есть хороший оптимизирующий компилятор, то целесооб- разно с помощью этого компилятора получать рабочие программы для малой машины. Эффективное компилирование означает, что ббльшую часть задач можно решить, не прибегая к помощи ма- шинного языка или не переходя на более мощную машину. И то и другое неприемлемо в большинстве случаев. Можно многое сказать о том, как добиться эффективного вы- полнения программ. Оставшаяся часть главы посвящена методам создания эффективных программ.
12Q ГЛАВА 3 Описанные здесь методы не зависят от машины и применяемого языка и пригодны для оптимизации времени выполнения и мини- мизации объема памяти компилируемых программ. Эти методы машинно-независимы, так как улучшения, сделанные в программе с их помощью, приведут к ускорению работы программы на раз- ных машинах. Методы не зависят от языка (за исключением ме- тодов, специфичных для определенного языка) в том смысле, что они применимы в общем случае для языков высокого уровня. Не- которые из этих методов при их реализации на одних машинах бу- дут давать более заметные результаты, чем на других. Даже различные модели одной и той же машины могут иметь разные наборы команд ассемблера, которые обусловят заметную разницу в оптимизации. Некоторые компиляторы оптимизируют выполнение программы. Имеются два типа такой оптимизации: машинно-зависимая и ма- шинно-независимая. К первому типу относятся способы, результат применения которых зависит от используемой машины. Как пра- вило, эти способы оптимизации обычно не известны или не понят- ны программистам на уровне входного языка. Они состоят из спо- собов обработки индексов, назначения регистров и анализа ма- шинных команд. ) Второй тип оптимизации — машинно-независимая оптимиза- ция,— которая выполняется на уровне входного языка. Хотя ком- пилятор может оптимизировать программу, но обычно у програм- миста большие возможности для этого. Многие способы оптими- зации может сделать только программист, так как они требуют знания логики программы. Некоторые способы оптимизации, вы- полняемые компилятором, могли бы быть применены, но не реа- лизуются просто потому, что требуют слишком много машинного времени. Таким образом, программисты, создающие программы, могут сделать очень много для оптимизации своих программ. Использование обсуждавшихся здесь способов оптимизации не исключает необходимости в оптимизирующем компиляторе, так как машинно-зависимая оптимизация редко предусматривается на уровне исходной програ'ммы. Кроме того, даже наилучшим обра- зом оптимизированная человеком исходная программа будет улучшена оптимизирующим компилятором. Применением методов структурного программирования вы можете помочь оптимизиру- ющему компилятору. Дело в том, что в этом случае программа уп- рощается (меньше операторов GO ТО) и облегчается ее анализ оптимизирующим компилятором. Используйте оптимизирующий компилятор. 3.4. ОПТИМИЗАЦИЯ ПРОГРАММ Часто возникает необходимость в оптимизации некоторой рабо- чей программы, потому что либо она выполняется слишком долго,.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 121 либо для нее требуется слишком большой объем памяти. Вполне возможно, что при создании программы не думали о том, часто ли ее будут использовать, и не позаботились о ее эффективности. Или в результате значительной модификации программа стала неэффективной, а теперь она используется довольно часто и зани- мает слишком много машинного времени или; возможно, близка к превышению объема памяти, имеющегося в ее распоряжении. Поэтому нужно попытаться сделать программу более эффективной. Прежде чем описать метод оптимизации программы, вернемся к вопросу о выборе алгоритма. Если программу следует оптими- зировать, необходимо тщательно проверить алгоритм. Основными критериями при этом должны являться время и объем памяти, ис- пользуемые программой. Если оптимизация старого алгоритма не дает желаемого ре- зультата, тогда, возможно, следует выбрать другой алгоритм. В дальнейшем предполагается, что первоначальный алгоритм обос- нован и целесообразен, однако программисты, которым нужно оп- тимизировать свою программу, никогда не должны делать такого предположения. 3.4.1. СЕГМЕНТАЦИЯ ПРОГРАММ Программу, подлежащую оптимизации, следует разделить на подпрограммы. Оптимизация значительно, облегчается, если про- грамма уже разделена на подпрограммы в соответствии с принци- пами структурного программирования. Как только подпрограммы выделены, следует ответить на три вопроса: 1. Какой процент общего времени использует каждая подпро- грамма? 2. Насколько (в процентном выражении) оптимизируется каж- дая подпрограмма? 3. Сколько человеко-часов необходимо для достижения этой цели? Каждый из этих вопросов подробно обсуждается в трех следу- ющих разделах. 3.4.2. ВРЕМЯ РАБОТЫ ПОДПРОГРАММ После деления программы на подпрограммы следует опреде- лить процент времени, используемый каждой подпрограммой. Это необходимо сделать для того, чтобы узнать, какие части програм- мы расходуют больше всего времени. Если для определения времени работы каждой подпрограммы используются оценки и предположения, то часто возникают ошиб- ки и оптимизировать программу не удается. Поверхностные иссле- дования приведут к тому, что для оптимизации будут выбраны не те подпрограммы. После того как установлено фактическое время
122 ГЛАВА 3 работы, подпрограмма, которая используется больше других, должна оптимизироваться в первую очередь. Предположим, что программа разделена на четыре подпрограммы и время их выпол- нения составляет для подпрограммы А — 5%, подпрограммы С — 15%, подпрограммы В — 60%, подпрограммы D — 20%. Очевидно, что, даже если подпрограмму А исключить совсем (что, вообще говоря, невозможно), мы смогли бы сэкономить толь- ко 5% общего времени работы программы. Таким образом, попыт- ку оптимизации, вероятно, следует предпринять в первую очередь в отношении подпрограммы В, где возможна наибольшая эконо- мия. Если нельзя получить фактическое время выполнения каждой подпрограммы, применяется другой подход, заключающийся в под- счете количества операторов в подпрограмме, используя листинг программы на входном языке высокого уровня. Операторы, вклю- ченные в тело цикла, следует учитывать многократно. Подсчет ко- личества операторов — достаточно надежный показатель времени, требуемого для каждой подпрограммы, но определение фактичес- кого времени работы подпрограммы гораздо лучше. Большинство программ имеет одну критическую точку, кото- рая использует большую часть времени выполнения. Нередко ка- кая-либо малая часть программы расходует более 50% времени выполнения. Очевидно, что эту часть программы следует оптими- • зировать в первую очередь. После оптимизации первой критической точки хорошо проана- лизировать программу еще раз, чтобы найти теперь уже другую критическую точку, которую также следует оптимизировать. Этот процесс можно повторять до тех пор, пока будут получены значи- тельные результаты. 3.4.3. ПРОФИЛИ ВРЕМЕНИ ВЫПОЛНЕНИЯ Раньше определение фактического времени выполнения частей программы было самым лучшим из возможных методов, пригод- ных для проверки программ на потери потребляемого времени. Се- годня в компиляторах КОБОЛа, ФОРТРАНа, ПЛ/1 и АЛГОЛа для этих целей используются профили времени выполнения. Неоднократно отмечалось, что разработчики программ не де- лают правильных оценок эффективности времени и памяти для критических областей. Они создают программы, не используя ме- тоды программного обеспечения для определения критических об- ластей; иначе говоря, эти критические области так и остаются не- известными. Профилируйте ваши программы.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 123 Дополнительное преимущество использования профиля време- ни выполнения выявляется в процессе тестирования и отладки. Так как при профилировании подсчитывается частота употребле- ния операторов, профили могут использоваться для обнаружения операторов, которые не были тестированы. Операторы, выполняв- шиеся с нулевой частотой, не подвергались тестированию. Подоб- ным образом можно применить подсчет частоты использования операторов для определения, выполнялся ли данный оператор ожидаемое количество раз. 3.4.4. ОЦЕНИВАЙТЕ ВОЗМОЖНОЕ УЛУЧШЕНИЕ Если точно определен процент общего времени, используемый подпрограммой, следует оценить ее возможное улучшение. Если подпрограмма расходует небольшой процент от общего времени программы и ее можно лишь незначительно улучшить, то не стоит тратить на это усилий. При определении возможного улучшения необходима или воз- можна только приблизительная оценка. Не так уж важно, со- ставляет возможное улучшение 20 или 25%. Однако существенно, будет оно 5 или 70%. Определение возможного улучшения — не тривиальная задача. Опыт здесь, вероятно, самый лучший руководитель. В настоящей главе предлагается множество способов написа- - ния эффективных программ. Используя данные здесь рекоменда- ции, можно обнаруживать операторы, которые следует модифици- ровать. Наиболее оправдывает себя тщательная проверка циклов и операторов ввода-вывода. Теперь мы можем установить некоторый параметр, который будет показывать возможное улучшение. Если у нас имеется под- программа, использующая 50% общего времени работы програм- мы, но дающая только 5% повышения эффективности, мы получим в результате 2,5% (.50 * .05 = .025) общего улучшения эффектив- нрсти. Другая подпрограмма, которая использует Ю-% общего вре- мени, но дает 50% повышения эффективности, обеспечивает 5% (.50*.10=.05) общего улучшения эффективности. Таким образом, вторую подпрограмму следует оптимизировать в первую очередь, так как это дает больший выигрыш. Итак, при выборе подпрограм- мы для оптимизации следует учитывать произведение процента потребления времени и процента улучшения. 3!4.5. НЕОБХОДИМЫЕ УСИЛИЯ При определении возможного улучшения мы должны оценить работу, необходимую для достижения этого улучшения. Для каж- дой подпрограммы можно вычислить следующий коэффициент: Процент времени*Процент улучшения Необходимые усилия
124 ГЛАВА 3 Подпрограмму с самым высоким коэффициентом следует оптими- зировать в первую очередь. Главное преимущество тщательного выбора подпрограмм, подлежащих оптимизации, состоит в том, что важные ресурсы (время программиста и время машины) будут использоваться там, где они принесут наибольшую пользу. Если ресурсы распределены плохо, можно затратить много усилий и по- лучить в результате лишь небольшое увеличение эффективности. Если нет достаточного времени или нет оснований для переделки всей программы, можно переделать наиболее важные подпрограм- мы. Существуют два подхода к оптимизации имеющихся программ: «чистка» и перепрограммирование. Оба подхода имеют как до- стоинства, так и недостатки. Первый подход заключается в ис- правлении очевидных небрежностей в исходной программе. Для этого можно использовать любые особенности языка, пригодные для оптимизации. Многие предложения, изложенные в этой главе, помогут в данном случае. Достоинство этого подхода состоит в том, что он требует мало времени. Однако повышение эффективности при этом обычно незначительно. Второй подход состоит в переделке исходной программы. Если программа разделена на подпрограммы, можно переделать под- программу, которая расходует наибольшую часть времени. Реа- лизация этого подхода обеспечивает обычно наилучший результат. Однако этот подход и самый дорогой. Вероятно, многому можно научиться при первоначальной по- пытке написания программы; эту информацию можно использо- вать при перепрограммировании. Особенно полезно перепрограм- мировать задачу, если программа подверглась значительному из- менению. Частые переделки могут привести к изменению цели пер- воначальной программы. Программа может постоянно переделы- ваться в течение некоторого времени. И когда наконец результаты- работы программы окажутся приемлемыми и дальнейшие пере- смотры будут менее обширными и менее частыми, следует заняться переделкой, используя приобретенные знания и поставив новые цели, что приведет к значительной экономии времени выполнения программы. 3.4.6. ШАГИ ОПТИМИЗАЦИИ Прежде чем перейти к методам оптимизации выходной програм- мы, подведем итоги вышеизложенного материала: 1. Оптимизируйте только в случае необходимости, так как, вы- полняя оптимизацию, можно ухудшить удобочитаемость програм- мы, либо добавить ошибки, либо потерять много времени на про- граммирование. 2. Если оптимизация необходима, попытайтесь вначале исполь- зовать оптимизирующий компилятор. Возможно, он умеет делать все, что вам нужно.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 125 3. Определите критические области, подлежащие оптимизации. Оптимизация некритичных частей программ — пустая трата вре- мени. 4. Применяйте локальную оптимизацию в критических обла- стях. Слишком часто программисты тратят много времени, улучшая редко используемую программу. Остаток главы посвящен послед- нему шагу — локальной оптимизации. 3.5. ЭФФЕКТИВНОСТЬ ВЫПОЛНЕНИЯ ПРОГРАММ Эффективность программы во время выполнения определяется использованием двух ресурсов. Первый из них — необходимое для работы время, а второй — память, которая требуется программе. Время — более важный фактор для программиста, так как в боль- шинстве случаев программа оценивается количеством машинного времени, необходимого для ее выполнения. Поскольку сейчас мно- гие вычислительные центры взимают плату за объем используемой программой памяти, вполне вероятно, что вопросы, связайные с памятью, приобретут еще более важное значение. Обычно же проблема памяти существенна только тогда, когда ее недоста- точно. Оптимизировать память труднее, чем время выполнения. Нет ничего удивительного в том, что после оптимизации программа бу- дет выполняться на 25% быстрее, но было бы необычно получить уменьшение размера используемой ею памяти на 25%; при этом предполагается, что не было допущено серьезных ошибок в перво- начальном варианте программы. Помните, однако, что если бы вы выбрали новый лучший алго- ритм и перепрограммировали задачу, то могли бы существенно улучшить как время выполнения, так и объем потребляемой памя- ти. Очень трудно значительно сократить используемую програм- мой память, потому что сэкономленные области памяти разбросаны в небольших количествах по всей программе. В отличие от этого скорость выполнения можно значительно увеличить, например оп- тимизируя такую часть программы, как цикл, который многократ- но повторяется. Но иногда даже небольшая экономия памяти бывает просто не- обходима (чтобы можно было использовать конкретную програм- му для данной машины), тогда как небольшая экономия времени не так уже важна. Мы увеличиваем эффективность выполнения программы, улуч- шая ее, насколько возможно, на стадии компилирования. Компили- рование включает такие действия, как инициирование массивов и переменных, вычисление констант и распределение памяти. Рас- пределение памяти на стадии компилирования обычно увеличивает расход памяти за счет сэкономленного времени выполнения про- граммы.
126 ГЛАВА 3 Хорошие приемы программирования приводят к уменьшению как времени выполнения, так и объема используемой памяти, од- нако зачастую улучшение одного из этих факторов происходит за счет ухудшения другого. Большинство из обсуждающихся здесь методов экономит как время, так и память. Если использование метода приводит к возникновению конфликта между временем и •памятью, это будет оговорено. В большинстве случаев небольшое увеличение эффективности !не имеет смысла, если оно связано с тратой значительного количе- ства времени на программирование и вызывает ухудшение удобо- читаемости, надежности, универсальности или удобства. Однако иногда это стоит делать. Типичным примером является компиля- тор, при создании которого затрачиваются большие усилия для получения эффективной программы. Но, поскольку компилятор ис- пользуется неоднократно, даже малое увеличение эффективности приносит большие дивиденды. Эффективность также очень важна' в библиотеках программ. Поскольку эти программы используются часто, небольшое увеличение эффективности будет, как правило, сокращать время работы машины. В каждом конкретном случае повышение эффективности зави- сит от ряда факторов, таких, как стоимость улучшения програм- мы, частота ее использования, относительная скорость выполнения различных операций в машине, и от способа компилирования раз- личных операторов. Хорошей считается программа, которая выполняется при ми- нимальном расходе машинного времени. В этом случае за данный отрезок времени можно выполнить еще и другие задания. С появ- лением мультипроцессорной обработки (т. е. выполнением более одного задания за то же время) стало желательным также и ми- нимальное использование памяти, так как при работе в указанном режиме каждая программа должна находиться в оперативной па- мяти. Чем меньший объем памяти требуется каждой программе, тем больше программ можно разместить в оперативной памяти и обработать за одно и то же время. В режиме мультипроцессорной обработки использование оперативной памяти так же важно, как и расход времени. Сокращение занимаемой памяти или расхода времени будет уменьшать стоимость выполнения программы. Так как ресурсы машины очень дороги, то экономия даже небольшого количества времени или памяти в многократно используемой про- грамме может вполне стоить затраченных усилий. Обычно любая попытка улучшить эффективность предназнача- •ется для часто используемых программ. Хотя это весьма резуль- тативный подход, однако есть другой путь сэкономить большое ко- личество машинного времени. Вырабатывайте комплекс привычек, которые будут способствовать составлению более эффективной программы и соответственно экономии машинного времени.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 127’ 3.6. ПАМЯТЬ Обычно программисты не заботятся о памяти до тех пор, пока не превысят ее размеры. Тогда становится очевидным, что память не бесконечна. Идеальной считается ситуация, когда мы распола- гаем машиной с высоким быстродействием и достаточным объемом памяти. Однако это имеет место только при выполнении относи- тельно небольших задач на больших машинах. Недостаток объема памяти в машине является общей пробле- мой. Несколько лет назад казалось, что эта проблема временная, так как память становилась дешевле. Предсказывали, что скоро- будем располагать достаточной памятью для решения любой за- дачи. Это предсказание не учитывало трех важных фактов. Первый: из них заключается в том, что размер задач увеличивается по ме- ре увеличения размера памяти; эту ситуацию поясняет следующее правило: размер программируемых задач увеличивается, стремясь заполнить память машины, предназначенную для использования. Вторым неучтенным фактом является быстрый рост дешевых мини-ЭВМ. С тех пор как мини-ЭВМ вошли в повседневную жизнь, возросло значение эффективного программирования; это относится и к размеру памяти, и к быстродействию. И наконец, третий факт: с введением мультиобработки ни одной программе не отводят всю • память. Вместо этого память делят на участки различного разме- ра. Если программа достаточно компактна, чтобы разместиться в • одном из таких участков памяти, то чем меньше этот участок, тем быстрее выполняется программа и ниже стоимость. Экономное использование памяти почти всегда сопровождается: увеличением времени работы программистов и времени выполне- ния программы. Поэтому, если память не используется полностью^ вопрос о ее распределении не представляет интереса до тех пор,. пока ее достаточно. 3.6.1. ОВЕРЛЕИНОСТЬ ПРОГРАММЫ ' Под оверлейностью программы понимают возможность пере- несения подпрограмм во время работы программы в быстродейст- вующую память из некоторого другого типа памяти таким обра- зом, что несколько подпрограмм в различное время занимают од- ну и ту же область памяти. Оверлейность используют в том слу- чае, когда общие требования к объему программы превышают размер имеющейся в нашем распоряжении оперативной памяти. Одной из немногих систем, не предусматривающих оверлейность,. является компилятор WATFIV ФОРТРАНа. Программу следует разделить на логические части таким об- разом, чтобы неперекрывающиеся по времени части можно было- последовательно вызывать в память по мере необходимости. Обыч- но для оверлейности используются программные модули. Чтобы?
428 ГЛАВА 3 пересылать модули из периферийной памяти, необходимо дополни- тельное время. Оверлейность экономит память, однако приводит к дополнительному расходу времени программистов высокого класса и машинного времени. Если программу можно сделать оверлейной, следует продумать размещение данных. Каждый раз при вызове модуля с дисков он считается неизменным. Данные, полученные при последнем исполь- зовании этого модуля, не сохраняются. Например, модуль, фор- мирующий заголовок, может содержать число страниц, число строк и специальную информацию для заголовка. Если бы этот модуль подвергался оверлейности, накопленные данные терялись бы по-, еле каждого вызова модуля. Простейшее решение этой проблемы состоит в том, чтобы удалить всю рабочую информацию из моду- лей нижнего уровня и поместить ее в главный, вызывающий мо- дуль. Этот модуль никогда не должен подвергаться оверлейности. Следует помнить, что при использовании оверлейности обраще- ние к диску происходит каждый раз, когда нужен оверлейный сег- мент. На это затрачивается время выполнения программы, поэтому оверлейность используется только в случае, когда это абсолютно необходимо. Лучше проверить, нельзя ли сократить размер про- граммы, и избежать использования оверлейности. Можно сделать некоторые замечания относительно оверлейно- сти для экономии времени и памяти. Желательно, чтобы сегменты были примерно одного размера. Размер максимального оверлейно- го сегмента определяет требования ко всей памяти. Не «пересегментируйте». Для каждого оверлейного модуля тре- буется небольшое количество неоверлейной памяти (таблицы сег- ментов) и некоторое количество дополнительных команд, необхо- димых для организации его вызова. Кроме того, существенным фактором является время. Не следует выполнять несколько овер- лейных вызовов для каждого сегмента. Необходимо помнить, что каждое обращение к оверлейному модулю обычно требует чтения с диска. Чтобы не «пересегментировать», желательно поместить не свя- занные между собой части программы в один сегмент, сократив тем самым число сегментов. Вначале следует определить размер самого большого сегмента, а затем проверить, нельзя ли объеди- нить несколько меньших сегментов для сокращения числа вызовов оверлейных сегментов. Можно попытаться сегментировать программы различными спо- собами и, используя одни и те же данные, посмотреть, сколько вре- мени экономится или теряется в каждом случае. 3.6.2. ВИРТУАЛЬНАЯ ПАМЯТЬ Возможность оверлейности реализуется также при использова- нии виртуальной памяти. Если машина имеет виртуальную память, ню операционная система автоматически делит программу на части
ЭФФЕКТИВНОСТЬ ПРОГРАММ 129 фиксированной длины, называемые страницами. Кроме того, опе- рационная система в случае необходимости пересылает страницы в оперативную память. Таким образом, проблема организации оверлейности перестает быть обязанностью программиста и воз- лагается на машину. Программисты полагают, что они имеют в своем распоряжении оперативную память очень большого объема даже на машине со сравнительно небольшим размером памяти. Однако, если для ра- боты нужна часть программы, которой нет в оперативной памяти, теряется время: недостающая часть считывается с диска. Чем ре- же возникает такая ситуация, тем быстрее будет выполняться программа. Программист может принять некоторые меры для повышения эффективности выполнения программы при использовании вирту- альной памяти. Программу следует писать с подпрограммами. Это улучшает свойство локализованности программы, т. е. степени, до которой во время выполнения удается выделить в программе неко- торый ее фрагмент. Это легко выполняемый прием программиро- вания. Локализованность улучшается при использовании методов структурного программирования. Избегайте использования COM- MON-, или глобальных, переменных, так как они приводят к ухуд- шению локальности. Кроме того, старайтесь не употреблять опе- раторы GO ТО и метки. То и другое обеспечит высокую степень локализованности логики вашей программы и ссылок на данные. Организация циклов и подпрограмм также способствует лока- лизованности, так как приводит к многократному выполнению не- большой части всей программы. Чем выше степень локализованно- сти в программе, использующей виртуальную память, тем более эффективно будет выполняться программа, так как ей не придется вызывать в память много страниц. Таким образом, при исполь- зовании виртуальной памяти храните части программ, связанные друг с другом, рядом. Другой способ повышения эффективности выполнения программ при использовании виртуальной памяти состоит в расположении подпрограмм в том порядке, в каком они будут обращаться друг к другу. Это уменьшит количество страниц, которые нужно будет считать в оперативную память. Имеется несколько классических примеров очень простых про- грамм, которые неэффективно выполняются при использовании виртуальной памяти. Например, программа на ФОРТРАНе, при- сваивающая нулевые значения массиву чисел двойной точности: DO 15 К = 1, 512 DO 15 L=l, 20 15 X(K,L)=0.0 9—899
130 ГЛАВА 3 Программа составлена неудачно, так как массивы в ФОРТРА- Не хранятся столбцами и каждый столбец занимает страницу раз- мером 4К. Таким образом, при каждом выполнении оператора с меткой 15 нужно обращаться к другой странице. Внутренний цикл будет присваивать нулевые значения элементам, находящимся на 20 различных страницах, и это действие будет выполняться 512 раз. Решение проблемы состоит в изменении порядка циклов: не- обходимо сделать К= 1,512 внутренним циклом. В этом случае программа присвоит нулевые значения 512 элементам, расположен- ным на одной странице, прежде чем потребуется новая страница. Сделав такое простое изменение порядка циклов, мы можем умень- шить количество обращений к дискам в 512 раз. Подобная ситуация возникает при обработке некоторых мас- сивов внутри цикла, например: DO 25 K=l, N А(К)=0.0 В(К)=0.0 С(К)=0.0 25 CONTINUE В каждом массиве при каждой итерации цикла выполняется обращение к одному элементу. Если массивы большие или нахо- дятся в разных частях памяти, то при каждой итерации для каж- ' дого массива может отсутствовать нужная страница. Поэтому на виртуальной машине лучше обрабатывать каждый массив своим циклом DO. Попытайтесь прогнать подобные программы на своей машине с виртуальной памятью. 3.6.3. СОВЕТЫ ПОЛЬЗОВАТЕЛЯМ ВИРТУАЛЬНОЙ ПАМЯТИ 1. Создавайте программы при помощи оптимизирующего ком- пилятора, так как в этом случае программа будет короче. 2. Структурированные программы и использование модулей увеличат локализованность. 3. Удаляйте подпрограммы, обрабатывающие исключения, под- программы обработки ошибок и другие редко используемые раз- делы программы из ее основной части, чтобы увеличить интенсив- ность обращений к наиболее часто используемым страницам. 4. Присваивайте начальные значения элементам каждого мас- сива данных непосредственно перед первым его использованием, а не перед началом работы программы. 5. Обращайтесь к данным для считывания (или для записи) в порядке их расположения в памяти. Например, если массивы рас- положены в памяти по столбцам, выполните вначале все обраще- ния к одному столбцу, прежде чем перейти к следующему. Данные, не являющиеся массивом, можно разместить рядом, перегруппиро-
ЭФФЕКТИВНОСТЬ ПРОГРАММ 131 вав их описания. Лучше всего располагать рядом наиболее часто используемые массивы. В работе [7] вы найдете многочисленные предложения, отно- сящиеся к системам с виртуальной памятью. 3.6.4. ЗАГОЛОВКИ СООБЩЕНИЙ Заголовок может состоять из нескольких строк, причем неко- торые из них содержат одни пробелы. Обычно вся строка заголов- ка определяется как литерал. Если большая часть заголовка со- держит пробелы, для экономии памяти следует использовать ко- манды, выполняющие заполнение пробелами этой части заголовка. Это можно сделать с помощью двух команд вместо использования длинного литерала, состоящего из пробелов. Подпрограммы формирования заголовков следует помещать в оверлейные сегменты, поскольку они обычно расходуют много па- мяти и редко используются. Литералы заголовков также следует «запоминать» в оверлейном сегменте. 3.6.5. ЭКВИВАЛЕНТНОСТЬ В большинстве машинных языков предусмотрены операторы, позволяющие двум переменным занимать одну и ту же ячейку па- мяти. Если одна переменная используется только в начале про- граммы, а вторая переменная нужна в другой части программы, обе переменные могут занимать одну и ту же ячейку памяти, так как они используются в программе в разное время. Этот тип операторов можно применять для экономии памяти, потому что, как правило, в программе имеются переменные, кото- рые используются только в отдельных сегментах программы. Уста- новив эквивалентность массивов, можно сэкономить память, по- этому в программах, требующих большого объема памяти, этому вопросу следует уделить особое внимание. Традиционным спосо- бом сокращения объема памяти является уменьшение размера массивов. Это следует делать до установления эквивалентности двух массивов в памяти. А так как эквивалентность несвязанных переменных может быть неявно выраженной, ее следует соответ- ствующим образом прокомментировать. 3.6.6. ИСПОЛЬЗОВАНИЕ ЦИКЛОВ Использование циклов для повторения последовательности опе- раторов является обычным способом экономии памяти. Разные части программ нередко содержат одинаковые последовательности операторов. Если программист стремится к экономному расходо- ванию памяти, он должен найти одинаковые части программы, ко- торые могут быть преобразованы в циклы. Циклы требуют некоторого дополнительного количества памя- ти на инициирование, проверку, изменение индекса и установку 9*
132 ГЛАВА 3 всех констант. Однако уменьшение общего объема памяти за счет удаления повторяющихся команд весьма значительно. Немного- кратно повторяющиеся последовательности операторов, требующие сложной организации циклов, часто можно писать последователь- но, а не итеративно. 3.7. ВЫЧИСЛЕНИЕ КОНСТАНТ Программы становятся более удобочитаемыми, если в них ис- пользуются выражения, включающие константы. Для выполнения вычислений, содержащих только константы, применяют множество различных методов компилирования. Некоторые компиляторы вы- числяют все выражения с константами во время компилирования и запоминают результат. Другие компиляторы запоминают кон- станты, а вычисления осуществляются во время выполнения. Вто- рой способ неэффективен, если выражения находятся внутри цик- лов. Приведем примеры выражений с константами: ТАХ = INC — 3200/12 Y = SQRT(ABS(COS(2.30259 * * 1,839))) INT = IRATE/365/12 В = 4.0* A/3.0 Все эти выражения содержат константы, и, включая их в про- грамму, следует проявлять осторожность. Если выражения с кон- стантами не вычисляются во время компилирования, необходимо их всегда располагать вне цикла. Процесс выполнения операторов, значения которых известны на стадии компилирования, что позволяет не выполнять их во время прогона программы, обычно называют сверткой. Свертка выполня- ется также для значений, которые могут быть определены внутри блоков программы. 3.8. ИНИЦИИРОВАНИЕ ПЕРЕМЕННЫХ Если начальные значения присваиваются переменным одновре- менно с их объявлением, то тем самым экономится время выпол- нения программы. В этом случае переменные получают начальные значения во время компилирования, а не во время выполнения: Инициирование переменных во время их объявления облегчает документирование программ, а также помогает избежать ошибок, которые могут возникнуть в случае, если переменным не были при- своены начальные значения. В большинстве программ имеются переменные, которым нужно присвоить начальные значения. Например, PI = 3.14159 Е = 2.71828
ЭФФЕКТИВНОСТЬ ПРОГРАММ 133 Так как эти переменные никогда не изменяют своих значений в программе, им следует присвоить начальные значения с помо- щью оператора объявления переменных DECLARE. ПЛЦ: DECLARE (PI, Е) REAL INITIAL (3.14159, 2.71828); Это позволяет исключить два оператора присваивания и сэко- номить тем самым как память, так и время выполнения программы. Инициируйте переменные во время компилирования, 3.9. АРИФМЕТИЧЕСКИЕ ОПЕРАЦИИ Арифметические операции выполняются с различной скоростью. Полезно знать, какие операции выполняются быстрее, так как иногда бывает целесообразно заменить одну операцию другой. Пе- речислим математические операции в порядке возрастания време- ни их выполнения: 1) сложение или вычитание, 2) умножение, 3) деление, 4) возведение в степень. Некоторые медленно выполняемые операции легко заменить на более быстрые. Сложение выполняется быстрее, чем умножение, поэтому умно- жение на небольшое целое число следует заменять сложением. Так, 1 должно быть заменено на I+1-f-I. Если в выражении не все числа являются целыми, то при замене может быть утеряна точ- ность. Ошибка округления действительных чисел имеет тенден- цию накапливаться, а не уменьшаться. Так, если R.— действитель- ное число, а I — целое, то I*R более правильно, чем R-f-R+R-|-R... (I раз). Преобразование уравнений может привести к исключению опе- раций. Например, выражение X=2*Y-j-(A—1)/Р+2*Т можно за- менить уравнением Х=2* (Y-|-T)4-(A—1)/Р, что исключает одну операцию умножения. Поскольку деление является более медленной операцией, всюду, где возможно, его следует заменять умножением. Умножение вы- полняется по меньшей мере в два раза быстрее деления. Исклю- чайте деление из вашей программы всюду, где это возможно: вме- сто А/5.0 пишите А *0.2. Если в вычислениях вы все время делите на некоторое число, например на X, замените его на обратную величину. Например: А = 1.0/Х С = B-J-D/X
134 ГЛАВА 3 Используйте вместо этого обратную величину: RX == 1.0/Х A = RX С = В 4- D*RX В этом примере несколько действий деления заменено одним. Важно также правильно задать тип показателя степени в опе- рации возведения в степень. Всегда, когда это возможно, следует использовать целые числа. Например, Медленный способ: А**8.0 или А**Р, где Р — число с плаваю- щей точкой. Более быстрый способ: А **8 или А**1, где I — целое число. Второй способ обеспечивает более быстрое выполнение; кроме- того, он и более точен, так как при этом исключаются некоторые типы ошибок. Например, (—6)**1 разрешено, если I — целое чис- ло, но (—6) **Р не разрешается, если Р — число с плавающей точкой. Пусть Р=0,5, тогда мы имели бы корень квадратный из отрицательного числа. Таким образом, если выполняется возве- дение в степень целых чисел, делайте показатель степени целым числом. Медленный способ: В=А**Р, где Р — число с плавающей точ- кой. Более быстрый способ: 1Р = Р В=А**1Р, где IP — целое число. Если показатель степени — целое число, то операцию возведе- ния в степень выполняют повторяющимся умножением. Если пока- затель степени является числом с плавающей точкой, то для вы- полнения операции возведения в степень необходимо вызвать спе- циальную подпрограмму. Функция извлечения квадратного корня реализуется обычно гораздо быстрее, и точность при этом выше, чем при выполнении операции возведения в степень: Медленный способ: А* *0.5 Более быстрый способ: SQRT(A) Умножение выполняется значительно быстрее возведения в сте- пень, поэтому, если показатель степени — небольшое целое число, то операцию возведения в степень следует заменять несколькими операциями умножения: Медленный способ: VOL = (4.0 *R* *3)/3.0 Более быстрый способ: VOL= (4.0*R*R* R)/3.0
ЭФФЕКТИВНОСТЬ ПРОГРАММ 135 Для возведения в степень обычно требуется библиотечная про- грамма. Поэтому замена его несколькими операциями умножения экономит и память, и время, если показатель степени является не- большим целым числом. Заменяйте Х**2 на Х*Х Заменяйте X **3 на X* Х*Х Заменяйте Х**4 на (Х«Х)*(Х*Х) или на (((Х«Х)*Х)*Х) Последний пример содержит повторяющееся вычисление (Х»Х), которое в дальнейшем может быть оптимизировано. Замена одной операции другой, выполняемой более быстро, называется уменьше- нием силы операции. Уменьшение силы операции может иногда ухудшить удобочи- таемость программ, и об этом следует всегда помнить. Кроме того, при таком преобразовании некоторое количество машинного вре- мени затрачивается на управление промежуточными результатами. 3.9.1. АРИФМЕТИКА С ФИКСИРОВАННОЙ ТОЧКОЙ Большинство машинных языков допускает целочисленную арифметику. Она может применяться для любых типов вычисли- тельных операций. Для целых чисел используются специальные процедуры, потому что многие вычислительные задачи имеют дело только с целыми числами' (обработкой информации, связанной с инвентаризацией, переписью), а целочисленная арифметика обыч- но выполняется одной машинной командой, в то время как ариф- метика с плавающей точкой часто выполняется подпрограммами, включающими множество команд машинного языка. Некоторые машины могут выполнять 50 операций сложения целых чисел за время, требуемое для выполнения одной операции сложения с плавающей точкой. В этом случае целочисленную арифметику следует использовать всюду, где это возможно, осо- бенно для выполнения большого числа простых арифметических операций над целыми числами, такими, как индексы. Использова- ние неправильного типа переменных для индексов может значи- тельно увеличить время выполнения любой программы. Впрочем, некоторые машины выполняют операции с плаваю- щей точкой быстрее, чем операции с фиксированной точкой. Обыч- но это большие машины, служащие для проведения научных рас- четов. Они имеют специальное оборудование, обеспечивающее вы- полнение арифметических операций е плавающей точкой. Особое внимание следует уделить тому, чтобы внутренние пе- реключатели, счетчики и переменные, встречающиеся в многочис- ленных вычислениях, были такого типа, который приводит к са- мым ^эффективным вычислениям. Это чаще всего является проб- лемой при работе с ПЛ/1 и КОБОЛом, где допускаются арифмети-
136 ГЛАВА 3 ческие операции с переменными, являющимися строками символов. Если возникает такая ситуация, то для каждого вычисления необ- ходимо делать многочисленные преобразования. И память, и вре- мя можно сэкономить правильным описанием переменных. 3.9.2. СМЕШАННЫЕ ТИПЫ ДАННЫХ Смешанные типы данных получаются в результате использова- ния чисел различного типа в арифметических и логических опера- циях. Если вы смешиваете числа разного типа, то при выполнении арифметических операций часто бывают необходимы преобразова- ния. Ситуацию можно улучшить,-объявляя как можно больше пе- ременных одинакового типа. В этом случае нужно меньше забо- титься о том, чтобы избежать смешанных вычислений, поскольку почти все переменные одного типа. Хотя смешанная арифметика допускается, чтобы уменьшить количество ошибок и помочь про- граммисту, ее следует избегать, так как она занимает больше вре- мени и памяти. Избегайте смешанных типов данных. 3.9.3. СПОСОБ УСТРАНЕНИЯ ОШИБОК В некоторых простых компиляторах следует тщательно выби- рать тип используемых констант. Например, ____ А = 0 Неэффективно А = 0.0 Эффективно Некоторые компиляторы требуют преобразования целого нуля в вещественный во время выполнения программы, как в случае А = 0; во втором случае необходимость в преобразовании отсутст- вует. Хороший компилятор запоминает константу в нужной форме при компилировании, а не во время выполнения. Если преобразование необходимо, оно может занять очень мн®- го времени при его выполнении внутри цикла. Например, ФОРТРАН: DO 10 I = 1,1000 А(1) = о . 10 CONTINUE Вышеуказанные операторы должны выполнить 1000 преобразо- ваний. Использование оператора А(1) = 0.0 позволит избежать преобразований. Подобная ситуация возникает при использовании оператора Y = 1/Х
ЭФФЕКТИВНОСТЬ ПРОГРАММ 137 где преобразование необходимо, в то время как для оператора ' Y=1.0/X его не требуется. Простой способ избежать подобных проблем со- стоит в том, чтобы писать все константы того типа, который преоб- ладает в выражении. Если при использовании КОБОЛа вы выполняете расширенную арифметику на полях DISPLAY вместо COMPUTATIONAL, это приводит к неэффективному использованию памяти и увеличению времени выполнения в 4—10 раз. Числовые поля, которые не ис- пользуются в арифметических операциях, следует определять как буквенно-цифровые (X PICTURE). 3.9.4. ВЫРАВНИВАНИЕ ДЕСЯТИЧНЫХ ЧИСЕЛ Программы, использующие переменные в фиксированном деся- тичном формате, можно сделать более эффективными, если тща- тельно выбирать их атрибуты. Если эффективность важна, полез- но изучить руководство по языку для вашей машины, чтобы знать, когда необходимо выполнять преобразования в арифметических операциях. В КОБОЛе и ПЛ/1 десятичные точки должны быть выравнены, поэтому программист должен тщательно выбирать атрибуты пе- ременных. Например, КОБОЛ: WORKING-STORAGE SECTION. 77 A PICTURE S999V99. 77 В PICTURE S99V9. PROCEDURE DIVISION. ADD A TO B. И время, и внутреннюю память можно сэкономить, если опреде- лить В как 77 В PICTURE S999V99. Это исключает необходимость в дополнительных командах для вы- равнивания десятичной точки. 3.9.5. УПОРЯДОЧИВАНИЕ ПАМЯТИ Можно получить значительную экономию времени и памяти, если переменные в памяти надлежащим образом упорядочить. Дру- гими словами, некоторые типы переменных должны быть выравне-
138 ГЛАВА 3 ны в памяти на границу слова или двойного слова. Если этого не сделать, то компилятор будет создавать команды, которые про- изводят соответствующее выравнивание во время выполнения программы. В ФОРТРАНе переменные типа COMMON будут надлежащим образом выравнены, если они расположены в порядке уменьшения их длин, т. е. сначала комплексные, затем с двойной точностью, действительные и целые. В КОБОЛе переменные типа COMPUTA- TIONAL могут быть выравнены при использовании оператора SYNCRONIZED. Если массивы не выравнены, могут иметь место значительные потери времени и памяти. Чтобы получить точную информацию о выравнивании, следует обратиться к руководству по программированию для соответствующего языка. 3.9.6. ГРУППИРОВКА При выполнении операций одного приоритета над операндами разного типа следует группировать операнды одного типа, заклю- чая их в круглые скобки. Например, если операнд I относится к типу 1, операнд А — к типу 2, операнд R— к типу 3, то выражение вида I*A*I*R*A* R* I следует записать как ((1*1*1)* А*А)* R*R. Группировка и скобки помогает избежать преобразований, ко- торые необходимо выполнить для первого выражения. Можно избежать лишних преобразований, если вначале вы- полнить преобразование одного типа данных в другой, а затем использовать нужный тип. Например, если I и А — переменные, которые используются вместе и требуют дополнительных преобра- зований, преобразуйте их сразу и используйте полученную форму в дальнейшем во всех математических операциях. Например, Медленный способ: В=А*1 С=(А+1)*2.0 D=A*A/I Эти операторы используют переменные А и I несколько раз. Лучше преобразовать один раз переменную I в переменную типа А и затем пользоваться новой переменной. Более быстрый способ: А1 = 1 В=А*А1 С=(А+А1)»2.0 D=A*A/AI Здесь переменную I следует преобразовать не три раза, а толь- ко один.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 139 3.9.7. ИСПОЛЬЗОВАНИЕ ЛИСТИНГА АССЕМБЛЕРА Один из простейших путей проверки эффективности различных операторов состоит в том, чтобы воспользоваться распечаткой ко- дов, созданных ассемблером для каждой команды. Выборочная проверка количества команд, созданных ассемблером для каждой команды более высокого уровня, поможет продемонстрировать от- носительную эффективность различных способов составления про- граммы. Приведем пример программы и соответствующего ей ли- стинга ассемблера: ФОРТРАН: 0008 А=А4-В 0009 А=А+1 Ассемблер: 000190 А=А+В 8 LE 0,100(0,13) A 000194 AF 0,104(0,13) В 000198 STE 0,100(0,13) A 000190 А=А+1 9 L 0,108(0,13) I 0001А0 LPR 1,0 0001А2 ST 1,156(0,13) 0001А6 LD 0,152(0,13) 0001АА AD 0,136(0,13) 0001АЕ LTR 0,0 0001ВО BALR 14,0 0001В2 ВС 11,6(0,14) 0001В6 LCDR 0,0 0001В8 AE 0,100(0,13) A 0001ВС STE 0,100(0,13) A Второй оператор сложения требует преобразования типов пе- ременных. 3.9,8. ПОВТОРЯЮЩИЕСЯ ВЫЧИСЛЕНИЯ Может показаться очевидным утверждение, что никакие опе- рации не следует повторять, однако повторяющиеся операции обычно имеются во многих частях программы, особенно в циклах. Лучше сделать как можно больше простых вычислений в самом начале программы, а затем использовать их результаты в качестве переменных во всей программе. Этот процесс называется удале- нием избыточных команд и выполняется некоторыми компилято- рами. Рассмотрим примеры повторяющихся операций.
140 ГЛАВА 3 Вместо операторов X=Y+A/B*C Z=W-|-A/B*C лучше использовать АВС=А/В*С X=Y+ABC Z=W+ABC Хотя вторая группа операторов длиннее, она более эффектив- на и использует меньше памяти. Меньшее число операторов ис- ходной программы еще не доказывает, что такая программа более эффективна по времени или памяти. Дешевле обходится двукрат- ное обращение к переменной АВС, чем двойное вычисление А/В*С. так как пересылки очень дешевы. Приведем еще примеры. Неэффективное решение: SIGMA1 = SIN(THETA) + SIN(THETA) * * 2 SIGMA2 = SIN(THETA)/3.0 Эффективное решение: RHO = SIN(THETA) SIGMA1 = RHO + RHO * RHO SIGMA2 = RHO/3.0 В последнем примере функцию SIN нужно вычислить только один раз, тогда как в первом примере она вычисляется трижды. Естественный способ решения задачи часто приводит к избы- точным выражениям. Например, обычный способ нахождения кор- ней квадратного уравнения состоит в следующем: ROOT1 = (—В 4- SQRT(B»*2—4.0*А#С))/(2.0*А) ROOT2 == (—В — SQRT(B*»2—4.0* А*С))/(2.0* А) Однако более эффективной (но менее удобочитаемой) програм- мой является такая: D = А + А DIS = SQRT(B*B — 4.0* А *С) ROOT1 = (—В + DIS)/D ROOT2 = (—В — DIS)/D Количество сэкономленного времени или памяти варьируется в за- висимости от типа используемой машины. На некоторых машинах исключение очень простого повторяющегося выражения не дает почти никаких результатов. Но чем сложнее повторяющееся выра- жение и чем чаще оно встречается, тем большая получается эко- номия. На некоторых малых машинах операции с плавающей точкой занимают почти все время, особенно если в машине нет спе-
ЭФФЕКТИВНОСТЬ ПРОГРАММ 141 циального блока для арифметики с плавающей точкой. В этом слу- чае имеются возможности для получения большой экономии вре- мени. 3.10. ОБРАЩЕНИЯ К ФУНКЦИЯМ Некоторые программы обращаются к встроенным функциям, таким, как SQRT, SIN, COS и ABS. В ряде машин можно управ- лять обращением к функциям. Если мы хотим сократить время вы- полнения программы, желательно вставлять вычисления каждой функции прямо в программу. Если же наша цель состоит в умень- шении объема памяти, то следует иметь только одну копию про- граммы каждой функции и обращаться к ней при каждом выпол- нении функции. Проверка программы, созданной ассемблером, должна пока- зать, обеспечивает компилятор включение команд вычисления функций в программу или только обращение к функциям. Если желательно иметь только одну копию программы функции, можно использовать оператор EXTERNAL для указания, что вычисление функции не вставляется в программу. Число обращений к функциям можно сократить, запоминая их значения. Обычно программа содержит много обращений к функ- циям. Если ни одно обращение к функции не изменяет аргумента, то значения функций следует запомнить, чтобы использовать их в другом месте программы. Встроенные функции, подобные TIME (ВРЕМЯ) и DATE (ДАТА), часто используются в нескольких ме- стах программы, но к DATE никогда не нужно обращаться более одного раза, так как значение этой функции сохраняется. 3.11. ОПТИМИЗАЦИЯ в ПРОЦЕССЕ КОМПИЛИРОВАНИЯ Некоторые компиляторы выполняют оптимизацию, которая со- стоит в исключении повторяющихся вычислений, однако сущест- вуют сильные ограничения на количество типов оптимизирующих операций. Чем больше их число, тем больше компилятор и, следо- вательно, тем медленнее выполняется компилирование. Програм- мист может облегчить оптимизацию для компилятора, предприняв некоторые действия на уровне входного языка. Так, компилятор, как правило, не устраняет повторяющегося выражения в операторе типа А = X*Y + 2.0 4- Y*X Так как порядок переменных отличается, компилятор не может определить, что X *Y — то же самое, что и Y*X. Подобным же об- разом некоторые компиляторы не могут обнаружить, что 2.00 X— то же самое выражение, что и 2.0 *Х, или что нижеследующий опе- ратор содержит повторяющееся выражение А =В*В*С*С
142 ГЛАВА 3 Если выполнить перестановку, то повторяемость выражения станет очевидной: А = (В*С)*(В*С) Вот другой тип повторяющегося выражения: А = В —С D = C —В Два выражения справа от знака присваивания одинаковы, за исключением знака. Эти примеры показывают, как трудно оптими- зирующему компилятору обнаружить все повторяющиеся выраже- ния. Поэтому программисты должны записывать программу так, чтобы исключить, насколько возможно, повторяющиеся выраже- ния. Другим ограничением на оптимизацию в процессе компилиро- вания является то, что компилятор может оптимизировать только линейные участки программы, т. е. те, которые имеют один вход (первая выполняемая команда) и один выход (последняя выпол- няемая команда). Так, например, может быть оптимизирована нижеследующая группа операторов: ПЛ/1: К = 1/3.0*В; р = 3 1/3.0*В; А = В*В + 1/3.0*В; Операторы, предшествующие этой группе, должны обратиться к первому оператору и выйти после последнего оператора. Однако, если второй оператор имеет метку, компилятор не может продол- жать оптимизацию программы, так как не знает, какое влияние окажет переход на второй оператор из другого места программы. Например, ПЛ/1: - \ , К = 1/З.О*В; LOOP1: Р = 3 + 1/3.0*В; А = В*В + 1/3.0*В; Метка LOOP1 мешает оптимизации в процессе компилирования, и только автор программы, который хорошо разбирается в ее ло- гике, знает, можно ли оптимизировать эти три оператора. Подобная ситуация возникает, если встречается обращение к нестандартной подпрограмме или функции, которое будет мешать оптимизации в процессе компилирования, поскольку компилятор не знает, какие переменные изменяются в подпрограмме.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 143 3.12. ИСКЛЮЧЕНИЕ ЦИКЛОВ Всегда, когда это возможно, избегайте циклов, так как при их использовании тратится время на приращение и проверку пара- метра цикла. Использование циклов может увеличить время вы- полнения на одну треть. Небольшие вычисления часто можно де- лать, минуя циклы. Например, вычисление полинома по формуле POLY=((A(1)*X-|-A(2))*X+A(3))*X+A(4) выполняется быстрее, чем в цикле ФОРТРАН: POLY=A(1) DO 1 1 = 2,4 1 POLY = POLY* X-)-A (I) Кроме того, в каждом языке есть свои особенности инициирова- ния массивов. Ниже перечислены операторы, которые использу- ются для инициирования переменных: Язык Оператор ФОРТРАН DATA ПЛ/1 INITIAL КОБОЛ VALUE Эти операторы наиболее эффективны для инициирования пере- менных, так как выполняют его во время компилирования, а не во время выполнения программы. Один из способов уменьшения количества циклов состоит в объ- единении двух и более циклов в один. Уменьшение количества цик- лов часто бывает возможно, если тщательно проанализировать задачу перед программированием. Программы, которые подверг- лись значительной модификации, должны быть подвергнуты осо- бенно тщательному анализу. 3.13. ОРГАНИЗАЦИЯ ЦИКЛОВ Значительная часть времени при использовании циклов тратит- ся на инициирование и проверку индекса цикла. Тщательной орга- низацией вложенных циклов можно сэкономить время. ПЛ/1: DO К=1 ТО 20; Инициирование цикла выполняется 1 раз DO J=1 ТО 10; Инициирование цикла выполняется 20 раз
144 ГЛАВА 3 DO L=1 TO 5; Инициирование цикла выполняется 200 раз операторы тела Операторы выполняются 1000 раз цикла •END; Завершение цикла выполняется 1000 раз END; Завершение цикла выполняется 200 раз END; Завершение цикла выполняется 20 раз Инициирование цикла выполняется здесь 221 раз, а заверше- ние цикла —1220 раз. Путем реорганизации циклов (которая не всегда возможна) можно сократить число инициирований и завершений цикла. На- пример, „ . ПЛ/1: DO L=1 ТО 5; Инициирование цикла выполняется 1 раз DO J = 1 ТО 10; Инициирование цикла выполняется 5 раз DO К=1 ТО 20; Инициирование цикла выполняется 50 раз операторы тела Операторы выполняются 1000 раз цикла END; Завершение цикла выполняется 1000 раз END; Завершение цикла выполняется 50 раз END; Завершение цикла выполняется 5 раз В этом примере 56 инициирований и 1055 завершений цикла. Таким образом, число инициирований и завершений цикла может быть сокращено вложением циклов друг в друга таким образом, чтобы внешний цикл имел наименьшее число итераций. 3.14. ОПТИМИЗАЦИЯ ЦИКЛОВ При попытке ускорить выполнение программы циклы обычно являются самым важным фактором. Это очевидно, так как опе- раторы внутри цикла могут выполняться много тысяч раз и любая экономия, даже совсем небольшая, будучи увеличена в тысячи раз, дает существенный выигрыш. Программы можно сделать более эффективными, если пользоваться приемами программирования, изложенными в этой главе. Однако, если программист тратит мно- го времени, пытаясь повысить эффективность своей программы, улучшая оператор, который выполняется только один раз, это по-
ЭФФЕКТИВНОСТЬ ПРОГРАММ 145 добно попытке уменьшить свой вес, остригая ногти,— вы можете добиться некоторого успеха, но весьма незначительного. Поэтому уделяйте основное внимание циклам. Разберем при- мер: ---Цикл А-----100 итераций --Цикл В-----100 итераций I--Цикл С------100 итераций 1—Конец С --Конец В ---Конец А Рассмотрим вначале цикл С. Любая экономия, даже самая ма- лая, будет здесь увеличиваться в 100X100X100=1 000000 раз, т. е. коэффициент улучшения равен одному миллиону. Очень малое улучшение внутри цикла С гораздо выгоднее, чем значительное улучшение вне его. Затем рассмотрите цикл В и, наконец, цикл А. Вычисления внутри цикла следует минимизировать. Почти все предложения по оптимизации, рассмотренные в этой главе, следует применять для циклов, особенно в случае повторяющихся вычис- лений, неизменяемых индексов, арифметических операций и пре- образования данных. Повторяющиеся вычисления в циклах являются наиболее типич- выми ошибками, влияющими на эффективность. В примере —Цикл А .--Цикл В „ _ X=Y*Z4-C(I, J) I--Конец В ' ' - —Конец А Y*Z будет вычисляться каждый раз при выполнении цикла, кото- рый может повторяться тысячекратно. Вместо этого программа должна вычислять Y Z вне цикла, т. е. YZ=Y*Z —Цикл А ----Цикл В v BX=YZ + C(I, J) 1--Конец В iv>/ --Конец А Теперь величина Y*Z вычисляется только один раз независимо от того, сколько раз выполняется цикл. Можно значительно сэконо- мить время выполнения программы просто проверкой циклов, вы- полняющих много итераций, и уменьшением повторяющихся вы- числений внутри этих циклов. Этот процесс обычно называют иск- лючением инвариантных выражений или чисткой циклов. Инва- риантными являются выражения, которые не изменяются внутри Ю-899
146 ГЛАВА 3 цикла. Если при компилировании можно удалить одну операцию умножения из цикла, который повторяется во время выполнения программы 1000 раз, мы сэкономим время, которое затрачивается на выполнение 999 умножений. За исключение инвариантных вы- ражений из циклов взимается небольшая плата в виде памяти, не- обходимой для запоминания и повторного использования резуль- татов выражений. Оптимизируйте сначала внутренние циклы. В нижеследующем примере повторяющихся вычислений исполь- зуется обозначение для части вычислений: ПЛ/1: DO I = 1 ТО 10; DO J = 1 ТО 10; A(I, J)=B(I, J) -h D/1 Н- D/K; END; END; Выражения D/К и D/I являются повторяющимися вычислениями и их следует исключить из внутреннего цикла. Например, ПЛ/1: DK = D/K; DO 1= 1 ТОЮ; DI = D/I; DO J = 1 ТО 10; A(I, J) == B(I, J) 4- DI 4- DK; END; END; Здесь D/K вычисляется один раз вместо 100, вычисление D/I—де- сять раз вместо 100. Операция пересылки заменяет операцию де- ления. Пересылка обычно дешевле, чем арифметические операции. Однако здесь имеется еще одно выражение, которое можно удалить из цикла. Можете ли вы найти его? Иногда издержки по организации завершения цикла оказыва- ются слишком дорогими. Если это действительно так, то цикл сле- дует преобразовать. ФОРТРАН: DO 12 1 = 1,1000 А(1) = 0.0 12 CONTINUE
ЭФФЕКТИВНОСТЬ ПРОГРАММ 147 Улучшение, сделанное следующим образом: ФОРТРАН: DO 12 1= 1,1000,2 А(1) == 0.0 А(1+1) =0.0 12 CONTINUE сокращает наполовину количество выполняемых операций прира- щения и завершения цикла. Заметим, что полученная программа требует большего объема памяти. Такое преобразование цикла на- зывается его разверткой. Часто циклы можно объединять. Поясним на примере: ПЛ11: DO I = 1 ТО 500; Х(1) = 0.0; END; DO I = 1 ТО 500; Y(I) = 0.0; END; Очевидный способ сокращения как времени, так и памяти состоит в следующем: DO I = 1 ТО 500; Х(1) = 0.0; Y(I) = 0.0; END; Он называется сжатием или объединением циклов. Сжатие цикла сокращает накладные расходы цикла и требуемый размер памяти. Последний пример оптимизации цикла, который противополо- жен сжатию цикла: АЛГОЛ W: FOR I : = 1 UNTIL 100 DO IF (T) THEN X(I) := A(I) 4-B(I) ELSE X(I) : = A(I) — B(I); Здесь оператор IF должен выполняться в каждой итерации 100 раз, хотя логическая переменная Т не изменяется внутри цикла. Более эффективный способ состоит в следующем: 1о*
148 ГЛАВА 3 АЛГОЛ W: IF (Т) THEN FOR I : = 1 UNTIL 100 DO X(I) :=A(I)+B(I) ELSE FOR I := 1 UNTIL 100 DO X(I) :=A(I)~B(I); Теперь оператор IF выполняется только один раз. Этот процесс называется разъединением. Время выполнения здесь уменьшается за счет расхода памяти. 3.15. УСЛОВНЫЕ ВЫРАЖЕНИЯ Если в языке имеется конструкция IF THEN ELSE, то ее мож- но использовать в некоторой точке программы, когда одну пере- менную нужно сравнить с несколькими значениями. Обычно дела- ют так: IF (A = l) THEN MOVE С ТО D. IF (А=2) THEN MOVE С ТО Е. IF (А о=3) THEN MOVE D ТО С. В этом случае, даже если А=1, будут выполняться все one- • раторы IF. При использовании конструкции с ELSE сравнение можно прекратить, как только будет найдено истинное условие. Например, IF (A=l) THEN MOVE С ТО D ELSE IF (А=2) THEN MOVE С TO E ELSE IF (A=3) THEN MOVE D TO C В этой программе пропускаются проверки на истинность остальных условий, как только найдено истинное условие. Дальнейшее улучшение (которое может привести к ухудшению удобочитаемости программы) состоит в том, чтобы расположить на первом месте условие, которое, по всей вероятности, является истинным, а за ним остальную часть условия. То есть если А обыч- но имеет значение 3, то оператор, проверяющий это, следует поста- вить первым: IF (А=3) THEN MOVE D ТО С ELSE ...
ЭФФЕКТИВНОСТЬ ПРОГРАММ 149 Можно также расположить остальные условия в порядке их веро- ятности быть истинными. В этом случае условия будут в среднем .завершаться настолько быстро, насколько это возможно. Условные выражения часто содержат повторяющиеся выраже- ния. Например, для вычисления корней квадратного уравнения имеется формула ТЛ —Ъ ± т/"б2 — 4ас Корни=---------------- Дискриминант Ь2— 4ас нужно проверить на отрицательное значе- ние, так как нельзя извлечь квадратный корень из отрицательной величины. Чтобы запрограммировать это, мы, вероятно, сделаем следующее: ФОРТРАН: IF (В*В — 4.0*А*С) 25,10,10 10 ROOT1 = (—В + SQRT(B*B — 4.0*А*С))/(2.0*А) ROOT2 —... Выражение В*В — 4.0*А*С является повторяющимся, так как юно используется в операторе IF и при вычислении корня. Было бы лучше вычислить выражение перед оператором IF и запомнить его значение, как это сделано в нижеследующей программе: ФОРТРАН: DIS = В*В — 4.0*А*С IF (DIS) 25,10,10 10 ROOT1 = (—В + SQRT(DIS))/(2.0*A) Сложные повторяющиеся выражения предпочтительнее распола- гать внутри условных операторов, поскольку обычно сначала нуж- но проверить часть этих вычислений. 3.16. ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ Правильное расположение логических выражений может сэко- номить время выполнения оптимизирующего компилятора. Неко- торые компиляторы прекращают дальнейшую обработку выраже- ний, как только станет известен результат A .OR. В .OR. С... X .AND. Y .AND. Z... Как только в первом выражении встретится значение TRUE, все выражение считается истинным, тогда как второе выражение счи- тается ложным, как только в нем встретится значение FALSE.
150 ГЛАВА 3 Хороший компилятор прекращает обработку выражения после то- го, как становится известен результат. Правильно располагая пе- ременные в этих выражениях, можно сэкономить время выполне- ния. В выражении A .OR. В .OR. С переменную, которая, вероятнее всего, может быть истинной, сле- дует записать первой. Если С обычно бывает TRUE, А иногда бы- вает TRUE, а В редко бывает TRUE, то эти переменные следует расположить в следующем порядке: С .OR. A .OR. В ... Для того чтобы получить пользу от оптимизации вычислений, про- граммист должен расположить переменные в выражении в таком порядке, чтобы самая левая переменная чаще других вызывала завершение всего логического выражения. Аналогичное правило- может быть разработано для операции AND. Некоторые компиляторы не прекращают вычислений, когда ре- зультат уже известен. Например, ПЛ/1: IF (А>В) AND (C<D) THEN ... Для ускорения выполнения это выражение можно переписать сле- дующим образом: ПЛ/1: IF (А>В) THEN IF (C<D) THEN ... В данном случае не всегда нужно выполнять оба сравнения. 3.17. ИНДЕКСАЦИЯ Индексы полезны, хотя они расходуют и машинное время, и память. Ни один программист не захотел бы отказаться от ис- пользования индексов независимо от того,' какое повышение эф- фективности получится при этом. Однако некоторые действия мож- но предпринять для повышения эффективности при использовании индексации. Если к индексированной переменной обращаются внутри одно- го или нескольких операторов более одного раза, присвойте ее зна- чение другой переменной. Например, выражение X=(A(I)+1/A(I))+AG)
ЭФФЕКТИВНОСТЬ ПРОГРАММ 151 следует привести к виду А1=А(1) Х=(А1-Ь 1/AI) + AI Во втором примере необходимо выполнить только одно вычис- ление с индексом, в то время как в первом требуется произвести три таких вычисления. Возможно, это сделает оптимизирующий компилятор. Далее, не следует применять индексацию внутри цикла там, где можно обойтись использованием простой переменной. Рассмотрим пример вложенного цикла. ПЛ//: DO 1=1 ТО 10; DO К=1 ТО 25; В(К)=В(К)+А(1); END; END; Здесь при каждой реализации внутреннего цикла необходимо вычислять индекс для А(1), даже если он не изменяется в этом цикле. Более эффективной была бы следующая программа: ПЛ/1: DO 1 = 1 ТО 10; А1 = А(1); DO К=1 ТО 25; В(К)=В(К)+А1; END; END; В первой программе индекс для А(1) вычислялся 25X10=250 раз. Во второй программе индекс для А(1) вычислялся только 10 раз. Поскольку на вычисление индексов тратится время, следует вы- вести из циклов всю ненужную индексацию, заменив ее перемен- ными без индексов. На рис. 3.1 показан еще один пример уменьше- ния количества вычислений с индексами. Индексация обладает еще одной особенностью: чем больше ин- дексов используется, тем менее эффективна программа. Это озна- чает, что массив А(720) более эффективен, чем массив А(12, 5, 12). Большинство программистов охотнее работают с массивами, име- ющими два и более индекса, если индексы имеют мнемонический смысл при программировании. Но если имеется часто применяемая подпрограмма, которая использует многомерные массивы, то лучше перейти в процессе отладки к одномерным массивам.
152 ГЛАВА 3 Некоторые программисты все время используют одномерные массивы и, по-видимому, считают, что ухудшение удобочитаемости при этом незначительно. PL/I Медленный способ DO I = Г ТО N; DO J = 1 ТО М; A(I,J) = 0; •' DO К = 1 ТО L; A(I,J) = A(I,J) + B(J,K) * C(K,J); END; END; END; PL/I более быстрый способ DO I = 1 TO N; DO J = 1 TO M; TEMP = 0; DO К = 1 TO L; TEMP = TEMP + B(J,K) * C(K,J); END; A(I,J) = TEMP; END; )END; z Рис. 3.1. Пример вычислений с уменьшением количества индексаций. Для преобразования двумерной матрицы NXM в вектор NXM элементов можно использовать один из нижеследующих способов: 1. Если первый индекс изменяется быстрее второго, то К-й эле- мент получается как VECTOR (К) = MATRIX (I, J) гдеК = 1+[Н* (1-1)]
ЭФФЕКТИВНОСТЬ ПРОГРАММ 153 2. Если последний индекс изменяется быстрее второго, то К-й элемент получается как VECTOR (K)=MATRIX (I, J) где К = J + [M* (J —1)1 I метод преобразует Г“ ье £| в [adb ecf]. II метод преобразует B[abcdef], Ввиду того что эти методы преобразования двумерного масси- ва в одномерный ухудшают удобочитаемость большинства про- грамм, их следует применять очень осторожно. Еще один метод сокращения числа индексов заключается в том, чтобы сделать двумерные или многомерные массивы эквивалент- ными одномерному массиву. Очень часто для этого необходимо присвоить нулевые значения всем элементам многомерного масси- ва. Обычно это делают следующим образом: ФОРТРАН: DIMENSION А(2,3,4,5) DO 10 1=1,2 DO 10 J = 1,3 DO 10 K=l,4 DO 10 L=l,5 A(I, J, K, L)=0.0 10 CONTINUE В данном случае требуются четыре индекса и четыре цикла. Значительно более эффективным решением этой задачи является такое: ФОРТРАН: DIMENSION А(2,3,4,5), В(120) EQUIVALENCE (А(1,1,1,1), В(1) ) DO 10 1 = 1,120 В(1)=0.0 10 CONTINUE Операторы устанавливают эквивалентность четырехмерного масси- ва А одномерному массиву В. Теперь требуются только один ин- декс и один цикл для присвоения нулевых значений элементам этого массива. Преимущества этого метода состоят в сохранении
154 ГЛАВА 3 памяти, ускорении компилирования и более быстром выполнении программы. Недостаток заключается в снижении удобочитаемости: читающий должен заметить, что два массива эквивалентны. Ухуд- шение удобочитаемости может быть серьезным недостатком, если оно приводит к путанице или затрудняет модификацию программы. Внутри циклов следует избегать вычислений со сложной индек- сацией. Например, ФОРТРАН: DO 6 I = 1,’10 X(3*I4+ 4)=Y(3*I + 4) + С 6 CONTINUE Каждый раз при выполнении цикла производятся вычисления с двумя сложными индексами. Поскольку оба индекса вычисляются одинаково, следует вычислить индекс один раз, установить эквива- лентность его новой переменной и затем использовать эту перемен- ную следующим образом: ФОРТРАН: DO 6 1 = 1, 10 IK=3*I4-4 X(IK) = Y(IK) + С 6 CONTINUE Хотя программа и улучшилась, ее можно сделать более совер- шенной, если переписать заново, используя цикл с приращением. ФОРТРАН: DO 6 1 = 7, 34, 3 X(I) = Y(I)+C 6 CONTINUE Ссылка на переменную с постоянным индексом обычно не тре- бует проведения оптимизации программистом. Большинство компи- ляторов вычисляет адрес арифметического выражения с постоян- ными индексами [такого, как А (7)=] во время компилирования, благодаря чему нет необходимости в индексации во время выпол- нения. Некоторые языки, такие, как ПЛ/1, допускают выражения над массивами, т. е. операторы ПЛ/1: DO I = 1 ТО 100 А(1) =В(1) END;
ЭФФЕКТИВНОСТЬ ПРОГРАММ 155 могут быть заменены на А=В Если указывается только имя массива, будет обрабатываться весь массив. Когда используют выражения над массивами, как в этом примере, компилятор выбирает самый эффективный метод для ра- боты с массивом. Большинство компиляторов создает команды для ввода-вывода всего массива, если в списке входных-выходных данных указано только имя массива (без индекса). Хотя это самый эффективный способ ввода-вывода массивов, он используется только в том слу- чае, когда должен быть обработан весь массив и когда порядок ввода-вывода массивов известен. 3.17.1. ТИП ИНДЕКСА Каждый язык программирования имеет определенный тип дан- ных, наиболее предпочтительный при индексации. Его следует ис- пользовать всегда, когда это возможно, поскольку при этом уско- ряется обработка. Использование другого типа индексов заставит машину выполнить несколько преобразований при каждом обра- щении к индексу. Легко оценить разницу в эффективности. Нужно просто выполнить программу дважды: с предпочтительным и дру- гим типом индексов. Разница во времени выполнения обычно довольно заметна. Наи- более эффективны индексы, представленные в двоичной форме. \ Используйте для индексации наиболее предпочтительный тип данных. 3.17.2. ФОРМА ЗАПИСИ ИНДЕКСА . Большинство компиляторов допускает использование в качест- ве индексов различные арифметические выражения. Однако только при некоторых конструкциях индексных выражений возможна оп- тимизация. К таким конструкциям относятся следующие: 1. V — целая переменная. 2. С —целая константа. 3. V±C — целая переменная плюс/минус целая положительная константа. 4. C*V — целая положительная константа, умноженная на це- лую переменную. 5. С1*У±Сг — целая положительная константа, умноженная на Целую переменную, плюс/минус целая положительная константа. Некоторые из более ранних компиляторов (ФОРТРАН II) фак- тически ограничивали индексы только вышеуказанными типами записи. Индексы всегда должны были записываться только в ви-
156 ГЛАВА 3 де, показанном выше. Алгебраический эквивалент такой записи индекса обычно не допускает такую же оптимизацию. Так, при за- писи вида A(I-f-3) оптимизация возможна, в то время как запись А(3+1) может ее не допускать. Для вышеприведенных видов записи индексов возможны по крайней мере частичные вычисления во время компилирования. Например, адрес А (2) может быть определен во время компили- рования, что позволит не производить необходимых вычислений индекса во время выполнения программы. 3.18. ВВОД-ВЫВОД Операции ввода-вывода расходуют много времени и должны быть сокращены до минимума. Другими словами, можно сказать так: не вводите данные, которые можно вычислить внутри програм- мы. Кроме того, удостоверьтесь, что каждый оператор ввода-выво- да передает минимальное количество физических записей. Две по- следовательные команды ввода-вывода для одного и того же уст- ройства часто можно объединить в одну команду. Это сокращает количество обращений к системным подпрограммам ввода-вывода и к супервизору. Не забывайте также после отладки программы изъять все лишние операторы ввода-вывода. Для каждого языка имеется наиболее эффективный способ записи или считывания информации. Если программа предусмат- ривает выполнение большого числа операций ввода-вывода, можно сэкономить значительное время, используя наиболее эффективный способ. Например, неформатный ввод-вывод обычно быстрее фор- матного; его можно использовать при работе с лентами, дисками и перфокартами, если данные выводятся во внешнюю память, а за- тем считываются опять той же или другой программой. Неформат- ный ввод-вывод выполняется быстрее, поскольку для этого не тре- буется преобразования данных из внутренней формы представле- ния во внешнюю или обратного преобразования. Этот способ вво- да-вывода более точен, так как во время преобразования данных из внутренней — машинной — формы во внешнюю — форматную— могут быть утеряны значащие цифры. 3.18.1. ВЫВОД НА ПЕЧАТЬ Вывод данных на печать следует сокращать до необходимого минимума. Это нужно делать не только из-за расхода времени на печать, но и потому, что редко кто-либо читает большие сообще- ния, состоящие из нескольких страниц чисел. Печатайте только ту информацию, которую пользователь хочет прочитать. Не говоря уже об экономии машинного времени, бумаги и денег, на игнориро- вание всех ненужных результатов (которые следует соответству- ющим образом определить) машина тратит гораздо меньше вре-
ЭФФЕКТИВНОСТЬ ПРОГРАММ 157 мени, чем вы его расходуете на то, чтобы разорвать и выбросить в мусорную корзину ненужные распечатки. С точки зрения эффек- тивности всегда дешевле написать лишние операторы, исключаю- щие ненужный вывод, чем печатать ненужную информацию. Бумага для машины стоит дорого, поэтому не размещайте одно число в строке, если вы печатаете много строк. Программируйте экономно — печатайте в каждой строке столько информации, сколь- ко можно прочитать. Старые распечатки можно использовать для черновиков. Кроме того, перфокарты и бумага — высококачествен- ный материал, который можно выгодно использовать повторно. 3.18.2. ПЕРФОКАРТЫ При использовании перфокарт минимизируйте их число. Этого можно достичь; упаковывая информацию на карте с максимальной плотностью. Десятичные дроби обычно можно сократить, что позво- ляет разместить на перфокарте больше цифр. Однако формат информации на перфокарте ограничен устрой- ством ввода и требованиями к перфокартам. Если применение перфокарт не обязательно и объем информации велик, перфокарты следует заменять файлами на ленте или на диске. Иногда информацию, подлежащую вводу, можно значительно сократить. Предположим, нам нужно ввести следующий столбец цифр: 10.0 10.1 10.2 24.9 25.0 Эта операция займет много машинного времени. Будет лучше, если мы считаем начальное и конечное значения (10.0, 25.0) и ве- личину приращения (0.1), а промежуточные значения машина вы- числит сама. 3.18.3. МАГНИТНЫЕ ЛЕНТЫ Если необходимо запомнить промежуточные машинные данные, не требующие физического носителя типа перфокарт, то дешевле всего использовать магнитную ленту. Ленту можно применять толь- ко для последовательных файлов. Сравнение магнитных лент и пер- фокарт показывает, что использование магнитной ленты вместо перфорации более 4000 карт обходится дешевле. Кроме того, маг- нитную ленту можно использовать многократно, а перфокарты — только один раз. Перфокарты занимают много места, в то время
158 ГЛАВА 3 как магнитная лента компактна. Перфокарты следует. использо- вать для вывода информации только в том случае, если выходная колода мала, например менее 1000 карт, или если карты нужны для каких-то других целей. Лента более эффективна для хранения или обработки данных. Самые медленные лентопротяжные устройства имеют скорость за- писи ~4000 символ/с, в то время как самый быстрый выходной перфоратор пробивает только 400 символ/с. Магнитная лента обычно может хранить информацию, эквивалентную 200000 кар- там. Ленту можно использовать с разной степенью эффективности в зависимости от того, как расположены на ней данные. Ленты ор- ганизованы в записи, блоки и файлы. Файл — полный набор запи- сей, обрабатываемых при выполнении некоторого задания. Блок — это группа записей, а запись — данные, обрабатываемые одной ко- мандой чтения или записи, появляющейся в программе. Если за- писи не объединены в блок, то при каждом обращении с ленты считывается одна запись. Чтение информации непосредственно с ленты выполняется медленно и неэффективно по сравнению с чте- нием из буферной области. Если записи сгруппированы в блоки, то весь блок считывается из буфера или записывается в него за одно обращение. Тогда каждая команда записи, встретившаяся в программе, помещает запись в буферную область. Как только бу- фер заполнен, происходит физическая запись (выполняемая управ- ляющей программой) блока на ленту; Ввод выполняется аналогич- но, т. е. блоки записей автоматически считываются с ленты, и каж- дый оператор ввода, встретившийся в программе, считывает одну запись из буфера. Машина может передавать записи из буфера гораздо быстрее, чем с ленты. Здесь показано, как выглядит на ленте блок записей. Этот блок состоит из п записей. Если весь блок считан, супер- визор автоматически читает новый блок. Обычно ввод-вывод вы- полняется с двойной буферизацией, что означает возможность до- ступа к двум блокам. Таким образом, блок записей может пересы- латься в один буфер (или считываться из него), в то время как записи другого буфера обрабатываются. Накапливание записей в буферной области и запись блока на магнитную ленту (или чтение его с ленты) выполняются автоматически. Все, что должен при этом сделать программист,— определить тип и размер записи, а также размер буфера. Если блоки достаточно велики, то не долж-
I ЭФФЕКТИВНОСТЬ ПРОГРАММ 159 но тратиться время на ожидание ввода-вывода, так как один бу- фер всегда должен быть готов для использования’). Каждый блок на ленте отделяется от другого межблочным про- межутком (МБП) длиной 15,2 мм* 2>. Этот промежуток необходим для правильного считывания данных. Если несблокированные дан- ные, являющиеся содержимым перфокарт, расположены на ленте Рис. 3.2. с плотностью записи 32 бит/мм, это означает, что для размещения 80 символов потребовалось бы 2,5 мм. Так как МБП занимает 15,2 мм (рис. 3.2), то отношение МБП к данным составляет 6:1, что означает, что большая часть времени тратится на чтение МБП. Если блоки имеют длину 1600 символов (содержимое 20 пер- фокарт), то для каждых 51 мм данных используется только один МБП длиной 15,2 мм. Эффективность возрастает, если применять большие блоки в по- следовательных файлах. Если в каждом блоке находится только одна запись, то каждый оператор ввода-вывода обрабатывает блок. Так как расходы на ввод-вывод достаточно велики, этот про- цесс может быть довольно дорогим. Кроме того, теряется много времени на ожидание новых записей, которые должны быть об- работаны. Если каждый блок состоит из 10 записей, то на каждую запись тратится только одна десятая времени ввода-вы- вода. Если же в каждый блок включить 100 записей, то на одну запись расходуется одна сотая времени ввода-вывода. Эта ситуа- ция может привести к предположению, что всегда лучше иметь большие блоки, что верно только отчасти. Группируйте записи в эффективные блоки для ввода-вывода. ” В большинстве случаев двойная буферизация не обеспечивает полной за- грузки процессора. — Прим. ред. 2) Величина межблочного промежутка зависит от типа ленты и лентопро- тяжного механизма. — Прим. ред.
160 ГЛАВА 3 Два обстоятельства препятствуют использованию блоков неог- раниченной длины: ошибки и объем памяти. Если при вводе-выво- де данных с ленты допущены ошибки, вы будете часто терять всю информацию, находящуюся на блоке. Если же блок содержит од- ну запись, то потери не так значительны, как в случае блока из 1000 записей. Поэтому, если ошибки при вводе-выводе с ленты яв- ляются обычным явлением, используйте небольшие блоки. Суще- ственная часть ошибок при вводе-выводе вызвана плохим состоя- нием лентопротяжного устройства. Так что, прежде чем начать операции ввода-вывода, приведите в порядок лентопротяжное уст- ройство. 3.18.4. БУФЕРЫ Вторым ограничением, налагаемым на размер блока, является объем памяти. Чем больше размер буфера, тем меньше нужно вре- мени для ввода-вывода и тем больший объем памяти требуется. В этом случае эффективность выполнения ввода-вывода достига- ется за счет увеличения используемой памяти. Для каждого буфе- ра требуется некоторый объем памяти. Обычно имеется ограниче- ние сверху на размер блока, который может быть считан. В неко- торых машинах блок содержит 32 760 символов. Если вы распола- гаете двумя буферами, а блок имеет максимальный размер, то буферы занимают 65520 символов, в памяти. Если ваша программа велика или мала емкость памяти используемой вами машины, то такой размер блока недопустим. > Каков же простой ответ? Если предположить, что ошибки вво- да-вывода отсутствуют (их не должно быть, если лентопротяжный механизм исправлен и хорошо обслуживается), то размер блока в 4000 символов обычно является минимальным. Это соответствует информации, помещающейся на 50 перфокартах (50X80=4000). Максимальный размер блока определяемся имеющейся памятью. Если нужно оптимизировать управление лентой таким образом, чтобы избежать слишком больших и слишком малых блоков, иде- альной была бы следующая ситуация. Каждый раз во время обра- ботки блока данных выполняются некоторые вычисления. Лучше всего было бы всегда иметь блок данных, готовый к вычислениям. Машина может выполнять ввод-вывод, используя буфер, пока вы- полняются другие вычисления. Поэтому, если время, требуемое для обработки каждого блока, ненамного больше времени, необ- ходимого для ввода-вывода предыдущего блока, то в машине с двойным буфером блок данных был бы всегда доступен для об- работки. При этом размеры блоков не должны быть чрезмерно большими. Таким образом, в файле, где каждая запись требует большого количества вычислений, лучше использовать блоки небольшого раз- мера. А если для файла выполняется мало вычислений, увеличение размера блоков повысило бы эффективность.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 161 Некоторые машины допускают увеличение числа буферов. Для больших задач с частыми обменами можно определить наиболее эффективные сочетания числа буферов и размера блоков с по- мощью нескольких тестовых прогонов. Для каждой программы можно установить количество буферов, отведенных каждому файлу. Но, поскольку буферы требуют опре- деленного объема памяти, существует предел для числа использу- емых буферов. Основной способ экономного расходования памяти состоит в том, чтобы использовать одну и ту же область памяти более чем для одного буфера. Это возможно только в случае, если два или более файла никогда не будут открыты в одно и то же время. Тогда вначале один файл использует область буфера, а за- тем, после того как первый файл будет закрыт, эта область предо- ставляется для использования второму файлу. Обычно так и поступают, когда с одним и тем же файлом рабо- тает несколько программ. Мы можем регулировать количество, буферов, используемых в каждой программе, но не можем изме- нить числа записей в блоке для большинства типов файлов (т. е. для последовательных файлов на ленте), так как размер записи и их число в блоке уже установлены. Поэтому на этапе проекти- рования системы следует тщательно рассматривать вопрос о раз- мере блока, исходя из требований, предъявляемых к объему памя- ти и к скорости обработки для каждого задания в системе. В про- тивном случае либо будет чрезмерно расходоваться память, либо обработка будет выполняться очень медленно. Большинство систем имеет двойную буферизацию, т. е. каждо- му файлу назначаются два буфера. Но если возникает необходи- мость в памяти, то под нее занимают второй буфер. Исключение второго буфера обычно приводит к экономии памяти, но к увели- чению времени выполнения, так как не будет выполняться совме- щение операций обработки с операциями ввода-вывода. 3.18.5. ФАЙЛЫ НА ДИСКАХ Эффективная организация файлов на дисках труднее, чем на лентах, потому что для этого имеется много возможностей. Ука- жем несколько важных рекомендаций. Для файлов на дисках следует использовать неформатный ввод-вывод. Эффективность в этом случае повышается за счет двух факторов: экономии памяти на дисках и отсутствия необходи- мости преобразовывать данные из внутреннего (машинного) пред- ставления во внешнее (для дисков). Кроме того, неформатный ввод-вывод обеспечивает лучшую точность передаваемых данных. Если в одном задании используется более одного файла на дисках, не располагайте все файлы на одном диске. Используйте различные диски, что позволит осуществить совмещение процесса ввода-вывода с обработкой файла. Если на одном диске располо- <1—899
162 ГЛАВА 3 жены два файла, теряется слишком много времени на ожидание установки держателя читающей головки диска. Если возможно, сделайте так, чтобы файлы при работе с дисками использовали разные каналы. Последовательные файлы. Для последовательных файлов на дисках размеры блоков данных должны быть примерно равны ем- кости дорожки на диске, но не превышать ее. Если программа так велика, что блоки такого размера невозможно расположить в ма- шине, то на дорожке следует размещать целое число блоков. Па- мять на дисках тратится на промежутки между блоками, поэтому чем меньше размер блока, тем больше памяти на дисках исполь- зуется для МБП. Таблица возможных размеров блоков дается изготовителем устройств. Таким образом, последовательные файлы на дисках подобны последовательным файлам на ленте, в которых большие блоки (но не больше, чем емкость дорожки) дают самые эффективные операции. Пересылка многочисленных, очень коротких логических блоков нежелательна. Если подпрограмма выполняет в основном ввод-вывод, возмож- но, более эффективно было бы уменьшить размер блока и увели- чить количество используемых буферов. При выполнении больших заданий можно позволить себе немного поэкспериментировать и, используя различные комбинации размера блоков и числа буфе- ров, определить наиболее эффективные сочетания. 3.19. ИЗУЧЕНИЕ НОВЫХ ОПЕРАТОРОВ Как только вы.достаточно хорошо изучили язык программиро- вания, следует обстоятельно ознакомиться со справочным руковод- ством по программированию. Весьма вероятно, что вы найдете там некоторые операторы языка программирования, которые не упот- ребляли, или обнаружите, что использовали не все возможности многих операторов. Перечитывание справочного руководства под- толкнет вас к применению новых способов программирования, а может, и к созданию более эффективных программ. 3.20. ПРЕДУПРЕЖДАЮЩИЕ СООБЩЕНИЯ Часто не принимаемый во внимание способ резкого повышения эффективности компилирования и выполнения программы состоит в удалении каких-либо условий, вызывающих предупреждающие сообщения. Так как предупреждающие сообщения не мешают вы- полнению программы, существует тенденция игнорировать их, по- скольку они значительно снижают эффективность. Каждый раз при выполнении компилирования на выдачу таких сообщений тратится время, что ведет к потере времени выполнения программы и рас- ходу памяти.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 163 В ФОРТРАНе, например, переменные, используемые в операто- рах COMMON, следует надлежащим образом выравнивать. Если они не выравнены, можно потерять значительное количество вре- мени выполнения программы. В этом случае дается предупрежда- ющее сообщение, которое часто игнорируется. Исправление программы с целью исключения предупреждаю- щих сообщений на этапе компилирования является, вероятно, са- мым простым и самым выгодным способом повышения эффектив- ности выполнения программы. Поэтому программисты, заинтересо- ванные в эффективности выполнения, сначала должны проверить, не могут ли они исключить какие-либо предупреждающие сообще- ния. (Предупреждающие сообщения даются часто при выполнении выравнивания и преобразований.) 3.21. ЗАГРУЗОЧНЫЕ МОДУЛИ Загрузочные модули — это программы, получаемые в результа- те компилирования. Их следует использовать во всех рабочих про- граммах. Как только программа отлажена и тестирована, для ее дальнейшей работы можно создать загрузочный модуль. Использо- вание загрузочных модулей на дисках позволяет экономить время, исключая процессы компилирования и установления связей. Ком- пилирование исходной программы при каждом прогоне является трудоемким процессом и обычно не требуется. Используйте загрузочные модули. 3.22. МОДУЛИ Поскольку целью настоящей главы является всестороннее рас- смотрение эффективности программ, следует обсудить основные не- достатки использования модулей, влияющие на эффективность. Я полагаю, однако, что множество преимуществ (удобочитаемость, модифицируемость, пригодность к отладке и тестированию) мо- дульного подхода перевешивают эти недостатки. Создание модулей облегчает написание программы, ее отладку, тестирование и модификацию, и, таким образом, уже на стадии написания можно сэкономить большое количество времени про- граммирования и машинного времени. Это очень важно, так как на некоторые программы затрачивают больше машинного време- ни на стадии их разработки, чем на стадии выполнения. Подпрограммы можно разрабатывать, отлаживать и тестиро- вать независимо. В результате этого можно сэкономить машинное время, потому что с увеличением длины программы время компи- лирования увеличивается быстрее, чем линейно. Недостатком использования подпрограмм является то, что они могут стоить большого объема памяти. За подпрограммы нужно 11-
164 ГЛАВА 3 расплачиваться и памятью, и временем, так как при работе с ни- ми необходимы команды связи, устанавливающие соответствие между вызывающей программой и подпрограммами. Для этого не- которые компиляторы КОБОЛа выделяют для каждой подпро- граммы дополнительную память объемом 1К. Но память стоит расходовать только в том случае, если к подпрограмме обраща- ются более чем из одного места вызывающей программы. Подпрограмма, к которой обращаются только из одного места программы, будет выполняться более эффективно, если располо- жить ее тело в точке вызова. Последовательное программирование исключает команды связи и экономит, таким образом, и время, и память. Вы должны решить, что вас интересует в первую очередь — эко- номия времени или памяти, поскольку экономия одного ресурса влияет на использование другого. Если вас в первую очередь инте- ресует экономия времени выполнения, то не следует использовать подпрограммы и программу надо писать последовательно. Но если экономия памяти стоит на первом месте, то повторно используе- мые части программ следует рассматривать как подпрограммы. Если подпрограмме нужно передать список аргументов, на это понадобятся дополнительные время и память. Адреса аргументов следует передать из вызывающей программы и связать их с коман- дами подпрограммы, которые будут затем использовать эти адре- са. Все это занимает и время, и память. Если список аргументов можно исключить или сократить, полу- чится значительная экономия. Обычный способ исключения или сокращения списка аргументов состоит в использовании команд, которые разместят эти переменные в общей области памяти, где все подпрограммы могут найти свои переменные. Поэтому, если переменной в подпрограмме всегда соответству- ет одна и та же переменная в основной памяти при каждом обра- щении к подпрограмме, то переменную следует расположить в об- щей области, которой присвоено имя. Однако если переменной в. подпрограмме соответствуют различные переменные при разных обращениях к ней из вызывающей программы, то переменная должна входить в список аргументов. Если аргумент является кон- стантой, его можно объявить переменной и поместить в общую область. Обычно все переменные стремятся поместить в список ар- гументов. Этого следует избегать, так как общая память гораздо более эффективна. При использовании общей памяти экономятся и память, и время. Еще один способ сокращения числа элементов в списке пара- метров заключается в объединении нескольких параметров в одну область памяти. Например, модуль, создающий заголовки подпро- граммы, может иметь некоторое количество данных, таких, как да- та, счетчик страниц, метки заголовков, передаваемых как пара- метры. Если эти данные объединить в одну область, то можно со-
ЭФФЕКТИВНОСТЬ ПРОГРАММ 165 кратить число параметров. Языки, подобные ПЛ/1 и КОБОЛу, име- ют структуры данных, обеспечивающие объединение нескольких параметров. 3.23. ИСПОЛЬЗОВАНИЕ СВЕДЕНИЯ О МАШИНЕ И КОМПИЛЯТОРЕ Каждая машина и каждый компилятор имеют некоторые осо- бенности, изучение и использование которых позволит более эф- фективно компилировать и выполнять программу. Информацию такого типа можно получить, изучая листинги ассемблера. Например, в одном широко распространенном компиляторе ФОРТРАНа сделано так, что если номера операторов распределе- ны равномерно по всему диапазону, то программа будет компили- роваться быстрее. Эта ситуация возникает потому, что компилятор для своих внутренних потребностей располагает номера операто- ров в таблицы, состоящие из строк, в которых во время компили- рования выполняется многократный поиск. Если количество входов в каждую строку примерно одинаково, сокращается среднее вре- мя, необходимое для нахождения номера оператора. Номера операторов записывают в пяти строках таблицы соглас- но последней цифре номера оператора. Номера операторов, окан- чивающиеся на 0 или 1, размещаются в первой строке, на 2 или 3 — во второй строке, на 4 или 5 —в третьей и т. д. Таким обра- зом, если номера операторов распределены равномерно, сокраща- ется время компилирования». Подобная ситуация имеет место и с именами переменных. Име- на записываются в строки в соответствии с длиной имени перемен- ной. Имена, имеющие длину в один символ, записываются в пер- вой строке, имена из двух символов — во второй строке и т.д. Та- ким образом, если имена распределены довольно равномерно по разным строкам, затрачивается меньше времени на нахождение каждого имени. Изложенные здесь замечания показывают, что, если эффектив- ность очень важна, вы должны знать не только общие приемы, позволяющие ее повысить, но также и обладать некоторыми све- дениями о машине, ее компиляторе и операционной системе. 3.24. ЗАКЛЮЧЕНИЕ Кажется очевидным, что написание эффективных программ бу- дет иметь существенное значение в обозримом будущем. Действи- тельно, написание эффективных программ вновь стало актуаль- ным с появлением мини-ЭВМ. Создание программ, эффективно ис- ” Подробнее метод расстановки см., например, в книге: Грис Д., Конструи- с°247Ие К0МПИЛЯТ0Р0В Для цифровых вычислительных машин. — М.: Мир, 1975,
166 ГЛАВА 3 пользующих память, приводит к необходимости сегментировать программу для оверлейности, применяя ассемблер и ориентируясь на более мощную машину, без чего данная задача не может быть решена. Программы, которые не эффективно используют машинное вре- мя, могут его быстро израсходовать, сократив тем самым число за- дач, которые можно было бы решить. Программы, написанные на языках высокого уровня, часто пи- шут только один раз, а затем в них делают вставки, вносят исправ- ления, добавления, в результате чего они расширяются до таких размеров, что имеющейся памяти уже не хватает. Раньше, если программа не могла поместиться в памяти машины, решение было всегда одно — перейти на более мощную машину. Однако памяти всегда недостаточно. Применение методов, подобных тем, которые обсуждались в этой главе, позволит вам получать более быстрые и меньшие по объему программы. В противном случае слишком мед- ленные или слишком большие программы будут постоянно «достав- лять беспокойство» вычислительной системе. 3.25. СОВЕТЫ ПРОГРАММИСТУ Если программа неправильна, не имеет значения, какова ее эффективность. Определяйте требования к эффективности программы на стадии проектирования. Удобочитаемость программы обычно более важна, чем эффек- тивность. Используйте оптимизирующий компилятор. Профилируйте ваши программы. Инициируйте переменные во время компилирования. Избегайте смешанных типов данных. Оптимизируйте сначала внутренние циклы. Используйте для индексации наиболее предпочтительный тип данных. Группируйте записи в эффективные блоки для ввода-вывода. Испол'ьзуйте загрузочные модули. 3.26. УПРАЖНЕНИЯ ПОВТОРЕНИЕ ПРОЙДЕННОГО 1. Что является наиболее важным при написании эффективных программ? 2. Какой тип программ следует оптимизировать? Какой тип программ оптимизировать не надо?
ЭФФЕКТИВНОСТЬ ПРОГРАММ 167 3. Почему оптимизирующий компилятор необходим даже в том случае, когда программист выполнил оптимизацию? 4. Что означают понятия: оверлейность программ, свертка, уменьшение силы операции, исключение повторяющихся выраже- ний, исключение циклов, исключение инвариантных выражений, развертка цикла, критическая область, локализованность? 5. В каком случае использовать цикл менее эффективно, чем программировать последовательно? 6. Как следует располагать вложенные циклы, чтобы сократить число инициирований и проверок цикла? 7. Какие области программы наиболее выгодны для оптими- зации? 8. Расположите нижеуказанные операции по скорости их вы- полнения— от самой быстрой к самой медленной: а) деление, г) извлечение корня, б) сложение, д) возведение в степень, в) умножение, е) вычитание. 9. Запрограммируйте следующие выражения наиболее эффек- тивным способом: , D 4А а) В— 3 б) С = Р**0.5 в) Y = 5x4+3x2—2х+2 т) Т=COS (THETA)—COS (THETA) **2.0 д) PUT=COST/2 *4* К e) T=P/2-J-(6—R)/4—T/2 ж) Y = 6-(-T** 5.0 з) Т=2* PI/4 и) IF(A<B) OR (C>D) THEN X = 4 ELSE Y = 0; ЗАДАНИЯ 10. Имеется ли оптимизирующий компилятор, пригодный для вашего языка программирования? Если да, то какие типы оптими- зации он выполняет? И. Какие режимы компилятора используются на вашей машине для ускорения компилирования во время отладки? 12. Какие режимы компилятора используются на вашей маши- не для ускорения выполнения программы? 13. Имеется ли в вашем компиляторе версия, минимизирую- щая использование объема памяти в объектной программе за счет снижения скорости выполнения? Можно ли повысить скорость вы- полнения за счет памяти? 14. Выберите два различных оператора в вашем языке програм- мирования и разработайте тесты, выявляющие, какой оператор выполняется быстрее.
168 ГЛАВА 3 15. Получите листинг ассемблера одной из ваших программ, записанных на языке высокого уровня. Проверьте по листингу ас- семблера, какие операторы программы создают самые эффектив- ные коды. 16. Опишите эффективный алгоритм нахождения всех множи- телей числа. Например, множителями числа 12 являются 1, 2, 3, 4, 6 и 12. Если вы не уверены, что это наиболее эффективный алго- ритм, напишите для него программу. 17. Игра в шахматы. Напишите алгоритм расположения мак- симального количества коней на шахматной доске таким образом, чтобы ни один конь не мог угрожать другому. 18. Какие шаги можно предпринять при компилировании с язы- ка программирования, используемого вами, для ускорения выпол- нения компилируемой программы? 19. Какой тип переменных наиболее эффективен для использо- вания в качестве индексов в вашем языке программирования? 20. Какой тип переменных наиболее эффективен для проведе- ния многочисленных вычислений с нецелочисленными переменны- ми в вашем языке программирования? 21. Как ваш компилятор обрабатывает приведенное ниже выра- жение? В=3.0/4.0*В Выражение 3.0/4.0 вычисляется во время компилирования или во время выполнения? Посмотрев программу на ассемблере, мож- но получить ответ. 22. Каким образом вы организуете оверлейность программ на языке программирования, который вы используете? 23. Имеются ли способы задания эквивалентности в вашем язы- ке программирования? Как следует описать два массива, чтобы они использовали одни и те же ячейки памяти? 24. Каким образом вы можете инициировать в вашем языке программирования переменные (массивы) во время компилирова- ния, а не во время выполнения? 25. Сохраняет ли ваш компилятор константы в форме, не тре- бующей преобразований во время выполнения? 26. Какое время выполнения на вашей машине каждой из ни- жеследующих операций (просмотрите машинные команды): а) сложения целых чисел, б) сложения вещественных чисел, в) умножения целых чисел, г) умножения вещественных чисел, д) деления вещественных чисел. 27. Получите листинг на ассемблере для программы на языке высокого уровня. Какие операторы представляются вам неэффек- тивными?
ЭФФЕКТИВНОСТЬ ПРОГРАММ 169 28. Использует ли ваша машина двойную буферизацию для опе- раций ввода-вывода? Можете ли вы запросить большее или мень- шее количество буферов? Если да, то попытайтесь изменить коли- чество буферов и посмотрите, будет ли изменяться время, исполь- зуемое программой. 29. Найдите, программу, которую нужно оптимизировать, и по- пытайтесь сделать это, используя методы, обсуждаемые в этой главе. 30. Можете ли вы придумать методы оптимизации вашей про- граммы, которые не обсуждались в этой главе? ПРОГРАММЫ 31. Напишите программу, которая должна прочесть первые N положительных целых чисел и найти их сумму. 32. Сложные проценты. Предположим, что сделан вклад 100 долл, на 55 лет с ежеквартальной прибылью 6%. Найдите суммарную прибыль. 33. Предположим, что 1, J, К и L — целые положительные чис- ла, меньшие 20. Напишите программу нахождения значений, удов- летворяющих уравнению' Z« + J3 + №=L3. Программа должна быть как удобочитаемой, так и эффектив- ной. Сравните свое решение с решением вашего коллеги. 34. Задачи сортировки являются хорошими тестами для выбора алгоритма и методов эффективности. Считайте N чисел, затем вы- полните их сортировку в возрастающей последовательности и на- печатайте номера членов последовательности, полученные в резуль- тате сортировки. Если у вас есть функция — генератор случайных чисел, используйте ее для получения 1000 случайных чисел и вы- полните их сортировку. Сравните вашу программу с какой-либо другой программой сортировки и посмотрите, чья программа быст- рее. Информацию об алгоритмах сортировки вы можете почерп- нуть из книги Д. Кнута1). 35. Табличный поиск. Составьте таблицу из 1000 случайных чи- сел, значения которых расположены между 1 и 10000. Напишите программу, которая считывает положительные целые числа, мень- шие 10 000, и определяет, имеются ли эти числа в таблице. Вы можете составить таблицу и организовать поиск в ней любым способом, который вам нравится. 36. Диагональная матрица2^. Все элементы диагональной матри- цы равны нулю, кроме тех, которые расположены по главной диа- р .. Knuth D. Е., The Art of Computer Programming, Vol. 3, Addison-Wesley, „no SR» Mass., 1973. [Имеется перевод: Кнут Д., Искусство программирования для ЭВМ, т. З. — М.: Мир, 1978.] Для этой мзтричной задачи придумайте сначала эффективный способ хра- нения матрицы. г
170 ГЛАВА 3 гонали. Напишите программу, которая перемножает две диаго- нальные матрицы. 37. Симметрическая матрица. Симметрическая матрица иден- тична матрице, полученной при ее транспонировании, т. е. для лю- бого элемента ац=а^. Напишите программу чтения и перемноже- ния двух симметрических матриц. Эффективна ли ваша програм- ма? 38. Треугольная матрица. Элементы нижней треугольной матри- цы равны нулю выше главной диагонали, т. е. а<3 = 0, если />/. Напишите эффективную программу для сложения двух больших нижних треугольных матриц. 39. Тетраэдная матрица. Тетраэдная матрица является трех- мерной треугольной матрицей. После того как вы представите се- бе, как она выглядит, напишите программу, которая читает ее эле- менты и эффективно работает с ними. 40. Трехдиагональная матрица. Трехдиагональная матрица со- держит нулевые элементы везде, кроме трех главных диагоналей, т, е. наддиагонали, диагонали и поддиагонали: ау=0, если |г—/|> 1. Напишите эффективную программу сложения двух трехдиагональ- ных матриц. 41. Повторяющиеся элементы. Предположим, у вас есть боль- шая прямоугольная матрица. Некоторые столбцы матрицы иден- тичны. Разработайте такой способ запоминания матрицы, чтобы одинаковые столбцы запоминались не более одного раза. Пусть имеются две такие матрицы одинакового размера. Сложите эти матрицы. Совсем необязательно, что одни и те же столбцы этих матриц одинаковы. 42. Разреженная матрица. Предположим, что у вас есть две очень большие матрицы (1000X1000 элементов каждая) и что большинство элементов этих матриц (95%) равно нулю. Приду- майте способ эффективного хранения матриц. Затем напишите программу сложения и умножения этих матриц. 43. Если предыдущие задачи для вас слишком легкие, попытай- тесь выполнить их для разреженной симметрической матрицы или для разреженной треугольной матрицы. Существует ли разрежен- ная симметрическая или треугольная матрица? 44. Напишите программу, которая находит наименьшее и наи- большее значения в векторе с п элементами. Определите минималь- ное число сравнений, необходимых для этого вектора. 45. Напишите программу, которая изменяет порядок элементов в векторе на обратный. Определите минимальное число изменений, которые нужно сделать для вектора с га элементами. 46. Позволяет ли система, на которой вы работаете, читать по- казания датчика времени? Как вы используете эти показания и какое время вы получаете (астрономическое время, время, прошед-
ЭФФЕКТИВНОСТЬ ПРОГРАММ 17! шее с момента запуска датчика, машинное время). Можете ли вы измерить время выполнения подпрограммы? 47. Числа Армстронга. Число из п цифр является числом Арм- стронга, если сумма цифр, возведенных в n-ю степень, равна са- мому числу. Таким числом является, например, число 153: 153= 184-53 + 33 Числом Армстронга из четырех цифр является 1634: 1634 = 14 + 6*+34 + 44 Найдите все числа Армстронга, состоящие из двух, трех и четырех цифр. Напишите эффективную программу нахождения этих чисел. ПРОЕКТЫ 48. Волчий остров размером 20X20 заселен дикими кроликами, волками и волчицами. Имеется по нескольку представителей каж- дого вида. Кролики довольно глупы: в каждый момент времени они с одинаковой вероятностью 1/9 передвигаются в один из вось- ми соседних квадратов (за исключением участков, ограниченных береговой линией) или просто сидят неподвижно. Каждый кролик с вероятностью 0,2 превращается в двух кроликов. Каждая волчи- ца передвигается случайным образом, пока в одном из восьми со- седних квадратов не окажется кролик, за которым она охотится. Если волчица и кролик оказываются в одном квадрате, волчица съедает кролика и получает одно «очко». В противном случае она теряет 0,1 «очка». Волки и волчицы с нулевым количеством «оч- ков» умирают. Волк ведет себя подобно волчице до тех пор, пока поблизости не исчезнут все кролики; тогда если волчица находит- ся в одном из восьми близлежащих квадратов, волк гонится за ней. Если волк и волчица окажутся в одном квадрате и там нет кролика, которого нужно съесть, они производят потомство слу- чайного пола. Запрограммируйте предлагаемую экологическую мо- дель, понаблюдайте за изменением популяции в течение некоторо- го периода времени. (Очень благодарен Биллу Мак-Кимэну за эту задачу.) 49. Вышеописанная модель по существу неустойчива: Волчье- му острову суждено стать пустыней. Добавьте изгородь (запрет- ную зону для волков) и понаблюдайте за результатами. 5(к Поиск ключевых слов. У вас есть список названий книг и статей. Ваша программа должна читать ключевые слова и опреде- лять, находятся ли соответствующие им названия книг и статей в списке. Названия, содержащие ключевые слова, печатаются. У вас так много названий литературных источников, что вы не можете хранить их одновременно в оперативной памяти. Вам предо- ставляется возможность организовать поиск и расположить наз- вания любым желаемым способом, но если программа выполняет- ся слишком медленно, проект будет отвергнут, а вас уволят,
172 ГЛАВА 3 51. Поиск телефонного номера. Ваша телефонная компания по- лучает много запросов от клиентов, которые хотят узнать адрес и фамилию владельцев телефонных номеров. Им отвечали на запро- сы по справочнику, но теперь передали эту работу вам, поскольку вы сказали, что можете выполнять ее эффективнее на машине. Начните с небольшого списка телефонных номеров, но проектируй- те систему таким образом, чтобы она могла значительно расши- ряться. Информация об абонентах может либо добавляться в файл, либо уничтожаться. 52. Напишите программу чтения карт у пяти игроков в покер.' То есть сдайте по пять (или по семь) карт каждому на руки, ука- жите, у какого игрока карты лучше (двойка, каре, флешь и т. д.). 53. Ханойская башня. Ханойская башня состоит из трех осей А, В и С, укрепленных на платформе, и п дисков разных разме- ров. Вначале все диски, упорядоченные по размеру; расположены один над другим на оси А, причем самый большой диск находится внизу. Ваша программа должна показать, как переместить диски с оси А на ось В по одному с условием, что ни один диск нельзя расположить на меньшем диске. Ось С может использоваться как вспомогательная. Сколько нужно сделать ходов для башни из п дисков? 54. Игра в палочки1'1. Напишите программу для игры в палоч- ки. Программа должна читать следующие данные: а) общее коли- чество палочек; б) количество палочек в каждом ряду; в) макси- мальное количество палочек, которые можно переложить за один ход. Хорошо составленная программа должна выигрывать в явно «опасной» ситуации. 55. На изображенной ниже игральной Доске имеются фишки во всех отверстиях, кроме центрального. За один ход вы можете пе- рескочить через одну фишку, и каждый раз такая фишка подле- жит удалению с доски. Конечная цель игры — одна фишка в цент- ральном отверстии.' Составьте программу успешного окончания игры, хотя это и достаточно трудно. D См. гл. 14 «Ним и так-тикс» в книге: Гарднер М., Математические голове» ломки и развлечения. — М.: Мир, 1971, с. 132.
ЭФФЕКТИВНОСТЬ ПРОГРАММ 173 56. Кубики сома**. Вы все, вероятно, пытались играть в эту иг- ру* 2). Это куб, состоящий из 27 маленьких кубиков, которые склее- ны по граням так, что образуют семь элементов. Сначала вы его разобрали, а собрать опять трудно. Напишите программу соедине- ния элементов куба. Прежде чем пытаться составить программу, попробуйте собрать такой куб. Посчитайте, сколько имеется раз- личных решений. 57. Разноцветные кубики**. У вас имеется четыре кубика, сто- роны которых окрашены в разные цвета. Составьте из этих куби- ков прямоугольную призму, каждая боковая грань которой раск- рашена во все четыре цвета. Напишите программу, определяю- щую расположение разноцветных кубиков. 58. Задача о восьми ферзях4*. Имеется шахматная доска 8X8 и восемь ферзей. Найдите такую позицию для каждого ферзя, чтобы ни один из них не угрожал другому, т. е. чтобы каждый ряд, столбец и диагональ содержали не более одной фигуры. Найдите все решения. 59. Для нижеследующих задач запрограммируйте эффектив- ный алгоритм и оцените необходимые усилия, которые нужно при- ложить для решения задачи. Решение может быть очень трудным или даже невозможным. а) Найдите два целых числа /пип, таких, что /п2=2п2. б) Найдите несколько целых значений для х, у, г, если х3+ +у3+г3=1. Два решения известны: х =//=!, —1, Х=у=—4, 2=5, в) Найдите все решения уравнения х3—у2 = 18, если х и у — целые числа. г) Имеется ли решение уравнения xn-j-yn—zn для целых чисел п, х, у, z, если п>2? д) Составьте карту таким образом, чтобы, используя для ее раскраски более четырех цветов, не раскрашивать двух смежных участков одинаковым цветом. е) Напишите программу создания лабиринта, имеющего только один выход. ’> В этой и двух следующих задачах решение может быть получено только при использовании эффективного алгоритма. Просмотрите работу МсКеегиап W. М., A Formal Model for Space-Filling Puzzles. Machine Intelligence, Vol. 8, D. Michie, T. Elcock (eds.), 1977. 2) Cm. гл. 21 «Кубики сома» в книге: Гарднер М., Математические голово- ломки и развлечения. — М.: Мир, 1971, с. 196. — Прим, пер ев. ,4) См. гл. 8 «Несколько нечисловых задач» в книге: Дрейфус М. и Ган- глоф К., Практика программирования на ФОРТРАНЕ. — М.: Мир, 1978, с. 73.— Прим, перев.
174 ГЛАВА 3 Подобные задачи и решение задачи с четырьмя цветами смот- рите в журнале Scientific American1*. ЛИТЕРАТУРА 1. Aho A., Hopcroft J., Ullman J., Design of Efficient Algorithms, The Design and Analysis of Computer Algorithms, Reading, Mass., Addison-Wesley, 1974. [Имеется перевод: Ахо А., Хопкрофт Дж., Ульман Дж., Построение и ана- лиз вычислительных алгоритмов. — М.: Мир, 1979.] 2. Allen F. Е., Program Optimization, Annual Review in Automatic Programming, Vol. 5, New York, Pergamon Press, 1969. 3. Cohn L. S., Effective Use of ANS COBOL Computer Programming Language, New York, Wiley, 1975. 4. Ignalls D. H., FETE: A FORTRAN Excution Time Estimator, Available from National Technical Information Service, Springfield, V,a. 22151. Document num- ber PB-198 510, 1971. 5. Knuth D. E., An Empirical Study of FORTRAN Programs, Available from Na- tional Technical Information Service, Springfield, Va. 22151, 1970. 6. Kreitzberg, С. B., Shneiderman B., The Elements of FORTRAN Style, New York, Harcourt Brace Jovanovich, 1972. 7. Morrison J. E., User Performance in Virtual Storage Systems, IBM. System Journal, No. 3 (1973). 8. Optimization and Efficient Performances, IBM System/360 Operating System PL/(F) Language Reference Manual, IBM Corporation. 9. Programming Considerations, IBM OS FORTRAN IV Compiler Programmer’s Guide, C28-6817. 10. Programming Techniques, IBM OS Full American National Standard COBOL, C28-6456. 11. Rustin R. (Ed.), Design and Optimization of Compilers, Englewood Cliffs, N. J., Prentice-Hall, 1972. 12. Samet P. A., Detailed Analysis of a Program — An Instructive Horror Story, Software — Practice and Experience (April —June 1975). 13. Zelkowitz M. V., Bail W. G., Optimization of Structured Programs, Software — Practice and Experience (January — March 1974). !) Gardner M., Mathematical Games, Scientific American (April 1975).
Одно из затруднений, связанных со скры- тыми дефектами программ, заключается в том, что вероятность их проявления возрастает со временем и с расширением масштабов использования программы. Программа, свободная от ошибок, есть абстрактное теоретическое понятие. ... Возмездье Рукой бесстрастной чашу с нашим ядом Подносит нам же... Шекспир. Макбет Глава 4 ОТЛАДКА ПРОГРАММ Мало в какой области деятельности имеется столько возмож- ностей для ошибок, как в программировании. Искусство локали- зации таких ошибок, когда факт их существования установлен, но- сит название отладки. Таким образом, отладка программы пред- полагает обязательное наличие той или иной ошибки; в противном случае мы имеем дело с тестированием. Одним из критериев профессионального мастерства программи- стов является их способность обнаруживать и исправлять собст- венные ошибки: начинающие программисты не умеют этого делать, у опытных программистов это не вызывает затруднений. Тем не менее ошибки в программах делают все. Программистов часто учат программированию, но редко — от- ладке. А поскольку отладка программы поглощает больше време- ни, чем ее первоначальное написание, и является делом более трудным, приходится только сожалеть, что обучению искусству от- ладки не уделяют должного внимания. В общем времени разра- ботки программы отладка занимает, по оценкам специалистов, от 50 до 90%. Написание программы схоже с написанием отчета: и в том и в другом случае необходимы предварительный вариант (до выявления ошибок) и окончательный вариант (после их устра- нения). Редко бывает так, чтобы предварительный вариант отчета или программы стал сразу и окончательным вариантом. По вопросам отладки программ имеется сравнительно немного работ. Отладку обычно называют искусством, с тем чтобы избе- жать решения трудной задачи обучения этому делу. Процесс отладки зависит от условий функционирования про- граммы, т. е. от используемой машины, языка программирования, операционной системы, специфики самой задачи и даже от особен- ностей конкретной программы. Можно с определенностью утверждать, что каждый язык, ком- пилятор и машина связаны с определенными типами дефектов программ и ошибок программирования. Простейшим примером яв-
176 ГЛАВА 4 ляются синтаксические ошибки, которые в очень сильной мере предопределяются конкретным языком программирования. Одна- ко мы располагаем весьма немногочисленными данными относи- тельно взаимосвязи между тем или иным языком, возможным ко- личеством ошибок и степенью сложности их обнаружения. Данная глава касается вопросов отладки программ, написан- ных на том или ином входном языке. Я питаю глубочайшие сим- патии к тем, кому нравится читать листинги и программировать выполнение арифметических операций в шестнадцатеричном или восьмеричном коде, однако использование, некоторого входного языка дает, в частности, то преимущество, что позволяет избежать изучения языка конкретной машины. Отладка программы, напи- санной на машинном языке, неудобна тем, что зависит от конкрет- ной машины, и изменения, вносимые в ЭВМ в связи с ее модифи- кацией или добавлением новых операций, могут обесценить всю информацию, записанную на машинном языке. Я глубоко убежден, что профессиональное обучение программистов должно быть в максимальной степени устойчиво к технологическим изменениям, и именно поэтому более важно научить отладке программ на вход- ных языках, чем на машинных. Применение машинного языка и отладка программ с использованием ее распечатки желательны лишь тогда, когда все остальные средства нё приводят к требуемо- му результату. Преимущества отладки программ на входных языках заключа- ются в следующем: 1) нет необходимости знать машинный язык; 2) результат отладки может быть выведен на печать в удобо- читаемой форме вместе с идентифицирующими метками; 3) для отладки программы возможно использование средств, предусмотренных входным языком. 4.1. РАЗЛИЧИЕ МЕЖДУ ОТЛАДКОЙ И ТЕСТИРОВАНИЕМ Многие программисты путают отладку программ с тестирова- нием, предназначенным для проверки их работоспособности. От- ладка имеет место тогда, когда программа со всей очевидностью работает неправильно. Поэтому отладка начинается всегда в пред- видении отказа программы. Если же оказывается, что программа работает верно, то она тестируется. Часто случается так, что пос- ле прогона тестов программа вновь должна быть подвергнута от- ладке. Таким образом, тестирование устанавливает факт наличия ошибки, а отладка выявляет ее причину, и эти два этапа разработ- ки программы перекрываются. На каждый из них должно специ- ально выделяться время, с тем чтобы подчеркнуть необходимость выполнения обоих указанных этапов создания программы. Начинающие программисты часто полагают, что программу до- статочно только отладить. Это означает предположение, что про-
ОТЛАДКА ПРОГРАММ 177 грамма, правильно функционирующая на специально подобран- ном массиве данных, будет так же хорошо работать и при любом другом массиве. И поэтому они бывают удивлены, когда после- многократного практического использования машинных результа- тов, не подвергавшихся сомнению, программа выдает явно невер- ную информацию. Естественно, что при этом приходится усомнить- ся и в достоверности всех предшествовавших результатов. Известны два подхода к отладке программ: при использовании одного из них значительная доля времени работы программиста затрачивается на то, чтобы попытаться избежать ошибок в про- грамме, а имеющиеся все же ошибки обнаружить вручную; второй. , подход предполагает максимальное использование ЭВМ для выяв- 1 ления ошибок. Выбор того или другого способа действий зависит от количества имеющегося в распоряжении программиста машин- ного времени. Наблюдается естественное стремление (определяе- мое человеческой ленью) перекладывать большую часть работы, по отладке н'а' машину. Однако лучше всего избегать возникнове- ния ошибок. Существует еще и третий подход к отладке, при котором она совмещается с процессом написания программы. Некоторые про- граммисты предпочитают писать программу по частям, сразу пы- таясь проверить, работает ли каждая написанная часть должным образом. Такие программисты пишут, отлаживают и тестируюг программы по ходу программирования. Было бы интересно выяс- нить, какой из рассмотренных подходов к программированию и отладке является наиболее эффективным. Описанный поэтапный процесс создания программы позволяет отыскивать ошибки в ходе ее кодирования. Это облегчается тем, что программа еще свежа в памяти программиста, и дефекты вы- являются легче. Такой подход аналогичен разбиению программы на стандартные подпрограммы. F । 4.2. ОТЛАДОЧНЫЙ БАРЬЕР Вероятно, проблема отладки программ является очередным барьером на пути развития вычислительной техники, который предстоит преодолеть в будущем. В начале 50-х годов ограничи- , вающим фактором этого развития были технические средства. Программист старался втиснуть свою' программу в память ем- костью 4К и писал ее так, чтобы гарантировалось наличие необхо- димых машинных ресурсов при каждой очередной загрузке про- граммы для выполнения. Вслед за этим началась эра системного , программного обеспечения, знаменовавшаяся появлением ныне широко применяемых алгоритмических языков АЛГОЛ, ФОРТРАН, КОБОЛ, ПЛ/1 и др. - Теперь мы имеем большие быстродействующие машины, обес- печивающие выполнение сложных программ наиболее эффектив- 12—899
178 ГЛАВА 4 ным способом, равно как и вспомогательные программные средст- ва, делающие возможным написание таких программ. Узким ме- стом стала в настоящее время их отладка. Кое-кто полагает, что по мере усложнения машинных языков и компиляторов ошибки в программах должны исчезать. При этом, однако, игнорируется тот факт, что компиляторы не в состоянии выявлять логические ошибки. В противном случае пришлось бы признать, что компилятору известны замыслы разработчика про- граммы, а значит, возможно и ее автоматическое генерирование. Программы стали больше и сложнее, однако уровень ошибок в них остается примерно постоянным. 4.2.1. ИЗ ИСТОРИИ ОШИБОК Бытует мнение, что первая программная ошибка была обнару- жена на заре развития ЭВМ, когда в Массачусетском технологи- ческом институте окончилась неудачей попытка запуска машины Whirlwind I. Неистовая проверка монтажа, соединений и оборудо- вания не выявила никаких неисправностей. Наконец, уже отчаяв- шись, решили проверить программу, представлявшую собой ма- ленькую полоску бумажной ленты. И ошибка была обнаружена именно в ней — в этом программистском ящике Пандоры1), из которого на будущие поколения программистов обрушились беды, связанные с ошибками программ. 4.3. ОШИБКИ В ОПИСАНИИ ЗАДАЧИ Обычно получается так, что, когда программа написана, поль- зователь обнаруживает несоответствие выдаваемых ею результа- тов желаемым. Подобная ситуация может складываться как из-за отсутствия взаимопонимания между программистами и пользова- телями, так и от того, что пользователи в действительности не знают, чего они хотят. Если при этом неверно работающая про- грамма помогает пользователю понять свои потребности, а прог- раммисту уяснить истинные пожелания пользователя, то еще не все потеряно. Иногда неверные результаты могут послужить толчком к тща- тельному пересмотру первоначальной постановки задачи. Некаче- ственное определение требований к программе приводит к созда- нию программы, которая будет правильно решать неверно сфор- мулированную задачу. В таких случаях, как правило, требуется полное перепрограммирование. При этом необходимо настаивать *> Пандора — в древнегреческой мифологии девушка, созданная из земли и воды богом огня и кузнечного ремесла Гефестом. Она получила от верховного бога Зевса ящик со всеми человеческими несчастьями, которые случайно выпу- стила, приоткрыв из любопытства крышку; отсюда «ящик Пандоры» — источник всяческих бедствий. — Прим, перев.
ОТЛАДКА ПРОГРАММ 179 на составлении нового графика разработки программы, поскольку она должна быть полностью переделана. Не позволяйте неверной программе занимать все ресурсы (время программирования, ма- шинное время и т. п.), так как должна быть создана новая про- грамма, и этот процесс должен быть запланирован вновь. Признаком того, что создаваемая для какого-либо заказчика программа может оказаться не соответствующей его истинным потребностям, служит ощущение неясности задачи. В этом случае во избежание написания «неправильной» программы лучше всего четко зафиксировать, что она должна делать, и привести пример. Письменная регистрация требований к программе заставляет за- казчика собраться с мыслями и дать достаточно точное определе- ние требований. Всякие устные указания являются заведомо нена- дежными и часто приводят к взаимному недопониманию. 4.4. ОШИБКИ В ВЫБОРЕ АЛГОРИТМА Как только задача до конца определена, программист начина- ет искать возможный алгоритм или метод ее решения. К сожале- нию, при этом он может выбрать неподходящий или неэффектив- ный алгоритм, и тогда весь процесс выбора придется повторять сначала. Примером неподходящего выбора может служить принятие ите- ративного расходящегося метода решения некоторого уравнения в то время, когда некоторый другой метод мог бы обеспечить по- лучение точного результата. В качестве примера неэффективного, плохо подобранного алгоритма можно назвать способ решения за- дачи, который дает точный ответ, но требует больших затрат вре- мени. К сожалению, часто плохой выбор алгоритма становится очевидным лишь после его опробования, однако, несмотря на это, все же следует уделять и внимание, и время выбору алгоритма, с тем чтобы впоследствии не приходилось переделывать каждую программу. По методам программирования имеется совсем немно- го книг, и в некоторых из них содержится описание конкретных алгоритмов. Читатель, желающий избежать выбора некорректных алгоритмов, должен быть хорошо знаком с литературой по своей специальности. 4.5. ОШИБКИ АНАЛИЗА Эти ошибки связаны либо с неполным учетом могущих возник- нуть ситуаций, либо с неверным решением задачи. К первому слу- чают относится, например, пренебрежение возможностью появления отрицательных значений переменных, малых и больших вели- чин. Во втором случае обычно имеют место крупные и мелкие ло- гические ошибки, из которых можно назвать: отсутствие задания начальных значений переменных; 12*
180 ГЛАВА 4 неверные условия окончания цикла; неверная индексация цикла; отсутствие задания условий инициирования цикла; неправильное указание ветви алгоритма для продолжения про- цесса решения задачи. , Самый лучший способ организации отладки — это сведение к минимуму необходимости в ней. Вдумчивая разработка функцио- нальной структуры программы, сопровождаемая достаточно под- робной блок-схемой или кратким описанием, обеспечивает усло- вия для лучшего кодирования программы. Блок-схемы помогают избежать многих ошибок, и, кроме того, их можно использовать в качестве вспомогательного средства вы- явления ошибок при отладке. Метод проверки правильности блок- схемы заключается в том, что через несколько дней после ее со- ставления программист повторно обращается к описанию задачи -и набрасывает блок-схему заново. Затем сличают оба варианта. Такой шаг на первый взгляд может показаться пустой тратой вре- мени, однако всякая ошибка на уровне алгоритма может в даль- нейшем обернуться катастрофой и повлечь основательный пере- смотр программы. 4.6. ОШИБКИ ОБЩЕГО ХАРАКТЕРА После того как найден подходящий алгоритм решения задачи, на этапе программирования все же могут появиться ошибки неза- висимо от выбранного языка. Такими ошибками могут быть: ошибки из-за недостаточного знания или непонимания програм- мистом языка программирования или самой машины; примером может служить ситуация, когда используемая команда или опера- тор действуют иначе, чем предполагал программист; эта ситуация может сложиться вследствие каких-либо неизвестных ему измене- ний в системном программном обеспечении; ошибки, допущенные при программировании алгоритма, когда команды, использованные в програ!мме, не обеспечивают последо- вательности событий, устанавливаемой алгоритмом; в качестве примера подобных ошибок можно назвать логические ошибки и ошибки кодирования, когда, например, ставят знак минус там, где. должен быть знак плюс; синтаксические ошибки; ошибки при выполнении синтаксически правильных операто- ров, например деление на нуль или извлечение квадратного корня из отрицательного числа; ошибки, вызванные неверными данными; типичным примером таких ошибок является выполнение некоторой математической операции над массивом буквенной информации. Все указанные ошибки общего характера, за исключением син- таксических, могут быть обнаружены посредством тестирования,
ОТЛАДКА ПРОГРАММ 181 Таблица 4.1 Характерные ошибки программирования Вид ошибок Пример 1. Неправильная постановка задачи Правильное решение неверно сформули- рованной задачи 2. Неверный алгоритм Выбор алгоритма, приводящего к неточ- ному или неэффективному решению задачи 3. Ошибки анализа Неправильное программирование алго- 4. Семантические ошибки ритма Непонимание порядка выполнения 5. Синтаксические ошибки команды Нарушение правил, определяемых язы- 6. Ошибки при выполнении операций ком программирования Отсутствие указаний на ограничивающие условия вычислений (деление на нуль и т. п.) 7. Ошибки в данных Неудачное определение возможного 8. Ошибки в документации диапазона изменения данных Документация пользователя не соответ- ствует действующему варианту про- граммы в результате которого работа над программой возвращается в ста- дию отладки; разновидности рассмотренных выше ошибок и соот- ветствующие примеры приведены в табл. 4.1. Последний из указанных в табл. 4.1 видов ошибок может быть свя- зан либо с самой программой, либо с документацией на нее. Наконец, существует еще один вид ошибок, который называ- ют глитчем (хроническим дефектом). Термин «глитч» применяется для указания того факта, что программы не удовлетворяют неко- торым требованиям, но эти требования не были сформулированы в задании на разработку программ. 4.7. ОШИБКИ ФИЗИЧЕСКОГО ХАРАКТЕРА .Можно назвать восемь типов ошибок, вызываемых неверными действиями программиста: 1. Пропуск некоторых программных карт или строк. з’. того, 4. 5. 6. 7. 8. Перестановка карт или строк программы. Появление лишних карт или строк (например, в результате что оказались не вычеркнутыми скорректированные строки). Отсутствие необходимых данных. Непредусмотренные данные. / Неверный формат данных. Отсутствие операторов управления заданиями. Использование неверной распечатки программы.
182 ГЛАВА 4 Все перечисленные ошибки можно предотвратить, если акку- ратно обращаться с колодами перфокарт. Обнаружить их гораздо сложнее, так как, например, пропуск или перестановка строк про- граммы могут не привести к возникновению легко распознавае- мых синтаксических ошибок. Необходима также последовательная нумерация перфокарт; эта мера позволяет предупредить или выявить большинство оши- бок физического характера, источником которых являются колоды программных перфокарт; кроме того, нумерация карт облегчает их нахождение при отладке программы. Во избежание ошибок в данных они должны тщательно конт- ролироваться в процессе редактирования и выводиться на эхо- печать, что подразумевает распечатывание всей введенной инфор- мации. 4.8. РАЗМЕТКА ПРОГРАММНОЙ КОЛОДЫ Все программные колоды следует снабжать крупными черниль- ными метками, присваивая их каждой подпрограмме следующим образом: 1. На лицевой стороне первой карты проставляются символы НК (начало колоды). 2. Обратная сторона последней карты помечается символами КК (конец колоды). 3. По обрезу колоды пишется имя программы. 4. По обрезу колоды прочерчиваются диагональные либо по- перечные линии. Указанные приемы облегчают идентификацию и нахождение карт в процессе отладки и помогают установить факт перемеши- вания колоды. 4.9. БЕСХИТРОСТНОЕ ПРОГРАММИРОВАНИЕ Большое значение для успешной отладки программы имеют простота и рациональность ее кодирования. Когда программа на- писана аккуратно и логично, легче избежать ошибок или выявить их в случае возникновения. На ранних этапах разработки сложной программы без колебаний переписывайте заново ее громоздкие блоки, если это ведет к упрощению. Следует избегать всевозмож- ных программистских трюков, поскольку чем их больше, тем труд- нее отладка программы для самого автора, а кто-либо другой, этого просто не сможет сделать. По вопросам, связанным с простотой программирования, чита- телю следует вновь обратиться к гл. 1, где шла речь о стиле про- грамм; многие из высказанных там рекомендаций могут помочь вам сделать свои программы более удобочитаемыми и облегчить процесс их отладки.
ОТЛАДКА ПРОГРАММ 183 4.10. ПРАВИЛЬНОСТЬ ПРОГРАММ Любые программы правильны в отношении их логического по- строения только для определенного типа данных. Например, про- грамма нахождения наибольшего общего делителя двух чисел верна лишь в том случае, когда оба числа целые. Если же подать на вход такой программы нуль или дробное число, нормальная ра- бота программы нарушится. Поэтому необходимо четко определять область значений данных, в которой программа способна функцио- нировать, и вводить операторы, позволяющие проверять, находятся ли данные в установленных границах. Чтобы программу можно было применять, прежде всего она должна быть правильной, а нарушение правильности может про- являться двумя способами: либо неверна синтаксическая конст- рукция программы, либо программа выдает неверные результаты. Правильность синтаксиса означает, что должны быть точно сфор- мированы наименования переменных, арифметические и логиче- ские операции должны подчиняться определенным синтаксиче- ским правилам и т. п. 4.11. СИНТАКСИЧЕСКИЕ ОШИБКИ Выявление транслятором синтаксических ошибок представля- ет собой самый важный и безусловно необходимый этап отладки программы. Чем больше ошибок обнаруживается и исправляется на этом этапе, тем легче оказываются отладка и тестирование в дальнейшем. Начинающий программист наивно полагает, что на этапе компилирования выявляются все ошибки. Более опытный и придирчивый программист знает, что многие «хитрые» синтак- сические ошибки транслятору обнаружить не под силу, и поэто- му стремится обеспечить наиболее полное диагностирование таких ситуаций с выдачей в каждом конкретном случае подробной ин- формации об ошибках. Если под синтаксической ошибкой понимать «всякое наруше- ние требований языка программирования», то следует признать, что многие такие ошибки остаются необнаруженными. Это имеет место, например, при отсутствии указания начального значения какой-либо переменной, при наличии ответвления алгоритма в се- редину цикла оператора DO и в случае нарушения правил индек- сации. Результаты таких операций непредсказуемы, и если вы- полнить компилирование программы с такими ошибками, она бу- дет работать самым таинственным образом. Выявление таких ошибок может оказаться делом нелегким. Отсутствие сообщений машины о синтаксических ошибках является необходимым, но не достаточным условием, чтобы считать программу правильной. Обнаружение синтаксических ошибок важно по той причине, что они непременно приводят к затруднениям при выполнении программы. Более того, для компилятора требуется, чтобы эти
184 ГЛАВА 4 ошибки были исправлены еще до начала пробного прогона про- граммы. В качестве примеров синтаксических ошибок в одном опера- торе можно назвать: пропуск необходимого знака пунктуации!; несогласованность скобок; пропуск нужных скобок; неправильное формирование оператора; неверное образование имен переменных; неправильное использование арифметических операторов; неверное написание зарезервированных слов. Примерами синтаксических ошибок, охватывающих взаимодей- ствие двух и более операторов, могут служить: противоречивые команды; отсутствие условий окончания цикла; дублирование или отсутствие меток; отсутствие описания массива; запрещенный переход. Если компилятор детально не анализирует взаимодействия двух или более команд, то нередко указанные выше ошибки оста- ются необнаруженными. Так, например, некоторые компиляторы не выдают программисту сообщений о непредусмотренном входе в цикл оператора DO. Среди ошибок, которые часто выявляются компилятором в ре- зультате контроля синтаксической структуры программы, следует упомянуть неописанные или неправильно описанные переменные, опечатки и использование запрещенных символов. Большинство синтаксических ошибок зависит от применяемого языка и используемой машины, поэтому мало что можно сказать конкретно по поводу способов их исправления в каждом конкрет- ном случае. Однако можно указать некоторые общие принципы, которыми следует руководствоваться. Во-первых, если в программе выявлено очень большое коли- чество ошибок, следует просто исправить очевидные ошибки и повторить прогон. Некоторые сообщения о синтаксических ошиб- ках являются на самом деле ложными, так как вызваны другими синтаксическими ошибками, и поэтому не стоит тратить много времени на то, чтобы разобраться в выданном сообщении, если характер ошибки не очевиден сразу. Ложные сообщения об ошиб- ках особенно часты тогда, когда в программе имеет место какое- то серьезное упущение, например отсутствие описания перемен- ной или массива. Во-вторых, обращайтесь без колебаний к соответствующему руководству по программированию на данном языке: при неявных синтаксических ошибках чтение раздела, касающегося синтаксиче- ских правил для конкретных операторов, обычно приводит к вы- явлению допущенной ошибки.
ОТЛАДКА ПРОГРАММ 185 В-третьих, большое внияние на объем работ по отладке ока- зывает выбор компилятора. Хороший отладочный компилятор может зачастую обеспечить сокращение затрат времени наполо- вину. Специальный отладочный компилятор обнаруживает больше ошибок, чем обычный, поскольку он детально анализирует син- таксическую конструкцию и взаимодействие-команд. Но еще важ- нее то, что в процессе выполнения исходной программы отладоч- ный компилятор производит многочисленные проверки, выявляю- щие переменные, для которых не заданы начальные значения, неправильные индексы и запрещенные переходы. Разумеется, эти дополнительные проверки требуют большего времени, и поэтому процесс компилирования программы обычно осуществляется го- раздо медленнее по сравнению с обычным компилятором. Те пользователи, которые работают с языком ФОРТРАН, уже давно ощутили выгоды применения отладочного компилятора WATFIV. Практика его длительного использования доказала, что при наличии хорошего компилятора продолжительность отладки может быть значительно уменьшена, а ошибки при этом устраня- ются в самом начале создания программы. Однако отладочные компиляторы не поставляются изготовите- лями ЭВМ. в составе их программного обеспечения, их следует приобретать отдельно. Что касается стоимости таких компилято- ров, то здесь оправданны любые затраты, так как если предполо- жить, что отладка занимает 70% рабочего времени программиста и значительную долю машинного времени, то становится ясной быстрая окупаемость затрат на хороший отладочный компилятор. Университет в Ватерлоо поставляет отладочные компиляторы WATBOL для КОБОЛа и WATFIV для ФОРТРАНа. В Корнелль- ском университете имеется подобный компилятор для языка ПЛ/1, который называется PL/С, а фирма IBM располагает для того же языка компилятором PL/1 Checkout. В Стэнфордском университе- те разработан компилятор АЛГОЛ W. Таким образом, все основ- ные языки программирования снабжены отладочными компиля- торами. Применяйте отладочный компилятор. 4.11.1. ОШИБКИ, НЕ ОБНАРУЖИВАЕМЫЕ КОМПИЛЯТОРОМ Существует множество ошибок, которые компилятор не в сос- тоянии выявить, если используемые в программе соответствующие операторы сформированы правильно. Примерами таких ошибок являются: пропуск части программы; переход не на ту ветвь, на которую нужно, после выполнения оператора условного перехода; использование неверного формата в операциях ввода данных; неправильные параметры циклов (начальное состояние, при- ращение или конечное состояние);
186 ‘ ГЛАВА 4 неполная или неправильная индексация массива; выпадение из рассмотрения некоторых возможных ситуаций» связанных с данными и вычислениями. Ниже приводится пример, показывающий почему ошибки ин- дексирования не могут быть выявлены в процессе компилирова- ния. Если некоторый массив насчитывает 10 элементов и индексы вычисляются с использованием переменных, то границы массива могут быть легко нарушены. Но нарушаются они только в процес- се выполнения программы. Так, если задан массив А (10) и I = 4 * К А(1) = ... то при К, большем 2, индекс массива окажется неправильным, од- нако этот факт не может быть установлен в ходе компилирования программы. Среди других ошибок, не выявляемых при компилировании, следует назвать неправильный тип фактического параметра в опе- раторах вызова, т. е. случаи, когда некоторая подпрограмма на- строена, скажем, на целочисленный аргумент, а ей передается дей- ствительное число. Ошибку такого вида компилятор обнаружить не в состоянии, поскольку каждая из подпрограмм может компи- лироваться отдельно и в распоряжении компилятора нет информа- ции, позволяющей проверить, совместимы ли типы аргументов вызываемой и вызывающей программ. Ошибку рассматриваемого вида можно выявить в процессе выполнения программы, либо ес- ли компилятором фиксируется информация относительно типа пе- ременных, либо при использовании хорошего отладочного компиля- тора. Если даже та или иная ошибка остается необнаруженной ком- пилятором, из программы на входном языке все же будет получе- на объектная программа, однако эта программа будет неправиль- но завершаться в некоторой точке. Если же такого непредвиденно- го останова не произойдет, то результатом работы программы окажется выдача неверной информации, но этот факт никак не будет отмечен. Для обнаружения ошибок, пропущенных компиля- тором, служит тестирование программы. Приведем еще несколько типов ошибок, которые могут выяв- ляться (но редко выявляются), компилятором: неиспользуемые метки; неописанные переменные; описанные, но не используемые переменные; неверный тип аргумента функции; неверный тип элемента в операторе формата. Все эти ошибки могут быть обнаружены на этапе компилиро- вания. Некоторые другие ошибки возможно выявить только в ходе
ОТЛАДКА ПРОГРАММ 187 выполнения программы. Ряд компиляторов позволяет генерировать вспомогательные блоки объектных программ (например, блок про- верки диапазона изменения индексов), служащие для последую- щего контроля за ошибками определенного характера при вы- полнении программы. Чем больше имеется таких контрольных блоков, тем менее трудоемкой оказывается работа, связанная с устранением ошибок. 4.12. ВИДЫ ОТЛАДКИ Отладка начинается с того момента, когда перестают выда- ваться сообщения о синтаксических ошибках. В начале процесса отладки надо использовать простые тестовые данные. Если при этом получаются верные результаты, следует переходить к тести- рованию программы посредством более сложных данных. В слу- чае если результаты неверны, возможны следующие ситуации: 1. Синтаксических ошибок нет, но программа не скомпилиро- вана. 2. Программа скомпилирована, работает, но не выдает резуль- татов. 3. Программа скомпилирована, работает, но происходит преж- девременный останов. 4. Программа скомпилирована, работает, но выдает неправиль- ные результаты. 5. Программа зациклилась. СЛУЧАИ 1. КОМПИЛИРОВАНИЕ НЕ ЗАВЕРШЕНО Подобная ситуация встречается сравнительно редко и свиде- тельствует о наличии какой-то принципиальной ошибки в програм- ме. В этом случае обычно появляются сообщения о тех или иных системных ошибках, которые можно использовать в качестве вспо- могательного средства для выявления имеющейся ошибки. Однако, как правило, интерпретация таких сообщений трудна и требует большого практического опыта. При отсутствии навыков работы с конкретной вычислительной системой часто бывает затруднительно выяснить причину проис- шедшего аварийного останова (авоста). Поэтому, если вы не знае- те, что означает конкретный останов, поинтересуйтесь у своих бо- лее осведомленных коллег. Хороший метод накопления опыта в отношении системных остановов состоит в том, чтобы вести книгу их регистрации с указанием выясненных причин. В противном случае легко забыть ситуации, создававшиеся в прошлом, так как в большинстве своем системные остановы — явление редкое. Если же рядом не оказалось никого, кто мог бы вам помочь, лучше всего попытаться изолировать ошибку. Для этого програм- му разбивают на более мелкие самостоятельные сегменты и де-
188 ГЛАВА 4 лают попытку компилировать их по отдельности. В таком сегмен- тированном виде программа сохраняется до момента окончания компилирования всех сегментов. После этого начинается их после- довательная увязка, и сегменты присоединяются до тех пор, пока не выявится факт прекращения компилирования программы; по- следний добавленный сегмент и будет тем блоком, который содер- жит ошибку. Путем тщательного анализа этого дефектного блока или посредством дальнейшего его сегментирования можно в кон- це концов выявить тот оператор, который препятствует компили- рованию программы. Однако применение описанного метода не- желательно, и он должен использоваться лишь в крайних случа- ях: предпочтительнее обратиться за помощью к своим более опыт- ным товарищам по работе. СЛУЧАИ 2. БЕЗРЕЗУЛЬТАТНАЯ РАБОТА ПРОГРАММЫ Когда программа работает, но не выдает никаких результатов, от нее мало проку, хотя это и некий прогресс по сравнению с пер- вым случаем. Такие неполадки могут вызываться какими-либо логическими или системными ошибками. Примером логической ошибки может служить ситуация, когда программа начинает рабо- ту и сразу уходит на ветвь окончания выполнения задания, не сформировав никакого результата для выдачи. Ошибки подобного рода могут быть обнаружены с помощью методов, описанных ни- же при- рассмотрении проблемы обнаружения ошибок. Системная ошибка имеет своей первоосновой некоторую про- граммную ошибку, которая заставляет операционную систему пре- рвать процесс выполнения программы. Сигнал прерывания может исходить от оборудования, самой операционной системы или ском- пилированной программы. При этом обычно сообщается код систем- ной ошибки, а может быть, и некоторая информация о ее характе- ре. Однако сообщения о системных ошибках крайне таинственны и не содержат информации о том, где допущена ошибка. Если же при этом выдан какой-либо результат, то имеется все же какое-то указание на местоположение ошибки, и задача сводится к тому, чтобы сузить «подозрительную» область либо посредством исполь- зования отладочных протоколов, либо с помощью анализа отла- дочных результатов. Оба эти вопроса рассматриваются в следую- щей главе. К программным ошибкам, приводящим к системным ошибкам, относятся деление на нуль, обращение к области данных и вос- приятие их как инструкций программы, неверная индексация мас- сива, переполнение или потеря значимости и др. Обнаружение ошибок в случаях отсутствия результатов рабо- ты программы и незавершенного компилирования иногда может оказаться делом чрезвычайно трудным. Один из возможных здесь подходов — перепрограммирование конкретного сегмента некого-
ОТЛАДКА ПРОГРАММ 189- рЫМ другим способом, приводящим к достижению той же цели. Если характер ошибки не очевиден, указанный подход может представлять собой наилегчайший путь к ее устранению. Схож с ним и другой подход, при котором создается несколько копий не- правильно функционирующего блока, и в них осуществляются раз- личные изменения, сопровождаемые повторными прогонами про- граммы. Этот процесс повторяется до тех пор, пока ошибка не исчезнет или не окажется изолированной. СЛУЧАИ 3. ПРЕЖДЕВРЕМЕННЫЙ ОСТАНОВ Эта ситуация характерна тем, что программа компилируется до конца, начинается ее выполнение, выдаются какие-то результа- ты, но затем работа программы прекращается раньше, чем это должно быть. Налицо явный прогресс по сравнению с двумя пер- выми случаями, и положение дел обычно облегчается наличием некоторой информации на выходе. Благодаря этому здесь можно применять традиционные методы отладки. Ошибки, приводящие к преждевременному прекращению рабо- ты программы и сопровождаемые затем сообщением о системной ошибке, называют взрывами (blowup) или воронками (cratered). Это тяжелые ошибки, так как они не дают возможности продол- жать выполнение программы. Для их выявления могут использо- ваться методы, описанные в разделе, посвященном обнаружению ошибок. СЛУЧАИ 4. НЕВЕРНЫЕ РЕЗУЛЬТАТЫ Когда программа работает, но выдает неверные результаты, опытные программисты всегда считают это удачей. Достижение этой стадии говорит о том, что программа в принципе правильна, а ее логика работает почти точно. Последующие разделы этой гла- вы будут посвящены способам выявления и устранения ошибок, относящихся к случаю 4. СЛУЧАИ 5. ЗАЦИКЛИВАНИЕ Этот вид ошибок, как правило, обнаружить не так уже трудно. Если сразу не удается распознать, в каком цикле имеется ошиб- ка, следует просто добавить в программу операторы печати до и после каждого «подозрительного» цикла. При этом, однако, сле- дите за тем, чтобы указанные операторы не оказались встроенны- ми внутрь цикла: в противном случае на печать Начнут выдавать- ся тысячи строк. Добавленные надлежащим образом операторы печати обеспечат выдачу соответствующих данных, которые пока- жут, из какого цикла при наличии входа не производится выход.
190 ГЛАВА 4 4.13. ОБЩИЕ РЕКОМЕНДАЦИИ При использовании перфокарт перфорируйте программу на кар- тах какого-либо одного цвета. Затем подготовьте отладочные карты другого цвета, чтобы их легко было идентифицировать и удалить по окончании отладки. По этим же соображениям опера- торы, вставленные специально для отладки, следует располагать в отдельных строках программы. Для всех таких отладочных строк помещайте в колонках перфокарты с 73-й по 80-ю какое-ли- бо сообщение типа DEBUG (ОТЛАДКА) или REMOVE (ИСКЛЮ- ЧИТЬ). Можно также использовать поле комментариев и приме- чаний, отмечать там дополнительные строки, подлежащие удале- нию после завершения отладки, что будет ясно уже из листинга исходной программы. Не позволяйте никакой программе излишне расширяться, так как существует прямая нелинейная зависимость между объемом программы и количеством и степенью сложности обнаруживаемых ошибок. С увеличением программы за пределы 50 операторов чис- ло ошибок и степень сложности их обнаружения растут экспонен- циально. (Однако в случае совсем коротких программ складыва- ется ситуация слишком частого их взаимодействия друг с другом, что также может явиться источником неполадок.) Думайте о стадии отладки еще на этапе написания програм- мы на входном языке, с тем чтобы она не оказалась непригодной для отлаживания или тестирования. Приступая к созданию про- граммы, набросайте хотя бы ее примерную блок-схему — это по- могает собраться с мыслями. Часто во время отладки полезно иметь список использованных в программе переменных и констант. Тажой список обычно фор- мируется компилятором и может быть использован для обнаруже- ния ошибок при печати, поскольку позволяет установить факт по- явления непредусмотренных наименований переменных. Возникающие затруднения следует четко разделять и устра- нять строго поочередно. Если внести сразу несколько изменений, то при появлении новых трудностей вы не сможете определить, ка- кое из произведенных изменений послужило их причиной. После исправления той или иной ошибки убедитесь в правильности всех выполненных действий, чтобы не повторить ее вновь. Если ошибка сопровождается выдачей слишком необычного сообщения, следует постараться запомнить ее причину на будущее. Все изменения производите только на уровне входного языка. Никогда не делайте заплат в программе на машинном языке: это нарушает достоверность документации, описывающей программу на языке высокого уровня, и создает дополнительные возможно- сти для внесения ошибок.
ОТЛАДКА ПРОГРАММ 191 Не считайте причиной ошибок машину. Современные машины и компиляторы обладают чрезвычайно высокой надежностью. Факт отказа машины, как правило, становится сразу очевидным для ее обслуживающего персонала, работающего, в машинном зале, по- скольку ЭВМ не делает мелких ошибок, а делает только крупные. Отдавайте себе отчет в том, что ошибки в программе наверня- ка существуют. Их допускают даже высококвалифицированные программисты, однако последние хорошо осознают факт обяза- тельного присутствия ошибок, обнаруживают их, исправляют и полностью исключают из программы. В отличие от программистов- профессионалов программисты-любители либо проявляют излиш- нюю самоуверенность относительно бездефектности своих программ, либо склонны не обращать внимания на ошибки, в резуль- тате которых появляются устойчивые или случайные неверные ре- зультаты. Очень часто можно обнаружить ошибку, просто пытаясь объяснить кому-нибудь, какие действия вы намерены выполнить. Многократные повторные прогоны программы в процессе отлад- ки являются обычным делом. Поэтому следует внимательно отно- ситься к выдаваемым каждый раз результатам, иначе последую- щие прогоны будут вносить путаницу. На каждом выданном ре- зультате следует проставлять дату и время, а ненужные результаты необходимо выбрасывать. Помните, однако, при этом, что по- пытки исправить ту или иную ошибку могут повлечь возникнове- ние новых ошибок, поэтому желательно иметь возможность воз- врата к предыдущему этапу отладки, если связанные с ним ошиб- ки были проще. __ Некоторые старые листинги все же необходимо сохранять. Их можно складывать в стопку в специально отведенном для этого месте, но только не на рабочем столе; это позволит не перепутать новые листинги со старыми, а также даст возможность обращения к последним в случае необходимости. При наличии такой возмож- ности легко в свободное от работы на машине время прослежи- вать ход отладки программы. Если вы сохраняете датированные временем выдачи распечат- ки, при необходимости программу можно легко восстановить. По- этому всегда оставляйте один-два последних листинга на случай утери программы, ее случайного выбрасывания, нарушения логи- ческой последовательности или каких-либо иных повреждений. При таких обстоятельствах последний вариант распечатки стоит многого. Для предосторожности время от времени можно делать копию программы на входном языке, чтобы обеспечить на случай необходимости ее запасной экземпляр. После исправления ошиб- ки тщательно анализируйте результат отладки, чтобы убедиться, что вы не внесли дополнительных ошибок. Этот совет особенно уместен в случае модификации логической структуры программы, что часто приводит к новым дефектам.
Л 92 ГЛАВА 4 4.14. НЕОПРЕДЕЛЕННЫЕ ПЕРЕМЕННЫЕ Распространенным источником программных ошибок являются неопределенные переменные и переменные, для которых не заданы начальные значения. Любая переменная, присутствующая в опера- торе вывода данных, или находящаяся справа от знака равенства в арифметических и логических операторах, должна либо слева от знака равенства принимать конкретное вычисляемое значение, либо использоваться в операторе ввода данных, либо задаваться в качестве параметра некоторой подпрограммы. Это значит, что нельзя использовать переменные, которые не были определены при вводе или в результате вычислений. Рассмотрим для примера следующие два оператора: А = 1 В = В +А Очевидно, что если переменной В ранее не было присвоено не- которого значения, то она является неопределенной. В большин- стве языков в данном случае в качестве текущего значения В бу- дет взята некоторая комбинация двоичных разрядов, оставшаяся в области памяти, отведенной для В, от предыдущих операций. В результате эта переменная приобретает случайное значение, и невозможно заранее предсказать, что произойдет дальше. Хоро- ший отладочный компилятор должен выявлять неопределенные пе- ременные. Достоверным признаком наличия неопределенных переменных является получение различных результатов в результате двукрат- ного прогона программы с одними и теми же входными данными (без внесения каких бы то ни было изменений- в программу). Ошибка такого типа приводит к нарушению правила детерминиро- ванности, согласно которому поведение программы должно быть многократно воспроизводимо при одном и том же наборе входных данных. Очень часто присутствие неопределенных переменных в программе обнаруживается лишь после длительного ее использо- вания. Это значит, что неопределенная переменная обычно прини- мала либо нулевое значение, либо такое, которое не приводило к явной ошибке. Существуют две причины появления в программе неопределен- ных переменных: отсутствие указаний относительно начальных значений используемых переменных и ошибки при перфорирова- нии программы. Первый случай был рассмотрен выше. Во избежание подобной ситуации программист должен строго следить за тем, чтобы лю- бая переменная употреблялась лишь после того, как она опреде- лена. Второй случай связан с подготовкой перфоносителей, поэтому отперфорированная программа должна всегда проходить проверку
ОТЛАДКА ПРОГРАММ 193 на предмет исключения опечаток. Помогает избежать подобных ошибок и тщательный подбор наименований переменных, позволя- ющий исключить двусмысленность, характерную, например, для следующих, опасных с этой точки зрения имен: КО может идентифицироваться как КО (нуль) и КО (буква), KI может интерпретироваться как К1 (единица) и K.I (буква). Особенно трудно обнаружить в наименованиях переменных ошибочную замену буквы О нулем и наоборот. Прочие виды опе- чаток будут рассмотрены в разделе, касающемся именно этого ти- па ошибок. 4.15. ПЛАН РАСПРЕДЕЛЕНИЯ ПАМЯТИ Для большинства компиляторов предусматривается возмож- ность составления так называемого плана распределения памяти, который представляет собой таблицу имен, встречающихся (или подразумевающихся) в исходной программе. План распределения памяти может использоваться, например, для выявления непре- дусмотренных наименований переменных путем простого просмот- ра составленного компилятором списка. В этом списке перемен- ные располагаются в алфавитном порядке, благодаря чему легко заметить, например, что рядом с переменной VI стоит еще и пере- менная VI, и, значит, налицо опечатка (отперфорирована единица вместо буквы I). Кроме того, план распределения памяти может помочь обнаружению посторонних имен. Обычно для различных типов переменных (целочисленных, действительных, буквенных и т. п.) формируют раздельные планы распределения памяти. В некоторых компиляторах планы распре- деления памяти содержат отдельные списки наименований функ- ций, подпрограмм, массивов и констант. Компилятор фирмы IBM для языка ПЛ/1, кроме того, указывает, какие переменные зада- ны явно, а какие неявно. Планы распределения ламяти обычно запрашивают посредст- вом операторов языка управления заданиями. Для каждого ком- пилятора характерна своя специфическая структура этого плана. Особенно полезны планы распределения памяти при внесении из- менений в чужую программу, поскольку они предоставляют в рас- поряжение соответствующего лица полный список использованных в программе переменных. 416. ТАБЛИЦА ПЕРЕКРЕСТНЫХ ССЫЛОК Таблица перекрестных ссылок указывает, в каком месте про- граммы используется та или иная переменная, а в некоторых слу- чаях еще и содержит информацию о том, где встречается обраще- ние к конкретной метке, функции или подпрограмме. Информация такого рода часто оказывается крайне полезной при модификаци- ях программ. 13—899
194 ГЛАВА 4 Кроме того, таблица перекрестных ссылок позволяет выявлять некоторые разновидности ошибок. Например, тот факт, что пере- менная описана, но в программе к ней обращения нет, может оз- начать неверное написание наименования переменной. Точно так же может свидетельствовать об ошибке неиспользуемая метка. Таблица перекрестных ссылок, как правило, запрашивается по- средством операторов языка управления заданиями. 4.17. ОПЕЧАТКИ Опечатки в программах на входном языке могут приводить к таким ошибкам, которые трудно обнаружить. Для сокращения числа подобных ошибок можно рекомендовать следующие меры: 1. Применение стандартных бланков программирования для соответствующего языка. При использовании в тех же целях обыч- ных линованных форматок вносятся ошибки, связанные с наруше- нием правил расположения символов, и затрудняется разбиение программы на параграфы. 2. Использование письменных принадлежностей, позволяющих писать разборчиво; предпочтительнее всего применять мягкий гра- фитовый карандаш. Текст, написанный, таким карандашом, удобо- читаем и легко стирается. 3. Кодирование программы посредством прописных букв, а не- строчных. 4. Обязательная проверка всех перфорируемых программ. Определение категории ошибок перфорации постоянно достав- ляет программистам уйму хлопот. Очень часто при печатании пу- тают следующие символы (написанные от руки}: цифра 1 буква I знак «ИЛИ» | косая черта / кавычка * буква Z цифра 7 цифра 2 цифра 4 знак 4- знак отрицания | цифра 7 знак «больше» > буква D буква О буква L знак «меньше» < буква О буква Q цифра О буква G буква С цифра 6 пропуск символа — знак «минус» — . буква S цифра 5
ОТЛАДКА ПРОГРАММ 195 На эти символы следует обращать особое внимание во избежа- ние их неправильной интерпретации. Например, букву S следует снабжать черточками, верхнюю часть цифры 4 замыкать с верти- кальной чертой, у буквы D продолжать «хвостики» влево. 4.18. ПРОВЕРКА ПРОГРАММЫ ЗА СТОЛОМ В период перфорирования программы можно осуществить це- лый ряд полезных действий, что явится для вас отдыхом от мо- нотонной работы по подготовке перфокарт или печатания програм- мы на терминале. Прежде всего следует обратиться к постановке задачи, проектной документаций, алгоритмам и блок-схемам и убе- диться в том, что задача понята до конца и созданная программа действительно способна ее разрешить. Если даже вам уже прихо- дилось это делать, полезно еще раз проанализировать проектные решения, пока программа свежа в памяти. Кроме того, хорошо, ес- ли программу просмотрит сотрудник, вообще не принимавший уча- стия в разработке проекта и способный поэтому непредвзято су- дить о вещах, подлежащих проверке. По окончании перфорирования и проверки программы следует распечатать содержимое перфокарт. Во многих вычислительных комплексах эта операция выполняется автоматически как неотъем- лемый этап работы стандартной подпрограммы чтения програм- мных перфокарт на входном языке. Распечатка перфокарт позво- ляет программисту провести проверку за столом в целях обнару- жения ошибок перфорации или пропущенных карт. Обычно даже беглая нерегулярная проверка карт исходной программы дает воз- можность исключить некоторое количество ошибок, благодаря че- му сокращается время компилирования и отладки программы. Такую проверку лучше всего проводить сразу после перфори- рования программы, пока она еще сохраняется в вашей памяти. Осуществление проверки через неделю, когда программист оказы- вается безнадежно втянутым в процесс отладки, будет затруднено, так как исходная программа забудется и нелегко будет заметить пропущенную или неправильно отперфорированную карту. Следо- вательно, несколько минут проверки за столом могут сберечь бес- численное количество часов отладки. Некоторые полргают, что компилятор должен устранять все ошибки перфорации. Это неверное предположение, поскольку ком- пилятор способен обнаруживать лишь такие дефекты, которые приводят к синтаксическим ошибкам. Первым делом проверяйте программу за столом. После того как перфокарты сопоставлены с программой, сле- дует еще раз сверить программу с блок-схемой. Покомандная про- верка на основе блок-схемы должна помочь заблаговременно выя- <3*
196 ГЛАВА 4 вить и устранить целый ряд ошибок. Иногда полезно поручить сравнение программы с блок-схемой своему товарищу (где найти такого бескорыстного друга?). В случае когда в программе ис- пользуются короткие подпрограммы, подобная работа не представ- ляется такой уж напряженной, поскольку вместо проверки одной большой программы могут проверяться ее отдельные блоки. Два типа сообщений об ошибках обычно свидетельствуют о присутствии в программе неопределенных переменных: ПЕРЕПОЛ- НЕНИЕ и ПОТЕРЯ ЗНАЧИМОСТИ. Этими словами характери- зуются соответственно слишком большие и слишком малые числа. Нынешние возможности ЭВМ таковы, что практически любые вы- числения могут выполняться ими без переполнения или потери значимости. Поэтому, если заведомо известно, что программа не имеет дела с очень большими или крайне малыми числами, то пе- реполнение или потеря значимости служат обычно достоверными признаками наличия в программе неопределенных переменных. Один из лучших способов избежать их появления заключается в том, чтобы максимум работы по присваиванию начальных зна- чений переменным выполнять при их описании. Это позволяет ис- ключить случаи отсутствия начальных значений, делает докумен- тацию более удобной, так как все начальные значения перемен- ных оказываются сосредоточенными в одном месте, устраняет потребность в операторах присваивания, что в свою очередь повы- шает эффективность функционирования программы в связи с переносом работы по присвоению начальных значений переменным на этап компилирования. 4.19. ОПИСАНИЕ ПЕРЕМЕННЫХ Правильным следует признать такой стиль программирования, при котором дается описание всех атрибутов переменных, даже ес- ли часть из них может быть очевидна по умолчанию. Описание атрибутов в явном виде помогает качественному документированию программы; если же какие-то атрибуты переменных не определе- ны, то при изменении условий, допускающих умолчание, нормаль- ная работа программы существенно нарушится. Явное описание атрибутов переменных предпочтительно еще и по той причине, что оно заставляет программиста вникать в их сущность. Это особенно важно для функций, введенных программи- стом, и для списков параметров, так как здесь необходимо иметь твердую уверенность в правильности описаний. Описание всех пе- ременных в явном виде должно исключить один из широко распро- страненных типов программных ошибок, а именно ошибок, связан- ных с незнанием физического смысла переменных. В дополнение к сказанному рекомендуется проверять, все ли типы данных в од- ном операторе совместимы. Разнотипность данных в пределах од- ного оператора может приводить к труднообнаруживаемым ошиб- кам.
ОТЛАДКА ПРОГРАММ 197 4.20. ОШИБКИ ВВОДА-ВЫВОДА Одним из первых шагов отладки должно быть распечатывание всех вводимых данных. Множество программных ошибок связано с неверным считыванием входных данных. При этом бесполезно тратится масса времени на поиски несуществующей ошибки в про- грамме, между тем как ее истинной причиной являются непра- вильные данные. Неправильные данные могут являться следствием опечаток, неверной интерпретации данных либо неправильного определения форматов ввода. Распечатывая введенные данные, программист получает возможность просмотреть их и убедиться в правильности их считывания машиной. Однако такая проверка, помогая выяв- лять ошибки перфорирования, никак не затрагивает форматных ошибок. Я почти всегда прибегаю к распечатыванию входных дан- ных, поскольку убедился на собственном опыте, что это действие является единственной надежной гарантией правильного считыва- ния вводимой информации: Введенные данные следует распечатывать немедленно после их считывания в память машины. Этот процесс называется эхо-про- веркой, и если предусмотреть его выполнение в программе не- сколько позже, то можно либо вообще не добраться до этого эта- па, либо содержимое области памяти, использованной для записи исходных чисел, может измениться в ходе работы программы. Выполняйте эхо-проверку вводимых данных. Распечатываемые при эхо-проверке данные не должны быть безымянными; необходимо, чтобы можно было легко разобраться, какие значения к каким переменным относятся. В ФОРТРАНе для этих целей можно использовать команду NAMELIST, а в языке ПЛ/1 — команду PUT DATAf В КОБОЛе значения переменных, участвующих в отладке, легко выводятся на печать посредством команды DISPLAY или EXHIBIT. В случае отладки модулей или подпрограмм входными пере- менными служат конкретные значения их параметров. Следует в обязательном порядке распечатывать эти значения, так как в слу- чае обнаружения в них ошибки вся работа по отладке, проделан- ная до этого момента, будет напрасной тратой времени. Что касается ошибок вывода данных, то они могут привести программиста к предположению, что во всем виновата программа, даже в тех случаях, когда их причиной является неправильная спецификация форматов. В этом плане наиболее распространенными ошибками являют- ся занижение или совершенно неправильное определение размеров требуемых полей выходных данных. Для программиста, имевшего дело с подобными ошибками, они, как правило, очевидны. Менее
198 ГЛАВА 4 опытные программисты могут приобрести необходимые навыки, анализируя результаты экспериментов с уменьшенными размера- ми полей и их неправильными спецификациями и запоминая ха- рактер выдаваемых при этом сообщений об ошибках. 4.21. «ПСИХОЛОГИЯ» ПРОГРАММНЫХ ОШИБОК Каждый тип программных ошибок имеет специфический харак- тер, или свою «психологию». Опытные программисты зачастую, взглянув на то или иное загадочное сообщение о системной ошиб- ке, могут сразу сказать: «Вероятно, нарушены границы для ин- дексов массива»— или что-нибудь еще в том же роде. За этой фразой скрывается большой практический опыт, приобретение которого можно ускорить, если составлять небольшие программы со специально введенными в них ошибками определенного харак- тера. В этом случае имеется возможность следить за выходными сообщениями и познавать таким образом «психологию» конкрет- ного типа ошибок. Таким же способом можно выявлять недора- ботки, упущения и ошибки в технической документации системы. 4.22. ПАТОЛОГИЯ ЧИСЕЛ При выполнении вычислительных операций с помощью на- стольного калькулятора легко обнаружить любые арифметические ошибки. Если же мы имеем дело с ЭВМ, ошибки в большинстве своем скрыты от наших глаз. Кроме того, вычисления производят- ся со скоростью до миллиона операций в секунду, и проследить за ними совершенно невозможно. Существует, однако, несколько типов ошибок, которые настолько серьезны, что пользователь пре- дупреждается о них аппаратными средствами, — это деление на нуль, переполнение и потеря значимости. Большинство затруднений бывает связано с тем, что количест- во разрядов действительных чисел, подлежащих хранению в памя- ти, ограничено. Даже в логически правильной программе долго не замечаемая ошибка может вызвать губительные последствия. Ча- сто такие ошибки обнаруживаются лишь тогда, когда новая сово- купность исходных данных приводит к явно неверным результа- там. Все это происходит потому, что точность представления дей- ствительных чисел в машине предопределена заранее. Пусть, на- пример, при выполнении операций А = 1.0/3.0 получается число 0.3333333 х В = А*3 получается число 0.9999999 Неосторожный программист может необдуманно вставить в та- кую программу оператор условного перехода IF (В = 1) ...
ОТЛАДКА ПРОГРАММ 199 Другая серьезная ошибка может быть связана с использова- нием оператора I = В, где j — целое число, а В — действительное число 0.9999999. Ре- зультат выполнения этой операции, хранимый в I, окажется нулем вследствие отбрасывания дробной части. Аналогичные ошибки могут возникать при сложении и вычи- тании. Предположим, что мы имеем дело с машиной, которая способ- на выполнять арифметические операции только над четырехраз- рядными десятичными числами. Пусть это будут числа X = 999.0 Y = —1000. Z = .001 и требуется вычислить X + Y4-Z+ 1.0 Приняв последовательность действий ((X + Y) -FZ) +1.0 получим ((999.0 — 1000.) + .001) + 1.0=(—1.0 + .001) + 1.0 = = —.999+ 1.0 = .001 Выполняя действия в другом порядке, будем иметь уже другой результат: X + (Y + Z) + 1.0 = 999.0 + (—1000. + .001) + 1.0= = 999.0 + (—1000.) + 1.0 = —1.0 + 1.0 = 0.0 В данном случае произошла потеря точности из-за наличия всего лишь четырех разрядов. Опасность, следовательно, кроется там, где производится вычитание двух почти равных чисел, по- скольку в этом случае легко потерять все значащие разряды. Та- кая ситуация, называемая потерей разрядов, может возникать в машинах любой разрядности. Часто оказывается, что потерянные разряды — наиболее важные, особенно когда получающееся чис- ло вообще не содержит значащих цифр. 4.23. ОБНАРУЖЕНИЕ ОШИБОК Часто обнаружение той или иной ошибки в программе оказы- вается делом довольно трудным. Мы приходим к мысли о том, что в программу закралась ошибка, в одной из следующих ситуаций: 1. Отсутствует уверенность в том, что программа начала вы- полняться.
200 ГЛАВА 4 2. Программа начала выполняться, но произошел преждевре- менный останов с выдачей или без выдачи сообщения о системной ошибке. 3. Программа начала выполняться, но зациклилась, о чем мож- но судить по ее чрезмерно долгой работе. 4. Программа выдала неправильную информацию. Любая из этих ситуаций требует от программиста проверки последовательности выполнения команд программы. Обычно для этих целей пригодны операторы TRACE, однако они обладают ря- дом недостатков, которые отмечаются ниже в разделе «Средства отладки». Если ошибка оказывается непонятной и ее местоположение не обнаружено, можно повторить прогон программы и посмотреть, появится ли та же ошибка снова. Большинство ошибок воспроиз- водимо, и этот шаг по крайней мере подтвердит вам, что ошибка в программе есть. Неповторяющиеся ошибки могут быть вызваны неправильным действием оператора ЭВМ, сбоем в работе оборудо- вания, колебаниями питающего напряжения или тупиковыми си- туациями в операционной системе. О таких ошибках надо сохра- нять какие-то записи, поскольку часто они возникают в случайные моменты времени. Одна из возможных причин неповторяющихся ошибок — наличие в программе неопределенных переменных. Ре- гистрировать факты появления таких ошибок можно, например, посредством внесения дополнительных примечаний в документа- цию и хранения сообщения об ошибке на будущее. Выявление поведения некоторого неизвестного блока в про- грамме означает прежде всего необходимость выработки стратегии поиска. В одних случаях (например, когда единственной выходной информацией является сообщение об ошибке) вообще отсутству- ют какие-либо сведения, позволяющие выявить ошибку. В других (например, когда выдан некоторый результат) программист все- таки располагает некоторыми данными о местоположении ошиб- ки. Обычно не представляет труда исправить ошибку, если изве- стно, где она находится. Иногда бывает полезно исключить из рассмотрения маловеро- ятные причины ошибок. Когда вы сталкиваетесь с какой-либо ошибкой или неполадкой, прежде всего следует решить, что тому виной: технические средства, операционная система, компилятор или ваша собственная программа? Если вы не работаете на новом оборудовании и не имеете дело с новым программным обеспечени- ем, то большинство ошибок — ваши собственные ошибки. Удостоверившись в наличии программной ошибки, вы можете действовать далее по методу последовательного исключения. Если вы получаете распечатки вводимых данных, то подпрограммы вво- да скорее всего работают нормально. Аналогично могут быть ис- ключены из рассмотрения некоторые другие подпрограммы, и об- ласть поиска постепенно сузится до двух-трех «подозрительных»
ОТЛАДКА ПРОГРАММ 201 блоков, которые и подвергаются более глубокому анализу. Глав- ный принцип в работе по обнаружению ошибки —это недопуще- ние беспорядочного поиска, к которому склонны многие неопыт- ные программисты. Полезно иметь в виду, что поиск ошибок необходимо начинать с рассмотрения в первую очередь простых ситуаций. Например, если вы считаете, что ошибка располагается в какой-то из трех подпрограмм и проверка каждой из них занимает соответственно 10 мин, 2 ч и 3 дня, то явно имеет смысл проанализировать сначала две первые подпрограммы, так как при этом есть возможность по- лучить желаемый результат за короткое время. (Следует заметить, что обычно руководствуются именно этим неписаным правилом.) Цель программиста состоит в том, чтобы продолжать сужение области поиска до полного выявления ошибки. Поскольку одни блоки программы могут характеризоваться нулевой вероятностью наличия в них ошибки, а другие—довольно высокой, задача за- ключается в постепенном увеличении вероятности определения ме- стоположения ошибки до 100%. Общепринятый метод отыскания ошибок в программе предпо- лагает введение в нее от 5 до 10 специальных отладочных опера- торов. Один ид них должен находиться в начале программы, дру- гой— в конце, а остальные следует располагать внутри програм- мы равномерно, но избегая попадания их внутрь циклов. Следует- размещать один оператор до начала цикла и один после его окон- чания; в противном случае соответствующая информация будет выдаваться на печать каждый раз при выполнении цикла, и не- трудно представить себе случай, когда цикл повторяется не одну тысячу раз. Моими излюбленными отладочными операторами, используе- мыми для поиска ошибок в программе, являются операторы вида DEBUG 1 DEBUG 2 Первый из них обеспечивает печатание слова DEBUG и циф- ры 1; второй — печатание того же слова с цифрой 2. Слово DEBUG указывает, что данный оператор введен в программу только для отладочных целей и подлежит исключению из нее по окончании отладки. Встраивание таких операторов в программу позволяет легко обнаруживать местоположение ошибки. Пусть, например, в программе имеются 10 отладочных операто- ров; тогда выдаваемая на печать информация может оказаться следующей: DEBUG 1 DEBUG 2 DEBUG 3 DEBUG 4
202 ГЛАВА 4 Поскольку последнее напечатанное предложение DEBUG 4, оче- видно, что ошибка располагается где-то между отладочными опе- раторами DEBUG 4 и DEBUG 5. Если анализ указанной части про- граммы не выявляет в ней ошибки, то необходимо разместить все 10 отладочных операторов в том же блоке, границы которого ра- нее отмечались операторами DEBUG 4 и DEBUG 5. В конце кон- цов этот процесс приведет к обнаружению ошибки. Однако с отысканием неверно действующего оператора работа по устранению ошибки зачастую не заканчивается, поскольку обычно оказывается, что причина ошибки находится где-то в дру- гом месте программы. Рассмотрим, например, оператор С = В/А Затруднения с .выполнением этого оператора должны возникать тогда, когда А равно нулю. Поэтому следующим шагом должна быть проверка предшествующего оператора для выяснения причи- ны обращения А в нуль. Процесс обнаружения ошибок характеризуется выявлением двух мест в программе: точки обнаружения и точки происхожде- ния. Точка обнаружения — это место в программе, где ошибка се- бя проявляет или становится очевидной. Точка обнаружения дол- жна выявляться первой. В рассмотренном выше примере такой точкой является оператор, использующий нуль в качестве дели- теля. Точка происхождения—это место в программе, где возникают условия для появления ошибки. В нашем примере эти условия возникают там, где А присваивается значение, равное нулю. Дей- ствительная ошибка исходит не из точки обнаружения, а из точки происхождения. Точка же обнаружения служит отправным пунк- том для поиска точки происхождения. Описанный выше метод обнаружения ошибок предполагает использование ЭВМ. Однако существует еще и другой, очень ча- сто недооцениваемый метод — это внимательное чтение листинга программы. Такой подход дает наилучшие результаты в случаях, когда область расположения ошибки сужена до небольших раз- меров. Был, например; проведен эксперимент с двумя группами программистов, перед которыми ставилась задача обнаружения ошибки в программе. Одна группа делала это с помощью ЭВМ, а другая — вручную. Оказалось, что обе группы затратили на обна- ружение ошибки одинаковое время. 4.23.1. ОТЛАДОЧНАЯ ИНФОРМАЦИЯ Информация, выдаваемая на печать в процессе отладки про- граммы, состоит из операторов, характеризующих результаты вы- полнения отладочных действий. Первым таким действием является
ОТЛАДКА ПРОГРАММ 203 эхо-печать всех введенных данных, затем необходима информа- ция о ходе вычислительных операций и в заключение — информа- ция о работе логической части программы. Если в программе выполняется большой объем вычислений, то неверный результат редко предоставляет информацию, достаточ- ную для нахождения местоположения ошибки'. Поэтому необходи- мы специальные отладочные средства. Прежде всего переменные, выдаваемые на печать в ходе отладки, должны иметь удобные идентификаторы. Не следует воспринимать как что-то необычное такой стиль программирования, когда значительная часть програм- мы пишется специально для облегчения процесса отладки. Вместе с тем вводимые в исходную программу отладочные блоки должны иметь такую структуру, чтобы их исключение не приводило к про- граммным ошибкам. Поскольку программа в ходе отладки компи- лируется 10—20 раз, выгодно предусматривать выдачу отладочной информации на возможно более ранних этапах, с тем чтобы уменьшить количество необходимых циклов компилирования. Наиболее эффективны в этом отношении отладочные средства, предусматриваемые в программе самим программистом. Обычно легче это сделать в процессе программирования, а не на более поздней стадии отладки. Очевидно, что после окончательной от- ладки и тестирования программы отладочные блоки должны быть из .нее исключены. К сожалению, для программистов типична ситуация, когда на этапе написания программы о средствах отладки забывают. При- чина подобного положения кроется в том, что мы не можем реаль- но оценить, насколько эти средства действительно необходимы. Для того чтобы решить, нужно ли вводить в программу отладоч- ные операторы, полезно в ходе программирования задаваться во- просом: достаточно ли выходной информации данного блока для обнаружения в нем ошибок, если таковые имеются? Не следует забывать о том, что на отладку тратится больше времени, чем на все другие этапы создания программы. А во мно- гих случаях на отладку программы расходуется даже больше ма- шинного времени, чем на ее рабочие прогоны. Вводите средства отладки как можно раньше. Стратегия ввода в программу специальных отладочных опера- торов, предназначенных для выдачи данных на печать, обеспечи- вает получение четкого представления о том, что происходит при выполнении как арифметических, так и логических операций. Как правило, чем ближе к началу программы расположены такие вспомогательные операторы, тем меньше требуется в дальнейшем отладочных прогонов. Как неоднократно подчеркивалось выше, после завершения от- ладки программы все отладочные операторы подлежат исключе-
204 ГЛАВА 4 нию из нее. Лучше всего преобразовать их в комментарии, чтобы прекратить выдачу отладочной информации. Тогда сохраняется возможность вернуться к ним впоследствии при необходимости. Если же их уничтожить совсем,' то в будущем в случае обнаруже- ния ошибок в программе или внесения в нее изменений отладоч- ными операторами воспользоваться не удастся, хотя такая необ- ходимость и возникнет. При наличии в распоряжении программиста системы терминаль- ного доступа имеется возможность помечать тем или иным обра- зом строки программы, предназначенные для использования при отладке и тестировании; тогда программа-редактор сможет иск- лючать их автоматически. Например, в случае применения языка ФОРТРАН можно начинать каждую такую строку с номера отла- дочного оператора 9ХХХХ, а впоследствии выдавать редактирую- щей программе команду на устранение всех помеченных указан- ным способом строк. В заключение следует сказать, что, будучи преобразованными в комментарии, отладочные и тестовые строки программы пред- ставляют собой важную часть ее документации, так как несут ин- формацию о конкретных проведенных испытаниях. Язык ПЛ/1 фирмы IBM позволяет программисту задавать ле- вую и правую границы считывания карт исходной программы по- средством использования оператора SORMGIN. В нормальном ре- жиме этот оператор имеет вид SORMGIN = (2,72), что может слу- жить основой для автоматического переключения программы с от- ладочного режима или режима тестирования на рабочий. Осуще- ствить это возможно следующим образом. При отладке и тестировании исходной программы используйте оператор SORMGIN = (4,70), а в рабочем режиме ее эксплуатации — опе- ратор SORMGIN = (2,72). Для всех отладочных и тестовых опе- раторов программы, которые вы хотите исключить из ее рабочего варианта, просто поместите знаки /* в колонках 2—3 и знаки */ в колонках 71—72. Поскольку указанные знаки служат ограничи- телями комментария, предлагаемая операция позволяет автомати- чески трансформировать отладочные и тестовые средства в ком- ментарии. 4.23.2. ВЫБОРОЧНАЯ ПЕЧАТЬ Часто в ходе отладки бывает желательно выдать на печать вы- борочно ту или иную информацию. Эта потребность возникает тогда, когда необходимо проверить работу программы лишь в конкретных ситуациях или если нежелательна печать излишне большого объема информации, как это может иметь место в слу- чае попадания оператора вывода внутрь программного цикла. Примером оператора выборочной печати может служить пред- ложение IF (X .LT. 0.0) WRITE ...
ОТЛАДКА ПРОГРАММ 205 Этот оператор будет обеспечивать печатание данных, только когда X меньше нуля. Для проверки индексов и выборочной распечатки отладочной информации может использоваться оператор IF (I .GT. 10 .AND. I .LT. 15) WRITE ... который обеспечивает выдачу на печать только в случае, если 1 = 11, 12, 13 или 14. Оператор IF (1/5*5—I .EQ. 0) WRITE ... учитывает особенности выполнения арифметических действий над целыми числами. Выражение 1/5*5—I равно нулю лишь тогда, когда I содержит 5 в качестве множите- ля (т. е. когда 1 = 5, 10, 15, ...). Поэтому приведенный выше опера- тор может использоваться для организации выдачи на печать ре- зультатов каждой n-й итерации (в данном случае — каждой пя- той) или результатов выполнения функции MOD. Выборочные распечатки могут также применяться для контро- ля за числом итераций, выполненных в итеративных алгоритмах, например, с помощью оператора IF (N .EQ. 1000) WRITE ... Он будет сигнализировать о том моменте, когда N достигнет зна- чения 1000, что может быть и результатом зацикливания програм- мы. Однако в последнем случае применение указанного условного оператора связано с гораздо меньшими издержками, чем встраива- ние какого-либо безусловного оператора вывода внутрь цикла. Еще одним полезным отладочным средством является опера- тор IF DEBUGGING THEN ... в котором слово DEBUGGING представляет собой логическую пе- ременную, принимающую значения «истина» или «ложь». Такая конструкция предложения дает программисту возможность вклю- чать средства отладки (или тестирования), предусмотренные в программе путем простого присвоения соответственно значения «истина» или «ложь» единственной логической переменной. При ис- пользовании этого метода отладочные операторы могут сохранять- ся в программе на случай, если в будущем возникнет потребность в их применении. Для блокирования их действия достаточно лишь присвоить переменной DEBUGGING значение «ложь». В некото- рых компиляторах (например, в трансляторе с языка ПЛ/1) пре- дусматривается возможность определения условий компилирова- ния, которой целесообразно пользоваться для включения и выклю- чения средств отладки и тестирования.
206 ГЛАВА 4 4.23.3. ПРОСЛЕЖИВАНИЕ ЛОГИЧЕСКИХ ВЕТВЕЙ Для указания логического пути, по которому идет выполнение программы, можно использовать те или иные операторы вывода, которые обычно вставляют либо в подпрограммы, либо непосред- ственно за операторами условного перехода. Благодаря этому за- вершение каждого этапа выполнения программы отмечается выда- чей на печать соответственно подобранных сообщений. Например: ВХОД В ПОДПРОГРАММУ MAXNUM. ВЫХОД ИЗ ПОДПРОГРАММЫ MAXNUM. ВХОД В ПОДПРОГРАММУ FIXNUM. . ПЕРЕХОД НА ВЕТВЬ «МЕНЬШЕ НУЛЯ». ОДНА ТЫСЯЧА ИНТЕРАЦИИ. Три первых комментария сообщают о том, какая из подпрог- рамм выполняется в данный момент; четвертый указывает конк- ретную ветвь, выбранную для исполнения; последнее предложение говорит о том, сколько проведено итераций. Выходные сообщения должны отмечать как желательные, так и нежелательные ситуа- ции; в этом случае они помогают программисту выявить ошибки, которые иначе могли бы остаться незамеченными. Существует еще одно простое средство отладки, часто недооце- ниваемое программистами, — это выдача всеми программами за- ключительных сообщений, свидетельствующих о нормальном за- вершении их работы. Такре сообщение должно печататься перед самым концом выполнения задания и состоять из предложения. НОРМАЛЬНЫЙ КОНЕЦ РАБОТЫ. Если такого сообщения не печатать, то легко просмотреть ситуа- цию, когда выполнение программы не будет завершено. Одновре- менно с сообщением об окончании работы программа может вы- давать на печать итоговые сведения о том, сколько было обрабо- тано правильных и неправильных элементов информации. 4.23.4. ПУТИ ПРЕОДОЛЕНИЯ ЗАТРУДНЕНИЙ Уместно задать вопрос: что же делать, если имеющуюся в про- грамме ошибку выявить не удается? Как правило, в этом случае нет никакого толку от беспрерывного многодневного размышления над проблемой без посторонней помощи. Здесь можно рекомендо- вать два способа действий. Первый из них заключается в том, что- бы снять каким-либо образом напряжение (например, устроив легкий завтрак) или просто пойти домой. Однако, даже прекратив поиски ошибки, программист все же продолжает о ней думать. Поэтому часто после хорошего ночного отдыха найти неуловимую ошибку бывает намного легче.
ОТЛАДКА ПРОГРАММ 207 Второй способ действий — это обращение за консультацией к кому-либо из коллег-программистов, который мог бы стать вашим помощником по отладке и которому вы могли бы рассказать о встретившейся ошибке. Часто бывает полезно, чтобы кто-то вы- слушал ваши объяснения, касающиеся постановки самой задачи и алгоритма ее решения; при этом в ходе рассуждений программист может сам обнаружить ошибку, либо у его нового партнера по от- ладке могут возникнуть конкретные предложения. Однако второй способ действий требует от программиста умения выслушивать советы коллеги без возражений, а если то или иное предложение кажется вам неправильным, вы можете не принимать его в расчет. Работать с партнером по отладке надо тактично, не забывая о том, что у него тоже есть свои собственные дела. Не следует, на- пример, просить его разобраться в распечатке, сделанной в шест- надцате- и или восьмеричном коде, или в построчной проверке тек- ста программы, поскольку и то и другое — ваша собственная обя- занность. Партнер по отладке — это тот человек, который готов понять вашу задачу, а вы с желанием делаете то же самое для него. 4.24. ЗАЩИТНОЕ ПРОГРАММИРОВАНИЕ Термин защитное программирование характеризует такой стиль написания программ, при котором появляющиеся ошибки легко обнаруживаются и идентифицируются программистом. Необходи- мость защитного программирования диктуется тем, что имеющие- ся в подавляющем большинстве программ ошибки далеко не всег- да сказываются явным образом на их работе и могут в течение многих месяцев оставаться необнаруженными. Встраивание отладочных средств в программу есть не что иное, как защитное программирование. Средства отладки, предусматри- ваемые в исходной программе, называют стопорами ошибок. Их назначение — сохранить «улики», позволяющие идентифицировать ошибку путем указания ее местоположения. Такой подход отлича- ется от традиционного, при котором вначале устанавливается факт явной ошибки, а затем делается попытка проследить за про- цессом ее возникновения в обратном порядке. Последнее, однако, затрудняется тем, что обычно ошибка не оставляет в самой про- грамме никаких следов. Поэтому для обнаружения ее местополо- жения приходится выполнять по нескольку отладочных прогонов. Существует несколько причин, по которым программисты не пользуются защитным программированием. Первая из них — это излишняя самоуверенность. Отладочные операторы не предусматриваются в программе лишь потому, что мы переоцениваем собственные шансы работать без серьезных ошибок. Бывает, что даже длительный практический опыт затруд- нений и неудач не помогает избавиться от этого неверного убеж-
208 ГЛАВА 4 Вторая причина кроется в излишней доверчивости по отноше- нию к другим программистам. Она проявляется в том, что, если достаточно опытный программист убеждает нас в совершенстве своей подпрограммы, нам в голову не приходит мысль о проверке этого утверждения, даже если в прошлом работа программиста не была столь уж безукоризненной. Третьей причиной пренебрежения защитным программировани- ем является неверное предположение, будто оно приводит к из- лишне большому расходу памяти и замедлению работы програм- мы. На самом же деле наиболее существенное обстоятельство, за- медляющее работу программы,—это невозможность заставить ее нормально функционировать. Первые два случая связаны с обычным заблуждением, но да- же если конкретная программа и работает без ошибок, средства отладки все же необходимы для того, чтобы быть уверенным в этом. Нельзя полагаться и на полную достоверность информации. Ошибки оператора, неверные форматы данных, изменения в про- грамме, вносимые в последнюю минуту, ошибки перфорирования и другие подобные огрехи — все это приводит к возрастанию труд- ностей обработки данных. Однако не следует впадать и в другую крайность, полагая, что если кто-то дает входные данные для ва- шей программы, то причина ее плохой работы заключается именно' в них. Ведь если ваша программа или подпрограмма принимает неверные данные к обработке, а затем выходит из строя или, что еще хуже, до конца обрабатывает неверные данные, вина за это ложится только на вас как на программиста. В случае же когда подпрограмма, составленная вами, принимает информацию, содер- жащую ошибки, и передает ее какой-либо другой подпрограмме, очевидно, что ваше детище не способно распознавать неверные данные, и поэтому вы — соучастник ошибки. Ведь вполне возмож- но, что именно ваша подпрограмма является в программе наибо- лее подходящим местом для выявления конкретной ошибки. Сомнительна и справедливость утверждения относительно не- эффективности применения защитного программирования. Дело в том, что незначительное число избыточных операторов, вводимых в программу для предотвращения или обнаружения ошибок, не должно приводить к большому дополнительному расходу машин- ного времени на выполнение программы в рабочем режиме. Нао- борот, такие операторы наверняка сэкономят ваше собственное и машинное время на этапе отладки. А если уж окажется необходи- мым исключить из программы все стопоры ошибок по соображе- ниям увеличения эффективности использования машинных ресур- сов, это легко сделать и позже. Стопоры ошибок проще всего вводить в программу на стадии ее написания. Всякое откладывание этой работы до момента появ- ления ошибок может привести к забыванию существенных деталей
ОТЛАДКА ПРОГРАММ 20’ и неверной организации контроля за ошибками. Кроме того, слиш- ком позднее введение отладочных средств в программу связано с ее повторным компилированием, что в свою очередь требует вре- мени. В большинстве стандартных подпрограмм, поставляемых изго- товителями ЭВМ, входные параметры подвергаются обязательно- му контролю.. Так, например, подпрограммы извлечения квадрат- ного корня и взятия логарифма проверяют, имеет ли аргумент по- ложительное значение. Подобным же образом осуществляется контроль за тем, чтобы значения чисел, участвующих в вычислени- ях, не превышали максимально допустимых. И такие проверки вы- полняются в каждом цикле работы подпрограммы, причем считается, что они оправданны. Следовательно, у программиста нет никаких оснований поступать иначе в собственных подпро- граммах, тем более, что их параметры обычно тоже подчиняются определенным ограничениям. Существует несколько принципов защитного программирова- ния: 1. Общее недоверие. В соответствии с этим принципом для каждого модуля должно предполагаться, что входные данные мо- гут оказаться неверными и подлежат проверке. 2. Немедленное обнаружение. Этот принцип гласит, что лучше всего выявлять ошибку возможно раньше, поскольку при этом уп- рощается задача установления ее первопричины. 3. Изолирование ошибок. Согласно этому принципу, ошибки в одной из частей программы изолируются посредством так называ- емых брандмауэров, чтобы не допустить их губительного влияния на другие части. Идеальной представляется такая ситуация, когда сообщения» печатаемые средствами отладки, могут использоваться для под- тверждения того, что конкретный модуль получил предусмотрен- ные входные данные, что в нем правильно выполнены все опера- ции и что он обеспечил выдачу правильных результатов вызывав- шей его программе. Соответствующие контрольные операторы должны быть независимыми в том смысле, что их исключение не" должно приводить к нарушению функционирования модуля. Еще одним полезным средством отладки является счетчик, фик- сирующий, как часто вызывалась та или иная подпрограмма и сколько раз выполнялся тот или иной цикл. Эта информация полез- на для отладки, так как позволяет судить о близости процесса вычислений к завершению и указывает на те области программы, которые используются наиболее интенсивно и где оптимизация вы- числительного процесса оправданна. Можно предусмотреть в программе текущий контроль за пове- дением наиболее важных данных, например проверку нахождения переменных в допустимых пределах посредством сравнения их ве- личин с максимальными и минимальными значениями. Такой конт- 14—899
210 ГЛАВА 4 роль носит название информационного фильтра. Кроме того, мож- но проводить сопоставление различных переменных в целях выяв- ления ошибок вычислений. В некоторых старых операционных системах информационные фильтры применялись для анализа те- кущего состояния самих операционных систем; если при этом об- наруживалась ошибка, то печаталось сообщение: «Вызывайте си- стемного программиста». К этому моменту автора подобного со- общения зачастую уже невозможно было найти, и поэтому никто не знал, что же следует предпринять. В настоящее время мы рас- полагаем несомненно большими возможностями. Теперь выясним, для чего и каким образом следует проверять Созданную программу: 1. Проверяйте тип данных. Контролируйте буквенные поля {по- ля имен), чтобы убедиться, что они не содержат цифровых дан- ных. Проверяйте цифровые поля на отсутствие в них буквенных данных. 2. Делайте проверку области значений переменных, чтобы удо- стовериться, например, что положительные числа всегда положи- тельны. 3. Выполняйте контроль правдоподобности значений перемен- ных, которые не должны превышать некоторых, констант или зна- чений других переменных. Например, начисляемые налоги и удер- жания не должны быть больше суммы, по которой они определя- ются. 4. Контролируйте итоги вычислений путем введения всюду, где это возможно, перекрестных итогов, контрольных сумм и счетчи- ков числа обрабатываемых элементов информации. 5. Используйте автоматические проверки, такие, как контроль за переполнением, потерей значимости и метками файлов. 6. Проверяйте длину элементов информации, если она задана, например код почтового индекса. 7. Контролируйте закрепленные признаки, под которыми пони- маются обязательные элементы полей или записей данных. Напри- мер, если определенный тип записей должен всегда содержать в колонках 73—75 код MST, необходимо этот факт проверять. 8. Выполняйте проверку контрольных разрядов. Так как неко- торые элементы данных могут иметь дополнительные контроль- ные цифры, существует возможность проверки правильности этой информации. Такой код называется избыточным. Защитное программирование, разумеется, приводит к созданию избыточной программы подобно тому, как использование контро- ля по четности вносит избыточность в работу аппаратных средств. Необходимо помнить вместе с тем, что ошибка, которую вы стре- ляйтесь обнаружить или появление которой стараетесь предотвра- тить, в дальнейшем потребует меньшего внимания. Сотрудники вычислительных центров любят ссылаться на поговорку: «Мяки- ну заложишь — мякину получишь», которую можно назвать прин-
ОТЛАДКА ПРОГРАММ 211 ципом МЗМП. Хорошая программа — это та, которая обнаружи- вает «заложенную мякину», не допуская ее появления на выходе. Контролируйте правдоподобность вводимых данных. 4.25. УТВЕРЖДЕНИЯ Некоторые новые экспериментальные языки программирования предоставляют возможность введения утверждений, позволяющих оговаривать условия, требуемые для правильной работы конкрет- ной программы. Чаще всего встречаются два типа утверждений: глобальные, которые содержатся вместе с описателями, и локаль- ные, располагающиеся внутри программы и позволяющие огова- ривать определенные значения переменных в данной точке выпол- няемой программы. Примером условий первого типа служит объ- явление N в качестве целочисленной переменной и утверждение о том, что она должна всегда принимать положительные значения. Примером утверждения второго типа является условие, что еже- недельная плата не должна превышать 2000 долл. Если какое-нибудь из утверждений не выполняется, программа прекращает работу и выдает сообщение об ошибке. Утверждения второго типа можно легко встраивать в программу с помощью оператора IF. Некоторые языки предоставляют программисту воз- можность вводить утверждения, на основе которых при компили- ровании формируется соответствующий контрольный блок програм- мы; в рабочем режиме утверждения трактуются как коммента- рии. 4.26. СПИСОК ХАРАКТЕРНЫХ ОШИБОК Для подавляющего большинства программистов характерно систематическое повторение в разных программах ошибок опреде- ленного типа. Это может быть использование неразрешенных ин- дексов, неправильное применение операторов условного перехода, установление начального значения, равного единице, там, где оно должно быть равно нулю, и т. п. Повторение ошибок наблюдает- ся особенно часто в тех случаях, когда программист работает с двумя или более языками: поскольку каждый язык имеет свои правила, их легко перепутать. Во избежание такой ситуации про- граммисту полезно иметь список своих характерных ошибок, ко- торый он может использовать в качестве вспомогательного сред- ства отладки. Ниже приводятся две категории наиболее распространенных ошибок программирования. К первой из них относятся ошибки об- щего (несинтаксического) характера, которые остаются в програм- мах после выполнения синтаксического контроля. Вторая катего- рия включает ошибки специального вида, которые особенно труд- ны для выявления. 14*
212 ГЛАВА 4 ОШИБКИ ОБЩЕГО ХАРАКТЕРА Логические ошибки 1. Неверное указание ветви алгоритма после проверки некото- рого условия. 2. Неполный учет возможных условий. 3. Пропуск в программе одного или более блоков составленно- го алгоритма. 4. Ответвление по неверной метке. Ошибки в циклах 1. Неправильное указание начала цикла. 2. Неправильное указание условий окончания цикла. 3. Неправильное указание числа повторений цикла. 4. Неверное индексирование цикла. 5. Бесконечные или замкнутые циклы. Ошибки при работе с данными 1. Определение не всех возможных типов данных. 2. Отсутствие редактирования неверных данных. 3. Организация считывания меньшего или большего объема данных, чем требуется. 4. Неправильное редактирование данных или несоответствие действительных полей данных их описаниям. Ошибки в описании переменных 1. Использование переменных без указания их начальных зна- чений. 2. Не предусмотрено изменение содержимого счетчика или на- копителя. 3. Неправильная установка значения программного ключа. 4. Использование неверного наименования переменной (т. е. ошибочное указание другой переменной). Ошибки при работе с массивами 1. Отсутствие возможности присвоения нулевого значения мас- сиву. 2. Неправильное описание достаточно больших массивов. 3. Неправильный порядок следования индексов. Ошибки арифметических операций (см. также Ошибки в описании переменных) 1. Неверное указание типа переменной (например, целочислен- ной вместо действительной).
ОТЛАДКА ПРОГРАММ 213 2. Переполнение и потеря значимости. 3. Использование неверных констант. 4. Неверное определение порядка действий. 5. Деление на нуль. 6. Извлечение квадратного корня из отрицательного числа. 7. Потеря значащих разрядов числа. Ошибки в подпрограммах' 1. Неправильное описание функции. 2. Неправильное указание требуемых параметров подпрограм- мы. 3. Неверное число параметров. 4. Неверные значения параметров. Ошибки ввода-вывода (см. также Ошибки при работе с данными) 1. Неправильный выбор спецификаций формата ввода-вывода. 2. Невыполнение операций перемотки (или установки) маг- нитной ленты перед чтением или записью информации. 3. Использование записей неправильной длины или неверного формата. Ошибки в цепочках символов 1. Указание неверной длины цепочки символов. 2. Обращение к символу за пределами строки. Ошибки логических операций 1. Неправильное применение логического оператора. 2. Сравнение переменных разного типа. 3. Пропуск оператора ELSE в сложных операторах IF, вклю- чающих множество условий. Ошибки машинных операций 1. Неправильный сдвиг числа. 2. Неверное использование машинной константы (например, в десятичной форме вместо требуемой шестнадцатеричной). Ошибки в применении разделителей 1. Отсутствие признака конца оператора. 2. Отсутствие признака окончания комментария. 3. Использование открывающих кавычек вместо закрывающих и наоборот. 4. Несогласованность кавычек в пределах строки. 5. Преждевременное появление символа признака конца.
214 ГЛАВА 4 Прочие ошибки 1. Неправильное использование полей операторов. 2. Использование неподходящей функции. ОШИБКИ СПЕЦИАЛЬНОГО ВИДА Семантические ошибки Вызываются неглубоким пониманием действия той или иной команды, например убежденностью в том, что результаты арифме- тических операций округляются. Другим примером подобных оши- бок может служить неправильное предположение, что цикл не бу- дет выполняться, если конечное значение переменной цикла мень- ше начального. В ФОРТРАНе фирмы IBM при использовании опе- ратора DO циклы выполняются всегда. Ошибки в расстановке семафоров Этот тип ошибок имеет место в тех случаях, когда в ходе вы- числений процесс А ожидает выполнения процесса В, а процесс В — выполнения процесса А. Характерен для систем большой сложности, таких, как операционные системы. Складывающаяся в результате указанных ошибок аварийная ситуация называется самозамыканием или клинчем (deadly embrace). Ошибки в синхронизации операций Такие ошибки возникают в тех случаях, когда две операции зависят друг от друга по времени, т. е. операция В не может на- чаться, пока не будет завершена операция А. Если же операция В начинается преждевременно, то возможно появление ошибок синхронизации. Ошибки этого типа, так же как и ошибки в рас- становке семафоров, называются ситуационными. Ошибки, связанные с неправильным результатом операции Возникают в результате машинных ошибок. Иногда неопытные программисты забывают о том, что машина выполняет арифмети- ческие операции в двоичной системе счисления; это приводит к то- му, что, например, выражение 1.0/5.0 * 5.0 не равно единице. Появляющаяся здесь машинная ошибка рас- познается при прогоне теста следующего содержания: А = 5.0 В = 5.0 IF(1.0/A*B .EQ. 1.0) ...
отладка программ 215 В результате выполнения последнего условного оператора возни- кает непредусмотренная ситуация, и возможно зацикливание. Исчезающие ошибки Еще один тип ошибок, обычно не проявляющий себя до нача- ла эксплуатации программы в рабочем режиме, — это так назы- ваемые исчезающие ошибки, которые внезапно появляются и затем могут не давать о себе знать в течение нескольких месяцев. Эти ошибки не повторяются даже в том случае, если новые прогоны программы выполняются с идентичными данными и на той же са- мой машине. Примером таких ошибок может служить программ- ный ключ, не установленный в начальное положение, но сохраня- ющий обычно правильное значение благодаря специфике работы машины, которая в данной области памяти хранит нули. Может случиться и так, что подобные ошибки будут исчезать с включением средств отладки. Автору, например, пришлось од- нажды безрезультатно потратить два месяца на поиски исчезаю- щей ошибки и в конце концов переписать программу заново. Ошибки с восстановлением Эти ошибки имеют место при отказах оборудования или сис- темного программного обеспечения. Термин «ошибки с восстанов- лением» подчеркивает, что в случае нарушения нормального функ- ционирования системы по той или иной причине (исчезновение питающего напряжения, поломка диска и т. п.) нежелательно пол- ное разрушение или частичное повреждение файлов. Ошибки, связанные с перегрузкой системы Ошибки подобного рода имеют место лишь тогда, когда превы- шается объем внутренних таблиц, буферных накопителей или прочих участков памяти. Эти ошибки губительно действуют на системы, работающие в режиме реального времени, но могут не проявляться годами, пока не создастся соответствующая ситуа- ция. Трудно, например, ответить на вопрос о том, что произойдет, если все пользователи системы, работающей в режиме с разделе- нием времени, попытаются одновременно получить доступ к од- ному и тому же диску? 4.27. ДВУМЕРНОСТЬ ПРОГРАММ В ходе отладки программа должна быть проверена в двух из- мерениях: в пространстве и во времени. Первое представляет со- бой не что иное, как запоминающее устройство ЭВМ, а второе — различные циклы вычислений при выполнении программы. Как -Правило, наиболее длительна, но и наиболее важна проверка во
216 ГЛАВА 4 времени. Отладочные средства предоставляют возможность про- граммисту проконтролировать оба указанных параметра; они яв- ляются как бы стетоскопами, служащими для обнаружения и иден- тификации ошибки. 4.28. СРЕДСТВА ОТЛАДКИ Разнообразие отладочных средств является залогом успеха при отладке программ. Однако программисту редко удается обойтись, стандартными средствами отладки и избежать создания своих, собственных средств. В работе [9] дается описание большого чис- ла программ, часть из которых может использоваться для целей отладки. Наиболее эффективными представляются такие средства отладки, которые вводятся в программу при ее написании. В этом случае места расположения ошибок могут определяться програм- мистом с большей точностью. Существует несколько типов отладочных средств, применяе- мых при программировании: 1. Распечатывание содержимого памяти. 2. Отслеживание хода выполнения алгоритма. 3. Отслеживание обращений к переменным. 4. Отслеживание обращений к подпрограммам. 5. Проверка индексов. 6. Воспроизведение значений переменных. Вывод содержимого памяти на печать предполагает регистра- цию текущего состояния программы в некоторый момент ее вы- полнения. Обычно такая распечатка осуществляется на машинном языке и по целому ряду причин имеет ограниченное применение. Главная из этих причин — трудность сравнения распечатки с ис- ходной программой, поскольку от программиста требуется понима- ние машинного языка и умение сопоставлять записи на этом язы- ке с записями на языке высокого уровня. В том случае, если ком- пилятор оптимизирует исходную программу, дело еще более осложняется, даже несмотря на знание программистом машинного языка. Компиляторы с высокой степенью оптимизации могут про- изводить настолько серьезные перегруппировки операторов внутри программы, что распечатка ее на машинном языке почти не при- носит пользы. Поскольку информация, содержащаяся в распечатке, дается не в той форме, которая нужна для ее практического ис- пользования, наблюдается тенденция к созданию отладочных средств, которые выдают отладочную информацию в более удоб- ном виде. Обсуждение различных типов распечаток содержится в книге Гейнеса '[4]. Отслеживание хода выполнения алгоритма представляет собой регистрацию логического пути выполнения программы. Она может быть использована для проверки правильности выполнения после- довательности операций, заданной программистом, и текущих зна- чений переменных. Обычно различают три вида такого слежения:
ОТЛАДКА ПРОГРАММ 217 для контроля передач управления в программе, для контроля зна- чений переменных и для регистрации вызовов подпрограмм. В пер- вом случае на печать выдаются метки выполняемых операторов, во втором случае — метки изменяющихся переменных и их новые значения. Третий вид слежения оказывается особенно полезным при отладке программ, в которых имеется много обращений к под- программам. При каждом обращении выдается на печать имя под- программы, а при выходе из нее — сообщение о возврате в основ- ную программу. Часто этот вид контроля полностью обеспечивает программиста информацией, необходимой для обнаружения программной ошиб- ки. Недостатками являются связанный с этим большой расход ма- шинного времени, а также то, что иногда количество выдаваемой информации может исчисляться тысячами строк. Достаточно под- робное отслеживание может увеличить время выполнения про- граммы в 10—40 раз. Для преодоления указанных недостатков средства контроля ло- гики программы обычно проектируют так, чтобы их можно было подключать и отключать в определенных точках программы для проверки только нужных частей. Средства отслеживания обращений к переменным разрабаты- ваются с таким расчетом, чтобы печатались не все переменные подряд, а только указанные в конкретном списке. Для языка АЛГОЛ W имеется возможность получать весьма сложные контрольные записи нескольких разновидностей: 1. Постпечать (распечатку, выдаваемую по окончании работы программы) значений всех используемых в программе перемен- ных, если происходит аварийный останов; в противном случае вы- дачи на печать не производится. 2. То же, что в первом случае, плюс подсчет частоты исполь- зования каждого оператора. 3. То же, что во втором случае, плюс покомандное слежение за изменением значений переменных. 4. То же, что в третьем случае, плюс регистрация каждой вы- зываемой переменной. Кроме того, по запросу можно получить контрольную запись работы программной логики и последовательности передач управ- ления. Возможно установление верхнего ограничения на число ре- гистрируемых случаев вызова того или иного оператора. Если, например, верхней границей является число четыре, то слежение за обращениями к операторам будет прекращено после четырех- кратной регистрации. Постпечать может показаться на первый взгляд непримечательным средством, однако на самом деле любая выдаваемая на печать отладочная информация представляется на входном языке: все переменные выдаются под теми же именами и в тех же форматах, которые предусмотрены в описаниях; ответ-
218 ГЛАВА 4 влениям программы присваиваются соответствующие программ- ные метки, а в ходе работы программы выполняемые операторы и связанные с ними переменные обозначаются в распечатках име- нами, присвоенными им в исходной программе. Проверка индексов предназначена для контроля за правиль- ностью индексации именованных массивов посредством сопостав- ления их индексов с объявленными границами массива. Если гра- ницы оказываются нарушенными, печатается сообщение об ошиб- ке. Обычно имеется возможность контроля как всех массивов, так. и заданного их множества. Воспроизведение значений переменных осуществляется посред- ством отладочной команды DISPLAY. Последняя позволяет поль- зователю указывать конкретное место в программе, в котором не- обходимо выдавать на печать значение конкретной переменной. При таком способе выдачи на печать удается производить эту опе- рацию в большей степени избирательно, чем при отслеживании обращений к переменным. Кроме того, по указанной команде обычно печатают имя переменной и ее текущее значение, что обес- печивает полноту выдаваемой информации. Языки КОБОЛ и ПЛ/1 обладают мощными средствами выяв- ления ошибок, позволяющими программисту контролировать ариф- метические ошибки (деление на нуль, переполнение, потеря зна- чимости и т. п.), признаки конца файлов и прочие условия. Так, в языке КОБОЛ есть команда USE, а в ФОРТРАНе — команда ON SIZE ERROR, благодаря которым возможен программный конт- роль весьма серьезных ошибок. ФОРТРАН, используемый в вычи- слительных системах фирмы IBM, имеет расширенные возможно- сти работы над ошибками. В программном обеспечении машины PDP-11 фирмы Digital Equipment для этой цели предусмотрены высокоэффективные стандартные программы на языке БЕЙСИК- ПЛЮС. Аналогичные средства имеются и в ряде других компиля- торов. Используйте доступные средства отладки. 4.29. ОТЛАДКА В ИНТЕРАКТИВНОМ РЕЖИМЕ Многие программы создаются в режиме непосредственного взаимодействия программиста с машиной, и в этих случаях целе- сообразно использовать более широкие возможности, предоставля- емые доступом с терминала для повышения эффективности про- цесса отладки. В подавляющем большинстве методы отладки, применяемые в системах пакетной обработки, пригодны для ис- пользования и в системах с разделением времени, однако особен- ности интерактивного режима функционирования последних требу- ют учета некоторых дополнительных факторов и привлечения ря- да новых средств отладки.
ОТЛАДКА ПРОГРАММ 219 Прежде всего наивно пытаться отлаживать программу в ин- терактивном режиме, не имея листинга, в правильности которого вы убеждены. Приходится только удивляться, как часто програм- мисты этого не понимают, особенно когда в их распоряжение пре- доставлен экранный терминал. Мне доводилось видеть людей, ко- торые, сидя за таким терминалом, неистово меняли строки про- граммы одну за другой, но это не приводило ни к какому резуль- тату. В подобной ситуации невозможно добиться успеха, если нет распечатки, позволяющей понять, что происходит с программой. В системах.с разделением времени часто имеются специальные средства отладки, перечисленные ниже. Однако следует помнить, что такие средства предоставляются далеко не всеми системами с терминальным доступом и что их использование облегчается, если отлаживаемая программа выполняется в режиме интерпретации, а не в машинном коде. Желательно иметь средства отладки тер- минальных систем, предусматривающие следующие возможности: 1. Ключи прерывания, обеспечивающие при определенных усло- виях прекращение выполнения программы, фиксацию ее последне- го состояния и передачу управления пользователю. 2. Прерывания по сигналам об ошибках, предназначенные для передачи управления пользователю в тех случаях, когда в про- грамме делается попытка выполнения каких-либо запрещенных действий (например, извлечения квадратного корня из отрица- тельного числа или нарушения границы массива). Одновременно выдается сообщение, указывающее характер ошибки и номер стро- ки, в которой она находится. 3. Анализ ситуации и внесение изменений в программу, позво- ляющие в случае прерывания по ключу или по сигналу об ошибке проверить или скорректировать значение любой переменной; для этого желательно предусмотреть простые методы выдачи перемен- ных на печать и присвоения им новых значений. 4. Повторный запуск, обеспечивающий продолжение работы программы с любого оператора, в частности с того, при выполне- нии которого произошло прерывание. 5. Модифицирование программы, необходимое для исключения, дополнения или изменения отдельных ее строк; эти действия дол- жны выполняться немедленно, чтобы мог продолжаться процесс отладки. Все описанные средства отладки имеются, например, в языке БЕИСИК-ПЛЮС для ЭВМ PDP-11, и пользователь, для которого эти средства доступны, может осуществлять процесс отладки до- вольно быстро. Очевидно, что по мере расширения применения терминальных систем использование интерпретаторов алгоритми- ческих языков и упомянутых выше средств отладки должно стать обычным делом. Хотя терминальные отладочные средства, несомненно, приносят большую пользу, в результате целого ряда иследований было до-
220 ГЛАВА 4 казано, что отладка протекает быстрее всего, тогда, когда ее не- продолжительные периоды перемежаются с проверками програм- мы за столом. Этому факту есть совершенно очевидное объясне- ние. Дело в том, что простые ошибки обнаруживаются легко и потому могут быть быстро зафиксированы в процессе отладки в ин- терактивном режиме. Если же обнаружение ошибки оказывается делом трудным, то при продолжении работы за терминалом про- граммист может начать вносить беспорядочные, плохо продуман- ные изменения, которые приведут только к новым ошибкам. По- этому в последнем случае лучше всего встать из-за терминала и провести проверку программы за столом. 4.30. ОТЛАДОЧНЫЕ МОДУЛИ ДЛЯ ПРОВЕРКИ ПРОГРАММ Иногда можно добиться облегчения процесса отладки посред- ством использования того или иного отладочного модуля. Напри- мер, если желательна некоторая сложная форма выдачи перемен- ных на печать, то в соответствующих местах программы может быть вставлен специальный модуль, выполняющий эту функцию. Кроме того, применение отладочных модулей упрощает задачу ис- ключения из программы отладочных операторов. Особенно полез- но их использование тогда, когда самостоятельные части програм- мы проходят раздельную тестовую проверку, предшествующую их объединению. 4.31. АВТОМАТИЧЕСКИЕ ПРОВЕРКИ При отладке следует широко использовать всевозможные про- верки, выполняемые машиной автоматически. К ним относятся контроль деления на нуль, арифметического переполнения, ариф- метической потери значимости, длины цепочек символов и значе- ний индексов. В некоторых компиляторах предусматривается воз- можность включения и отключения этих средств контроля в свя- зи со значительными затратами машинного времени на указанные проверки. Средства такого контроля наряду с обнаружением упо- мянутых специфических ошибок часто способны выявлять и серь- езные логические ошибки. 4.32. ПРОГРАММИРОВАНИЕ БЕЗ ОШИБОК Если вы верите, что можете писать программы правильно, так оно и будет. Если же вы убеждены, что написанная вами програм- ма непременно содержит ошибки, — они неизбежны. Многие про- граммисты считают непреложной истиной, что их программа обя- зательно будет испещрена ошибками и что половину их рабочего времени займет отладка. Не сомневайтесь, так оно и будет.
ОТЛАДКА ПРОГРАММ 221 В то же время, если вы ощущаете, что способны писать про- грамму без ошибок с самого начала, вас не будет подводить не- брежность, оправдываемая предвзятым мнением, и вы сможете со- средоточиться на исключении всех ошибок еще до первого цикла компилирования программы. Ниже приводится целый ряд полез- ных рекомендаций, направленных на создание программ без оши- бок. Прежде всего после написания части программы сразу прове- ряйте ее. Если вы ни разу не попытаетесь отперфорировать и вслед за этим выполнить на машине законченный блок програм- мы, то не будет никаких оснований надеяться на то, что в даль- нейшем программа окажется работоспособной. И неудивительно, если потом вы обнаружите целый ряд опечаток или логических ошибок и будете вынуждены подвергать программу многократным испытаниям, пока в ней не исчезнут все явные ошибки. Иногда внимательное прочтение программы до начала работы с ней может избавить вас от необходимости выполнения несколь- ких отладочных прогонов, однако добровольное признание за со- бой права на большое количество ошибок зачастую просто парали- зует вашу способность писать программы правильно. Ведь если вы ожидаете, что программа будет содержать ошибки, вы даже не бу- дете пытаться сделать ее правильной с самого начала. В этом отношении полезны следующие рекомендации: 1. Добивайтесь правильности работы логических узлов прог- раммы посредством последовательного продвижения по блок-схе- ме алгоритма сверху вниз, пошагового уточнения и попутной про- верки. 2. Старайтесь с самого начала избегать синтаксических оши- бок. Разумеется, вы знакомы с подавляющим большинством син- таксических конструкций, используемого языка программирова- ния, однако, если возникает хоть какое-то сомнение, обратитесь- вновь к синтаксическим правилам, подобно тому как вы проверяе- те написание слова по словарю. 3. Не допускайте опечаток. Следите за печатаемым текстом и по окончании подготовки перфоносителей не останавливайтесь пе- ред проведением еще одной проверки. Многие виды опечаток не обнаруживаются компилятором и потому превращаются впослед- ствии в ошибки. Если вы будете строго следовать этим трем советам и концент- рировать свое внимание на выполняемой работе, вы сразу заме- тите, что стали программировать на порядок лучше. Делайте программу правильной с самого начала. Изучение практики программирования показало, что многие программисты тратят на отладку половину своего времени. Следо- вательно, отладка — процесс весьма дорогостоящий, и надо стре-
222 ГЛАВА 4 миться сразу писать правильные программы, чтобы не возникало необходимости в их отладке. И в этом вопросе основное внимание должно быть обращено на то, чтобы вообще не допустить проник- новения ошибок в программу. Если в нашем распоряжении находится программа, в которой при тестировании обнаружено и исправлено 10 ошибок, и програм- ма, в которой в результате тестирования не выявлено ни одной ошибки, то мы склонны в большей степени полагаться на послед- нюю программу. Поэтому лучший путь к сохранению уверенно- сти в хорошем качестве программы — это недопущение в ней оши- бок. 4.33. ПСЕВДООТЛАДКА " Как же узнать, все ли ошибки в программе выявлены? Отве- тить на этот вопрос непросто, поскольку мало что можно сказать даже о том, как производить подобную оценку. Один из известных методов оценки количества необнаруженных ошибок называется псевдоотладкой. Он состоит в том, что в программу случайным об- разом вносятся заранее известные искусственные ошибки и затем по результатам их выявления выносится суждение о степени уст- ранения действительных ошибок. Это значит, что если в програм- ме присутствуют 100 искусственных ошибок, а через несколько дней отладки обнаружено 50 из них, то можно сказать, что мы выявили 50% настоящих программных ошибок. Эта же информация может быть использована и для предсказа- ния количества ошибок, оставшихся необнаруженными. Так, если мы выявили 30 действительных ошибок и 50 искусственных, сле- дует ожидать, что в отлаживаемой программе осталось примерно 30 настоящих ошибок, поскольку искусственные ошибки выявле- ны лишь наполовину. Описанная процедура позволяет предсказать количественные характеристики ошибок, однако гораздо труднее оценить, сколько времени потребуется на устранение в программе большинства оши- бок (например, 95%). Поиск ошибок характеризуется во времени нелинейной зависимостью; эта зависимость представляет собой кривую, уходящую в бесконечность. Так, если мы обнаружили 95% всех ошибок, может оказаться, что на выявление последую- щих 1—2% ошибок нам потребуется времени в два раза больше. Читателям, интересующимся вопросами оценки качества програм- много обеспечения, можно рекомендовать книгу Гилба1* [6]. Полезно хранить в машине данные о количестве выявленных ошибок и времени, затраченном на их обнаружение. Эта инфор- Идеи, изложенные Т. Гилбом, нашли свое развитие в книге Б. Боэма и др. -«Характеристики качества программного обеспечения», которую изд-во «Мир» выпустит в 1981 г. — Прим, перев.
ОТЛАДКА ПРОГРАММ 223 мация может быть выведена на графопостроитель и использована, для прогнозирования момента, к которому проектирование может быть завершено при обеспечении заданного уровня надежности системы. Другим полезным мероприятием является сбор данных о том, какой модуль программы содержит наибольшее количество ошибок. Персонал, занятый эксплуатацией действующих программ,, знает, что большая часть работы по устранению в них ошибок связана с отладкой незначительного числа модулей, и отыскание- ошибок лучше всего вести в окрестностях именно этих модулей. Если количество обнаруживаемых при этом ошибок слишком ве- лико, то дефектный модуль лучше всего переписывать заново. Своевременное выявление любого такого модуля позволяет сэконо- мить массу времени у эксплуатационников. 4.34. ВРЕМЯ, НЕОБХОДИМОЕ ДЛЯ ОТЛАДКИ Наблюдается устойчивая тенденция к недооцениванию необхо- димых затрат времени на отладку программ. Для надежного* определения этого параметра я всегда поступал следующим обра- зом: устанавливал, сколько времени требуется для программирова- ния, удваивал эту величину и принимал ее за единицу. Тогда про- гноз временных затрат выглядит так, как это иллюстрирует табл. 4.2. Таблица 4.2 Относительная продолжительность этапов создания программы Этапы Длительность (в отн. ед.) Разработка алгоритма 1 Программирование 1 Отладка программы 4 Тестирование 1 Обычно никто не возражает против того, что отладка занимает больше времени, чем все остальные стадии создания программы. Вопрос в том, насколько больше. Попробуйте проверить на прак- тике временные соотношения, приведенные в таблице. Вам может показаться, что данные неверны, — тогда составьте свою таблицу. Однако имейте в виду, что если вам удалось сократить продол- жительность отладки, но во время рабочих прогонов программа постоянно выходит из строя, то вы просто занимаетесь самообма- ном.
224 ГЛАВА 4 4.35. ПРЕДОТВРАЩЕНИЕ ОШИБОК Обычно с отладкой связана наибольшая часть затрат по разра- ботке программы, поэтому необходимо стремиться к предупрежде- нию программных ошибок. Некоторые из них можно избежать, ес- ли руководствоваться приводимыми ниже правилами. Не применяйте непроверенных способов программирования. Имейте в виду, что любое новое средство можно считать эффек- тивным лишь тогда, когда имеется твердая уверенность в том, что оно нормально работает. Используйте простейшие операторы. Не пытайтесь обхитрить компилятор или операционную систему. Они настолько сложны, что бывает необыкновенно трудно найти в них такое место, где можно нарушить синтаксические правила и все же продолжать получать верные результаты. Однако если даже предположить, что это удалось сделать,'остается нерешенной дру- гая проблема: как гарантировать работоспособность «хитроум- ной» программы в условиях новых версий или изменений машин- ного языка и операционной системы? Ведь изготовитель ЭВМ сов- сем не обязан над этим задумываться, а обнаружить старую ошибку в новых условиях чрезвычайно трудно, так как со времени на- писания программы может пройти 1—2 года. Старайтесь не использовать принцип умолчания. Во всех язы- ках программирования имеются некоторые стандартные значения переменных, которые компилятор присваивает им по умолчанию. Это экономит трудозатраты программиста, но чревато опасностя- ми, потому что фирмы-изготовители время от времени изменяют систему умолчаний. Программист же при этом может предполо- жить, что он неверно использовал оператор умолчания. Известен случай, когда фирма IBM, для которой характерно частое усовер- шенствование собственных языков программирования, в очередной обновленной версии языка ПЛ/1 изменила стандартную длину па- раметра для одного типа переменных. В результате такого изме- нения многие программы, написанные на этом языке, оказались неработоспособными. Следует отметить также, что механизм дей- ствия операторов умолчания в разных машинах различен. Поэто- му, если желательно обеспечить независимость программы от кон- кретной ЭВМ, то лучше избегать излишне частого использования операторов умолчания. Никогда не допускайте зависимости работы программы от до- стоверности данных. Не следует полагаться на то, что информа- ция, используемая программой, всегда будет вводиться в нужном виде или в заданных пределах изменения. Необходимо организо- вывать контроль данных при вводе для того, чтобы убедиться в их правильности. Данные всегда ведут себя в полном соответствии с законом Мэрфи, который гласит: «Все, что может испортиться, портится». Ошибки в них могут вызываться нарушением инструк- ций. по вводу данных, опечатками при подготовке либо сбоями
отладка программ 225 устройств ввода-вывода. Если такого контроля не проводить, то программа будет периодически самым таинственным образом при- ходить в неработоспособное состояние. И хотя после тщательного анализа ошибки будет показано, что она вызвана входными дан- ными, и программа, и программист прослывут сомнительными. Добивайтесь полноты логических решений. Если некоторый эле- мент данных должен принимать значения, равные 1 или 2, не сле- дует после несравнения его с единицей полагать, что это значение должно быть равно двум. Подобная практика не позволяет предус- мотреть действия программы в ситуациях с неверными данными, которые не так уж редки. Поэтому логика контроля должна быть несколько иной. Вначале надо сравнивать проверяемое значение с единицей, а в случае их неравенства—с двойкой. Однако долж- на приниматься во внимание и такая ситуация, когда значения не совпадают и во второй раз; на этот случай должны быть запро- граммированы соответствующие действия: выдача сообщения об ошибке или аварийный останов. Таким образом, при наличии N возможных условий в программе необходимо выполнять М+1 про- верок, из них N рабочих и одну, направленную на выявление ошиб- ки в данных. Стремитесь минимизировать число обращений к оператору ЭВМ. Никогда не надейтесь на то, что оператор машины будет делать все правильно: если конкретное задание выполняется до- вольно часто, что-нибудь непредвиденное обязательно произойдет. Поэтому, во-первых, не возлагайте на оператора действий, кото- рые можно поручить программе: незачем, например, просить опе- ратора вводить с пультовой машинки дату, если она с успехом мо- жет быть сформирована операционной системой. Во-вторых, кон- тролируйте все действия оператора, предусматривая в программе проверку всех данных и файлов на соответствие их требуемой ин- формации. Дело в том, что оператор может ошибочно установить не ту магнитную ленту либо команды управления заданиями мо- гут вызвать обращение не к тому файлу. 4.36. ЗАКЛЮЧЕНИЕ Списки возможных ошибок, приведенные в этой главе, вероят- но, не смогут ни для кого служить учебным пособием по отладке программ. Однако их можно использовать в качестве справочного материала, помогающего определить, на что надо обратить внима- ние в случае задержки на этапе отладки. Основная концепция процесса отладки заключается в том, что- бы обеспечить выдачу на печать такого объема информации, кото- рый позволил бы легко обнаружить программную ошибку. Опыт- ный программист знает при этом, куда надо вставлять отладочные операторы при программировании; начинающий же просто не за- думывается об этом на стадии написания программы и потому 15—899
226 ГЛАВА 4 тратит впоследствии на отладку существенную часть своего лич- ного и машинного времени. Между тем редкая программа не нуж- дается в применении отладочных операторов. Очень удачно выражена эта мысль у Грюенбергера, который пишет: «Когда отладка завершена, это означает, что программа, несомненно, решает какую-то задачу». Вопрос о том, как убедить- ся, что она при этом решает нужную задачу, рассматривается в гл. 5. 4.37. СОВЕТЫ ПРОГРАММИСТУ Применяйте отладочный компилятор. Первым делом проверяйте программу за столом. Выполняйте эхо-проверку вводимых данных. Вводите средства отладки как можно раньше. Контролируйте правдоподобность вводимых данных. Используйте доступные для вас средства отладки. Делайте программу правильной с самого начала. 4.38. УПРАЖНЕНИЯ ПОВТОРЕНИЕ ПРОЙДЕННОГО . 1. Дайте толкование следующих терминов: авост, план распре- деления памяти, эхо-проверка, стопоры ошибок, детерминирован- ность, таблица перекрестных ссылок, потеря разрядов, брандмау- эры, общее недоверие, МЗМП, ключи прерывания, утверждения, клинч. | 2. Составьте дневник ошибок. Так называется список програм- мных ошибок, с которыми вам приходится сталкиваться в повсед- невной работе. 3. Обсудите достоинства и недостатки, присущие методам от- ладки, рассмотренным в данной главе. 4. Какие еще методы отладки вам известны? 5. Когда следует использовать стандартный оператор TRACE и в каких случаях необходимо вводить в программу специальные операторы слежения? 6. Какие отладочные средства вы предпочитаете использовать? Какие из них для вас доступны? 7. Рассмотрите раздел «Ошибки общего характера». Полон ли приведенный список? Можете ли вы предложить какие-либо из- менения и дополнения? 8. Используя в качестве исходной информации данные раздела «Ошибки специального вида», внесите соответствующие дополне- ния и составьте свой индивидуальный список таких ошибок.
ОТЛАДКА ПРОГРАММ 227 9. В гл. 4 перечислено несколько типов ошибок, которые ком- пилятор не в состоянии обнаружить. Назовите хотя бы пять дру- гих типов аналогичных ошибок. 10. Что такое пространственное и временное измерения про- граммы? Как осуществляется ее проверка в каждом из этих изме- рений при отладке? 11. Назовите несколько причин, по которым использование стандартных подпрограмм упрощает отладку. 12. В чем состоит различие между синтаксическими ошибками и ошибками исполнения? Перечислите по семь примеров ошибок того и другого типа. ЗАДАНИЯ 13. Установите доску объявлений и поместите на ней короткие программные сегменты, в которых имеются труднообнаруживае- мые ошибки. Начните с некоторых из программ, приведенных ни- же. Предложите своим коллегам выявить имеющиеся ошибки и попросите их представить свои собственные программы, содержа- щие ошибки. Если в вашем вычислительном центре выпускается «информационный листок», опубликуйте в нем некоторые из соб- ранных вами материалов. 14. Редактирование входных данных. Файл, формируемый по- средством ввода данных с перфокарт, характеризуется следующи- ми полями: позиции 1—9 — регистрационный номер в системе социаль- ного страхования, позиции 10—30 — фамилия и имя, позиция 31 — пол (1 или 2), позиции 32—50 — номер дома и название улицы, позиции 51—68 — город и штат, позиции 69—74 — почтовый индекс, позиции 75—80 — дата рождения (ЧЧ ММ ГГ). Опишите детально, как должны редактироваться эти данные, чтобы гарантировать их достоверность. Легко ли это сделать на используемом вами языке программирования? 15. Предположим, кто-то принес вам информацию, выданную одной из ваших программ (этот факт установлен достоверно); од- нако распечатка программы отсутствует, а текст не содержит ни- каких идентифицирующие ее признаков. Как бы вы ответили в данном случае на следующие вопросы: а) Какая версия программы была использована? б) Какой массив данных обрабатывался? в) Завершилось ли выполнение программы или она незадолго до конца вышла из строя? г) Сколько записей было обработано? 15*
228 ГЛАВА 4 Что надо сделать, чтобы иметь в программах информацию, необходимую для ответа на эти вопросы? Можете ли вы привести примеры ситуаций, в которых такая информация оказалась бы чрезвычайно полезной? 16. Различают синтаксические ошибки локального характера (относящиеся к единственному оператору) и глобального характе- ра (для выявления которых требуется анализ нескольких операто- ров). Примером глобальной ошибки является указание в операто- ре GO ТО не той метки. Подберите еще примеры глобальных и ло- кальных синтаксических ошибок. 17. Псевдоотладка. При отладке своей программы попросите кого-нибудь из коллег внести в нее пару ошибок. После этого, зная о наличии искусственных ошибок и о времени, необходимом для их обнаружения, оцените, как долго вам придется искать дру- гие, настоящие ошибки. 18. Припомните какую-нибудь ошибку, которая появилась в начале эксплуатации вашей программы. Какие отладочные сред- ства могли бы в свое время помочь выявить эту ошибку? Что вам следовало сделать для того, чтобы такая ошибка не возникла или была обнаружена до начала функционирования программы в ра- бочем режиме?- 19. Займитесь изучением своих ошибок. Для этого примите за правило ведение дневника ошибок, в который вы записываете ин- формацию о встретившихся программных ошибках. По прошествии некоторого времени такой дневник сможет сообщить вам интерес- ные сведения о том, какой тип ошибок для вас наиболее характерен. Используйте эту информацию при отладке других программ. 20. Какие проверки (подобные контролю индексов) вы хотели бы осуществить с помощью вашего компилятора на этапе выпол- нения программ? 21. Какие из перечисленных ниже средств отладки предусмот- рены в используемом вами компиляторе? а) Отслеживание хода выполнения алгоритма. б) Включение и выключение средств слежения за логикой вы- полнения программы. в) Отслеживание обращений к переменным. г) Выборочное отслеживание обращений к переменным. д) Отслеживание обращений к подпрограммам. е) Распечатывание содержимого памяти. ж) Воспроизведение имен и значений переменных. 22. Если в используемом вами компиляторе предусмотрена про- верка области значений индексов, сумеет ли он обнаружить ошиб- ку, заключающуюся в том, что вместо объявленной размерности массива А (3,4) будет указана размерность А (4,2)? 23. Имеется ли отладочный компилятор для того языка, на ко- тором вы программируете? Если да, то оцените, оправданно ли его приобретение?
ОТЛАДКА ПРОГРАММ 229 24. Если вы используете язык программирования, в котором имеются блоки операторов (например, DO, BEGIN), способен ла компилятор устанавливать глубину вложения блоков? 25. Разработайте для своих программ таблицу сообщений об. ошибках с указанием их вероятных причин по следующему об-- разцу: Сообщение об ошибке Вероятная причина Превышено отведенное время Зацикливание Переполнение Неопределенная переменная 26. Какие из перечисленных ниже проверок выполняются на этапе компилирования в используемом вами компиляторе? 1) Упорядоченность исходной колоды перфокарт. 2) Неиспользуемые метки. 3) Неиспользуемые операторы. 4) Неиспользуемые переменные. 5) Потеря разрядов при пересылках. 6) Оператор, передающий управление самому себе. 7) Передача управления в середину цикла DO (FOR). 8) Соответствие типа переменной спецификации формата. 9) Несоответствие типа аргумента характеру функции. 10) Попытка рекурсивного использования нерекурсивной под- программы. 11) Попытка использования подпрограммы как функции и на- оборот. 12) Неправильное число индексов. 13) Запрещенная последовательность битов. 14) Правильность описателей операций сравнения. 15) Совместимость присвоений. Расположите перечисленные виды проверок в порядке их важ- ности для вас. Проведения каких других видов проверок вам хо- телось бы от вашего компилятора? Позволит ли логическая струк- тура компилятора их осуществить? 27. Какие из йеречисленных ниже проверок осуществляются ва- шим компилятором на этапе выполнения оттранслированной про- граммы? 1) Использование переменной, для которой не задано началь- ное значение. 2) Запрещенный индекс. 3) Переполнение при операциях над действительными числами. 4) Потеря значимости при операциях над действительными чис- лами.
230 ГЛАВА 4 5) Переполнение при операциях над целыми числами. 6) Переполнение порядка числа. 7) Потеря значимости порядка числа. 8) Деление на нуль. 9) Неопределенность вида 0**0. 10) Правильность указанного количества параметров подпро- граммы. 11) Правильность описателей параметров подпрограммы. 12) Нарушение ограничений по длине цепочки символов. 13) Ошибки преобразований. 14) Проверка соблюдения границ действия операторов, если они строги. Расположите указанные виды проверок в порядке их важно- сти .для вас. Какие еще виды проверок вы хотели бы обеспечить с помощью вашего компилятора? Позволит ли логическая структура компилятора их осуществить? 28. Предусмотрены ли в вашем компиляторе перечисленные ни- же средства, облегчающие отладку? а) Список всех переменных, упорядоченный по алфавиту (план распределения памяти). б) Список всех используемых констант. в) Таблица описателей переменных. г) Список используемых функций. д) . Список используемых подпрограмм. е) Полный список переменных с указанием всех операторов, в которых используется каждая из них (таблица перекрестных ссылок). ж) Полный перечень используемых меток с указанием всех операторов, обращающихся к каждой из них. Расположите перечисленные средства в порядке их значимости для вас. Какие еще средства вы желали бы иметь? Позволит ли логическая структура компилятора их реализовать? 29. Что такое глитч? Известны ли вам примеры ошибок этого вида? Поинтересуйтесь у других программистов, знают ли они, что такое глитч? 30. В программе можно использовать множество ухищрений. Например: DIMENSION X (25), Y (20) DO 10 I = 1, 45 X(I) = 0.0 10 CONTINUE При выполнении этой программы будет присвоено нулевое значение как массиву X, так и массиву Y, поскольку они распо- ложены один за другим. Однако если в дальнейшем будут измене- ны размеры одного из них, то возникает серьезное затруднение,
ОТЛАДКА ПРОГРАММ 23Г так как факт присвоения нулевых значений массиву Y скрыт от глаз пользователя программы. Подобный трюк осуществим во многих языках программирования. Попробуйте применить его в своей программе. Найдите ряд других примеров программистских ухищрений. В каких случаях их применение оправданно, если оно вообще допустимо. Возникновения проблем какого характера сле- дует при этом ожидать программисту, если впоследствии он захо- чет модифицировать программу? 31. Самомодифицирующиеся программы. В большинстве язы- ков программирования имеются команды, позволяющие изменять исходную программу в процессе ее выполнения. Например, ФОРТРАН'. ASSIGN 16 ТО I GO ТО I КОБОЛ: PARAGRAPH-1. GO ТО BYPASS-PARAGRAPH. ALTER PARAGRAPH-1 TO PROCEED TO PARAGRAPH-OTHER. Того, кто должен читать программу, содержащую подобные блоки, ожидает неприятная работа, связанная с попытками разо- браться, какая же ветвь была выполнена в действительности. По- этому введение операторов, модифицирующих программу, не на- ходит горячей поддержки у специалистов по программированию. Согласны ли вы с тем, чтобы исключить эти операторы из языка? Когда, по вашему мнению, подобные операторы необходимы? 32. Выявив очередную труднообнаруживаемую ошибку в од- ной из своих программ, проанализируйте, каким образом возникла эта ошибка. Сделайте краткое описание ошибки и проследите путь ее появления. 33. Существует мнение, что число ошибок в программе про- порционально количеству содержащихся в ней меток. Какова ва- ша точка зрения относительно справедливости этого утверждения? 34. Занимаясь поиском ошибки, фиксируйте последовательность своих действий, направленных на ее выявление. Со временем вы сможете получить четкое представление о своей стратегии отлад- ки. Если подобный анализ вас интересует, прочтите статью Дж. Гоулда «Психологические аспекты процесса отладки людьми ма- шинных программ»1. 11 Gould J. D, Some Psychological Evidence on How People Debug Computer Programs, International Journal of Man-Machine Studies, № 7, 151—182 (1975).
232 ГЛАВА 4 35. Допустим, что вы захотели перепроектировать некоторую ЭВМ таким образом, чтобы высказывание X=Y было истинным не только в случае равенства, но и тогда, когда обе переменные отличаются лишь последним младшим разрядом. Как, по-ваше- :му, хороша ли эта идея? Если ее реализовать, то будет ли из ра- лвенств а = Ь и Ь — с следовать тот факт, что а = с? Приведите по нескольку примеров утвердительного и отрицательного ответов на 1Этот вопрос. ПРОГРАММЫ 36. Напишите небольшую программу (не более 50 строк). По- соревнуйтесь с другими своими коллегами в части написания про- граммы без ошибок и посмотрите, при компилировании чьей про- граммы машиной будет выдано наибольшее количество сообще- ний об ошибках. 37. Составьте небольшую программу (не более 50 строк), ко- торая будет проходить этап компилирования без синтаксических ошибок, но производящую аварийный останов, прежде чем завер- шится ее выполнение. Тривиальный способ добиться такого ре- зультата— вызвать действие, подобное делению на нуль. Укажи- те, каким образом вы можете это сделать. 38. Отдайте вышеупомянутую программу кому-нибудь из своих коллег и попросите его обнаружить ошибку. 39. Возьмите небольшую программу и модифицируйте ее так, чтобы результат оказался заведомо неправильным. После этого отдайте программу кому-нибудь для использования и проследите, обнаружит ли пользователь вашу ошибку. 40. Напишите небольшую программу с неправильной записью некоторых служебных слов. Сколько различных типов сообщений об ошибках может быть вызвано таким дефектом? 41. Напишите программу, в которой некоторый массив исполь- зовался бы без объявления. Сколько различных типов сообщений об ошибках появится в результате такого нарушения правил? Укажет ли хоть какое-нибудь из них на причину ошибки? 42. Напишите программу, которая приводила бы к максималь- но возможному количеству выдаваемых компилятором различных сообщений об ошибках. Выданные сообщения классифицируйте по типам, например: а) Ошибка обнаружена, и сообщение правильно ее иденти- фицирует. б) Ошибка обнаружена, но печатается неправильное сообщение о ней. в) Ошибка отсутствует, но сообщение о ней выдается: Встретились ли вам явные ошибки, не сопровождавшиеся вы- дачей соответствующих сообщений?
ОТЛАДКА ПРОГРАММ 233 43. Напишите ряд программ, в ходе выполнения которых воз- никали бы перечисленные ниже ситуации, и проанализируйте действия, которые они вызовут в используемом вами языке про- граммирования. а) Деление на нуль. б) Переполнение и потеря значимости. в) Превышение размерности массива. (Начните с незначитель- ного превышения, затем постепенно увеличивайте его. Например, можно объявить массив размерностью 10 элементов и попытаться присвоить нулевые значения сначала 11 элементам, а потом — 1000. Что при этом произойдет?) г) Рекурсивный вызов нерекурсивной программы. д) Указание недостаточного или избыточного количества па- раметров вызываемой подпрограммы. е) Несогласованность типов переменных, например использо- вание в программе действительного числа вместо целого. 44. Пусть F2= 1,36103. Проведите 100 раз с одной и той же точностью вычисление функции F2 = (F2* 1.66673)/! .66673 Какой должен быть выдан ответ? Проделайте то же самое 1000, 10 000 и 100 000 раз, обращая внимание на печатаемый ре- зультат. Ошибки отбрасывания значащих разрядов будут сказы- ваться на нем лишь в том случае, если точность машины не пре- вышает семи разрядов. Для ЭВМ, обладающих большей точно- стью, попробуйте увеличивать число значащих разрядов в кон- стантах приведенного примера. 45. Выясните, каковы максимальное и минимальное значения целых чисел, которые могут быть представлены в используемой вами ЭВМ. Проделайте то же самое в отношении действительных чисел, определив максимальное и минимальное значения отдель- но для мантиссы и порядка. Напишите программу, позволяющую обнаруживать факт выхода обрабатываемых чисел за допустимые пределы. 46. На основе использования логических операторов постройте программы, способные выполнять следующие проверки: а) При всех ли значениях X справедливо равенство Х°=ХО(Г= б) При всех ли значениях X справедливо равенство SQRT(X) = = Х°.5 = /(1-0/2,0). в) При всех ли значениях X справедливо равенство Х=Хг — г)# При всех ли значениях X справедливо равенство X* Х=Х2= ^д) При всех ли значениях X справедливо равенство Х*Х*Х*=
234 ГЛАВА 4 е) При всех ли значениях X и Y справедливо равенство Х/У*У=Х. ж) При всех ли значениях X справедливо равенство sin2X+ +cos2X=l. 1000 з) Справедливо ли равенство 1000 *(1.0/5.0) =2(1.0/5.0). Z=1 Используйте только действительные значения X и У. Придумайте другие примеры подобных «логически правильных» утверждений и проверьте их. 47. Напишите программу вычисления выражения Y=A+B-^C гдеЛ =—2 500 000,00; В = 0,01; С=2 500 001, 00. Правильный ли •ответ выдает машина? Если нет, то почему? Можете ли вы изменить последовательность действий таким образом, чтобы результат полу- чался правильным? 48. На основе применения логических операторов постройте программы, способные выполнять следующие проверки: а) При всех ли значениях А и В имеет место равенство — —А = В. б) При всех ли значениях А, В, С справедливо утверждение А(В—С)=АВ—АС. в) При всех ли значениях А, В, С справедливо утверждение (А—В)/С=А/С—В/С. г) При всех ли значениях А, В, С справедливо утверждение (Л-)-В)+С=Л + (В+С). Используйте только действительные значения А, В и С. 49. Напишите программу, выполняющую следующие опе- рации: а) Суммирование 0,1 десять раз. б) Суммирование 0,01 сто раз. в) Суммирование 0,001 тысячу, раз. г) Суммирование 0,0001 десять тысяч раз. д) Суммирование 0,000001 миллион раз. Выдавайте на печать максимально возможное число значащих раз- рядов. Насколько близки к 1 результаты проведенных вычисле- ний? Почему некоторые из результатов неточны? 50. Запрограммируйте вычисление 2 1/п3 посредством после- довательного суммирования чисел от /1=1 к п=1000 и в обрат- ном порядке. Какая из сумм оказалась ближе к точному ответу? 51. Многие математические функции имеют соответствующие им обратные выражения. Иногда при вычислении обратной функ- ции вследствие отбрасывания значащего разряда не обеспечива- ется получение исходного значения. Напишите программу, позво- ляющую установить, всегда ли в результате выполнения указан-
ОТЛАДКА ПРОГРАММ 235 вых ниже действий получается исходное значение переменной (значения X выбираются случайным образом): a) Y=SQRT(X) и затем X=Y Y б) Y=LOG(X) и затем X = EXP(Y) в) Y=SIN(X) и затем X=ARSIN(Y) г) Y=COS(X) и затем Х=ARCOS(Y) Можете ли вы продолжить список функций, для которых воз- можны аналогичные проверки? Есть ли какая-нибудь общая осо- бенность в результатах всех проведенных вычислений? С какими числами возникает больше затруднений: с большими (либо близ- кими к числу л) или' с малыми? 52. Напишите программу для вычисления каждой из следую- щих сумм: 1,1 1 । 1 1 а> 2 + 3 — 4 + 5 10000 111 1 б) ~~ 10000 + 9999 — 9998 2 +1 ч (, 1 Г41 1 \ { 1 1 1 1 \ в) + 3 +ir+ + 9999 \Т+Т + 6 т------------Ь 10000 ) ( 1 1 1 \ / 1 1 1 1 V г) ( 9999 +l9997 -3 +Ч~I 10 000 + 9998 ----Ь 4 + 2 / Заметьте, что все эти выражения представляют собой одну и ту же сумму. Почему тогда ответы получаются разными? Какой из результатов правилен? 53. Перед записью в память ЭВМ константы, являющиеся дей- ствительными числами, представленными в десятичной системе счисления, должны быть переведены в систему счисления, исполь- зуемую в машине. В некоторых компиляторах в случае большого количества значащих разрядов в преобразуемом числе результа- ты чтения и записи констант оказываются неодинаковыми. По- пробуйте на любом языке выполнить следующую программу: А1 = 1111111111.111 READ А2 WRITE Al, А2 END $DATA 1111111111.111 Напишите свою программу, аналогичную представленной здесь, и попытайтесь поработать с константами, имеющими боль- шие значения. Всегда ли для А1 и А2 будут печататься одинако- вые числа? 54. При пользовании стандартными подпрограммами в различ- ных языках часто дает о себе знать одна особенно неприятная! ошибка. Приведем следующую программу.
236 ГЛАВА 4 ФОРТРАН: CALL SUBA (2,К) WRITE (6,10) К 1=2 WRITE (6,10) I 10 FORMAT (IX,15) STOP END SUBROUTINE SUBA(L, M) L=L+L M=L RETURN END ъ) Внимательно ознакомьтесь с программой и определите, ка- кие значения должны выдаваться на печать. б) Отперфорируйте программу и проверьте, как она работает. Соответствует ли печатаемый ею результат тому, который вы ожидали? в) Если фактический результат отличается от ожидавшегося, то чем вы можете это объяснить? Каким образом можно избежать ошибок подобного типа? 55. Ниже приводятся программа и ее выходной результат. а) Разберите текст программы и определите, какие результа- ты должны ею выдаваться в действительности. Правилен ли от- вет, приведенный в конце программы? б) Почему печатаемый результат может быть неверен? Если вам не удается найти ошибку, отперфорируйте программу и от- ладьте ее. FACTOR: PROCEDURE OPTIONS (MAIN); /• FIND FACTORIAL OF 5 */ FACT: PROCEDURES RECURSIVE; IF (N> 1) THEN RETURN (N*FACT(N—1)); ELSE RETURN(l); END FACT; M = 5; X = FACT(M); PUT DATA(X); END FACTOR; Выданный результат: X = 5.00000E 4- 00;
ОТЛАДКА ПРОГРАММ 237 56. Использование оператора присваивания, который одной и той же переменной присваивал бы сразу два значения, нежела- тельно. Такой оператор мог бы иметь следующий вид: (А,А)=(2, 3) Можете ли вы предложить какой-нибудь другой способ реализа- ции такого присваивания в используемом вами языке программи- рования, подобный приводимому ниже: CALL SUBA (А,В,А) PRINT А STOP END SUBROUTINE SUBA (X,Y,Z) X = 1.0 Y = 2.0 Z = 3.0 RETURN END Как вы думаете, какое значение переменной А будет выдаваться на печать в этой программе? Запрограммируйте те же операции на языке, с которым вы предпочитаете работать. 57. Какой результат будет выдаваться на печать по окончании следующей программы? А = 2 CALL SUBA(A,A) ' WRITE(6,10) А 10 FORMAT(1X, 'А = ', F5.1) STOP END SUBROUTINE SUBA(A.B) A = A -f-B**3 IF (A .LT. SQRT(B) ) В=5.15 RETURN END Если вы можете согласиться с правильностью выдаваемого ре- зультата, запрограммируйте ту же задачу на наиболее удобном Для вас языке. 58. Ниже приводится программа и выдаваемый ею результат, а) Разберите текст программы и определите, какие результаты должны ею выдаваться в действительности. Правилен ли ответ, приведенный в ее конце?
238 ГЛАВА 4 б) Почему приведенный результат может быть неверен? Если вам не удается найти ошибку, отперфорируйте программу и от- ладьте ее. LOOP: PROCEDURE OPTIONS (MAIN); DECLARE X(10) DECIMAL FLOAT; DO Y = 0.1 TO 1.0 BY 0.1; I = 10*Y; X(I)=Y; END; PUT EDIT ((X(I) DO I = 1 TO 10)) (SKIP, F(8,2)); END LOOP; Выданный результат: 0.20 0.30 0.40 0.50 0.60 0.70 0.80 0.90 1.00 29312.00 ПРОЕКТЫ 59. Ранее было отмечено, что отладка облегчается, если про- граммы имеют небольшой объем. Но насколько малыми они должны быть? Ведь каждый отдельный оператор можно сделать самостоятельной программой. Однако с увеличением количества программ увеличивается и число необходимых интерфейсов меж- ду ними, а отсюда, вероятно, и количество ошибок. Можете ли вы внести какие-либо предложения относительно рационального чис- ла подпрограмм и общего объема программы? Решение этой за- дачи гораздо сложнее, чем может показаться на первый взгляд. 60. Напишите программу, которая анализировала бы програм- му на входном языке и затем выводила бы на печать все неис- пользуемые операторы, метки и неопределенные переменные. 61. Исследуйте, какие типы и разновидности средств отладки, предусмотренных используемым вами языком программирования, доступны на вашем вычислительном комплексе (очевидными яв- ляются такие средства, как отладочные компиляторы, программы выявления перекрестных ссылок, программы-профилировщики и
ОТЛАДКА ПРОГРАММ 239 отладочные системы). Подготовьте техническую документацию в форме простого описания, в котором излагались бы существую- щие в вашем вычислительном центре возможности отладки про- грамм. 62. Для любого языка программирования можно разработать набор правил, позволяющих избежать программных ошибок. Со- здайте такую сводку правил для используемого вами языка. По- кажите ее своим коллегам и посмотрите, найдутся ли у вас еди- номышленники. 63. В этой главе описана программа выявления перекрестных ссылок. Напишите аналогичную программу для вашего языка про- граммирования. 64. Наиболее легкий (но и наиболее рациональный) путь вы- полнения предыдущего задания состоит в том, чтобы найти со- ответствующую готовую программу, пригодную для используемо- го вами языка. Если вам удастся это сделать, тщательно проверь- те такую программу и сделайте ее общедоступной для всех поль- зователей вашей машины. 65. Программное обеспечение, поставляемое изготовителями ЭВМ, всегда содержит ошибки. Часто составляют целые списки возможных тупиковых ситуаций с указанием обстоятельств их возникновения. Такие списки никогда не называют «Списком оши- бок» или «Дефектными ведомостями». Фирма IBM, например, присвоила им такие названия, как «Заблаговременные предупреж- дения» и «Памятка по системе программирования». Фирма Di- gital Equipment называет списки ошибок «Примечаниями к про- граммному обеспечению». Отыщите несколько таких списков по какой-нибудь из фирм и путем сравнительного анализа установи- те, увеличивается, сокращается или остается постоянным количе- ство ошибок со временем. Если вы можете узнать также и коли- чество команд в тех или иных пакетах программ, то имеет смысл вычислить для различных элементов программного обеспечения показатель «плотности ошибок», разделив общее количество оши- бок на число команд. 66. В любом языке программирования определенные свойства языка порождают частые ошибки, называемые характеристиче- скими. Так, например, в ФОРТРАНе в отличие от АЛГОЛа не всегда требуется описание переменных. Поэтому на основании вы- ражения MISTAKE=MISTEAK+1 переменные MISTAKE и MISTEAK в ФОРТРАНе будут рассмат- риваться как разные, и ошибка останется не обнаруженной. В АЛГОЛе же в связи с тем, что описание переменных — условие обязательное, ошибка будет обнаружена. Выявите некоторые ошибки, свойственные используемому вами языку программиро- вания.
240 ГЛАВА 4 67. Стандартизация диагностических сообщений и сообщений об ошибках. В любой нетривиальной программе обычно выявля- ются те или иные синтаксические или семантические ошибки. Со- временные компиляторы обнаруживают подавляющее большинство таких ошибок, однако нет двух компиляторов, которые оповеща- ли бы о них одинаковым способом. Сравните диагностические средства двух каких-нибудь компиляторов либо для одного и то- го же языка, либо для различных языков. После этого разрабо- тайте ряд правил для стандартизации диагностических сообщений по содержанию и по форме. Установите также, как и где должны предусматриваться диагностические сообщения. 68. Пакеты отладки. Изучите какие-либо отладочные пакеты» примерами которых в КОБОЛе фирмы IBM является оператор DEBUG, а в ФОРТРАНе той же фирмы — оператор АТ. С други- ми пакетами отладки можно ознакомиться в книге Растина [11]', посвященной методам отладки больших систем программного обеспечения. Сделайте обзор существующих пакетов отладки для общедоступных программ, найдите в них общие черты и определи- те, какими дополнительными возможностями обладают некоторые из них. Установите, какой отладочный пакет наиболее подходит для ваших целей. Разработайте ряд условий, которым должна, на ваш взгляд, удовлетворять хорошая система отладочных средств. 69. Подпрограммы обработки ошибок. Изучите доступные в современных компиляторах подпрограммы обработки ошибок. Примерами подобных программных средств являются в языке ПЛ/1 переход по оператору ON, а в языке БЕЙСИК, разработан- ном для машины PDP-11 фирмой Digital Equipment, переход по оператору ON ERROR. Наличие таких средств (подпрограмм) предоставляет пользователю широкие возможности для того, что- бы эффективно справляться с самыми различными ситуациями, возникающими в результате появления ошибок при выполнении программы. Сделайте обзор существующих подпрограмм обра- ботки ошибок и попытайтесь выработать основные требования к ним. 70. Соберите статистические данные относительно того, с ка- кими свойствами языков программирования связано наибольшее количество программных ошибок. Один из возможных способов решения этой задачи — анализ большого числа забракованных листингов с последующей классификацией обнаруженных в них ошибок. 71. Легко написать вспомогательную программу, которая будет считывать основную программу на входном языке и выявлять в ней всевозможные грубые ошибки кодирования, такие, как а) необъявленные переменные, б) непонятные коды, в) неиспользуемые переменные или метки,
ОТЛАДКА ПРОГРАММ 24Г г) переменные, использованные только один раз. Составьте список грубых ошибок кодирования, которые могла бы отмечать указанная программа. Основой этого списка может по- служить классифицированное множество ошибок из предыдуще- го задания. 72. Более труднообнаруживаемыми ошибками кодирования по сравнению с перечисленными в п. 71 являются ошибки вида Т = (арифметическое выражение) Здесь размещены операторы, не содержащие обращения к Т, которые являются правильной частью программы Т= (другое арифметическое выражение). Поскольку в данном случае Т переопределено, не будучи ни разу использованным, налицо ошибка. Как, по-вашему, важно ли для программиста, чтобы компилятором выявлялись ошибки подобно- го рода? Принимая во внимание такие ошибки и ошибки, рас- смотренные в п. 71, составьте их список, упорядоченный по сте- пени важности обнаружения соответствующих ошибок компиля- тором, и определите возможность реализации необходимых для этого проверок. 73. Отладка программы не ее автором. Зачастую программист не сам отлаживает собственную программу, а это делает кто-ли- бо другой. Одно из преимуществ подобного метода работы со- стоит в том, что программисты, зная о предстоящей отладке их программы другим лицом, обращают особое внимание на ее удо- бочитаемость и полноту документирования. Эти свойства про- граммы облегчают и ее дальнейшую эксплуатацию. Выберите для себя и предложите выбрать кому-нибудь из своих коллег неко- торые из заданий, помещенных в данной книге, запрограммируйте соответствующие задачи и затем выполните отладку программ друг друга. Составьте краткий отчет о результатах этого экспери- мента. 74. Ограничения на использование компилятора. Любой ком- пилятор имеет определенные ограничения по применению. Обычно границы использования настолько широки, что их редко прини- мают в расчет. Так, например, один из широко распространенных компиляторов допускает использование в пределах одного опера- тора порядка 400 круглых скобок. Представляет интерес отыска- ние подобных ограничений в компиляторе, которым пользуетесь вы, например: а) максимально допустимого количества круглых скобок в од- ном операторе; б) максимально допустимого размера одномерного массива' и максимально возможной размерности массива; -£99
242 ГЛАВА 4 в) максимально возможной длины литералов или разрядности цифровых констант; г) максимально допустимой длины отдельного оператора; д) максимально возможной длины одного комментария или максимального количества следующих подряд комментариев; е) максимально допустимого числа вложений операторов DO, блоков операторов или условных операторов IF THEN ELSE; ж) максимально возможного количества подпрограмм в про- грамме (или вложенных обращений к подпрограммам); з) максимально допустимого числа аргументов подпрограммы; и) максимально возможного количества рекурсивных вызовов. Попытайтесь дополнить этот список ограничений. (Указание: проанализируйте список сообщений об ошибках, предусмотренных в используемом вами компиляторе.) ЛИТЕРАТУРА 1. Barron D. W., Programming in Wonderland, Computer Bulletin, 15 (1971). 2. Boar В. H., Abend Debugging for COBOL Programmers, New York, Wiley-In- terscience, 1976. 3. Brown A. R., Sampson W. A., Program Debugging, New York, American Else- veir, 1973. 4. Gaines R. S., The Debugging of Computer Programs, Princeton, N. J., Insti- tute for Defense Analysis, August 1969. 5. Geller D., Debugging Other Languages in APL, Software — Practice and Ex- perience, Vol. 5, No. 2 (April — June 1975). 6. Gilb T., Software Metrics, Cambridge, Mass., Winthrop Publishers, Inc., 1977. 7. Halpern M., Computer Programming: The Debugging Epoch Opens, Compu- ters and Automation (November 1965). 8. IEEE Symposium on Computer Software Reliability, New York, IEEE Compu- ter Society, 1973. 9. Kernighan B. W., Plauger P. J., Software Tools, Reading, Mass., Addison- Wesley, 1976. 10. Poole P. C., Debugging and Testing, Advanced Course in Software Engineering, New York, Springer-Verlag, 1973. 11. Rustin R. (ed.), Debugging Techniques in Large Systems, Englewood Cliffs, N. J., Prentice-Hall, 1971. 12. Satterthwaite E., Debugging Tools for High Level Languages, Software — Practice and Experience, Vol. 2 (1972). 13. Shooman M. L„ Bolsky M. I., Types, Distribution, and Test and Correction Ti- mes for Programming Errors, Proceedings of 1975 International Conference on Reliable Software, New York, IEEE, 1975. 14. Weinberg G. M., The Psychology of Computer Programming, New York, Van Nostrand Reinhold Co., 1971.
Тестирование призвано указывать на наличие, а не на отсутствие ошибок. Дейкстра О тестировании необходимо думать на протяжении всего периода разработка программы. Глава 5 ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ Если тестирование программы проводится интуитивно, по прин- ципу невмешательства в работу программы и без какого-либо четкого плана испытаний, то этот процесс можно назвать искус- ством. Если же тестированию предшествует тщательный подбор данных для контрольных примеров и заблаговременный выбор эле- ментов программы, подлежащих проверке, а само оно выполняет- ся последовательно и аккуратно, то тестирование становится наукой. Разработка алгоритмов и программирование стали ремеслом, которым можно овладеть. Теперь пришло время сделать то же самое в отношении тестирования программ, покончив с подходом к нему как к искусству. Это позволит исключить повторения ис- пытаний, обеспечивать контроль за их эффективным проведени- ем и четко определять момент завершения тестирования. Испытания любой системы всегда представляют собой один из наиболее ответственных этапов ее разработки и часто бывают связаны с наибольшими трудностями и наибольшими потерями времени. Отладка и тестирование — это два четко различимых и непохожих друг на друга этапа. В первом случае происходит устранение синтаксических ошибок и явных ошибок кодирования. Во втором случае имеют дело с программой, не содержащей от- меченных ошибок, которая выдает какие-то правильные резуль- таты. Основная цель выделения отладки и тестирования как от- дельных этапов создания программы заключается в том, чтобы обратить внимание на обязательность обеих стадий и на необхо- димость специального планирования временных затрат по каж- дой из них в отдельности. Никогда не делайте вывода, что программа правильна, лишь на том основании, что она не отвергнута машиной, полностью транслирована и выдала численные результаты. Ведь все, что достигнуто в данном случае, — это получение некой выходной ин- формации, не обязательно правильной. В программе все еще мо- 16»
244 ГЛАВА б жет оставаться большое количество логических ошибок, а между тем задача, которая ставится при написании программы, — это не просто получение ответов, но получение правильных ответов. По- этому обычно бывает необходима «ручная» проверка машинных результатов. После того как отладка полностью завершена, даже в программе опытного программиста существует примерно одна ошибка на 20—30 написанных операторов. Эти ошибки могут быть как катастрофическими по своим последствиям, так и не- значительными и могут быть связаны как с неправильным алго- ритмом, так и с несущественными ошибками кодирования. Таким образом, отлаженная программа — это программа, для которой просто не нашлось подходящего набора тестовых данных, чтобы привести ее к отказу. 5.1. НЕБРЕЖНОСТЬ НАЧИНАЮЩИХ ПРОГРАММИСТОВ Программисты-новички не осознают того факта, что программы нельзя считать правильными до тех пор, пока это качество не доказано. Если тестированию не было уделено должного внима- ния, то непроверенная программа впоследствии, когда она начи- нает выдавать явно неверные результаты, становится источником серьезных затруднений. При этом, если такая программа уже на- ходилась некоторое время в эксплуатации и заслужила доверие пользователей, то достоверность -всех предыдущих результатов также подвергается сомнению. Именно в такой момент програм- мисты обнаруживают, что они только отладили программу, но не выполнили надлежащего ее тестирования. Программисты-непро- фессионалы работают над программой до тех пор, пока не доби- ваются от нее выдачи результатов (которые не всегда бывают верными), а после этого приступают к какой-нибудь новой раз- работке. 5.2. ПРОБЛЕМА ЖИВУЧЕСТИ ПРОГРАММЫ Цель тестирования всякой программы состоит в том, чтобы убедиться, что она решает действительно ту задачу, для которой предназначена, и выдает правильный ответ при любых условиях. Последнее требование обычно связывают с живучестью (robust- ness)0 программы и говорят, что она не обладает этим свойством, если .легко перестает формировать правильные результаты. Жи- вучесть программ со временем возрастает, так как в процессе их эксплуатации отказы обнаруживаются и устраняются. Иногда программа может казаться работоспособной в течение многих месяцев и даже лет, пока не становится очевидным, что в каком-то ее блоке имеется серьезная ошибка. Теоретически счи- 1) Этот термин обычно переводят как «робастность». — Прим, перев.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 245 тается, что большие программы никогда не бывают полностью свободными от ошибок, поэтому никогда не следует жертвовать качеством тестирования во имя мнимой экономии времени и де- нег. Неправильная программа не имеет никакой ценности, и луч- ше вообще не иметь никакой программы, чем принимать на ее ос- нове ошибочные решения, которые могут стоить очень дорого. До- статочно представить себе, во что обойдется, например, полное повторение обработки годового объема входных данных из-за недостаточно тщательно проверенной программы, чтобы ощутить необходимость ее полного тестирования. Живучая программа — это такая программа, которая продол- жает сохранять свою работоспособность, несмотря на рассеян- ность операторов подготовки данных, небрежность персонала, от- ветственного за контроль информации, и безграмотные действия операторов ЭВМ. Тот, кто принимает во внимание закон Мэрфи, который, естественно, распространяется и на программное обес- печение, будет создавать программы, обладающие свойством жи- вучести. Разумеется, невозможно дать абсолютно точные указания от- носительно того, как надо испытывать программные средства, од- нако ряд практических рекомендаций и принципов, излагаемых в последующих разделах, может служить хорошим руководством по тестированию программ; 5.3. ОБЩИЕ РЕКОМЕНДАЦИИ Наиболее важный принцип, относящийся к тестированию про- грамм, состоит в том, чтобы думать об этой стадии еще на этапе написания программы. Следует постоянно задаваться вопросом: как будет тестироваться данный сегмент? Если ответ на вопрос о способе тестирования программы неясен, она должна быть либо переписана заново, либо разбита на модули. Нарушение этого принципа неизбежно приводит к тому, что программу' вообще не удается проверить до конца и рано или поздно при очередном ра- бочем прогоне она откажет. К сожалению, при написании программ о тестировании не за- думываются. Стараются сделать их эффективными, удобочитае- мыми, мобильными и т. п., но никак не полностью тестируемыми. А между тем средства тестирования должны заблаговременно встраиваться в программу. Проектировать программу следует та- ким образом, чтобы процесс разработки легко контролировался; при этом особое внимание, необходимо обращать на простоту и яс- ность программы, выбирая каждый раз такой способ ее кодиро- вания, чтобы всегда существовала возможность проверки соответ- ствия программы своему назначению. Сборка и перекомпоновка программной колоды должны про- изводиться непосредственно перед каждым тестовым прогоном.
246 ГЛАВА 5 Загрузочными модулями пользоваться не следует, поскольку ис- ходная программа находится в состоянии непрерывного измене- ния. В такой ситуации применение программы-загрузчика может легко вызвать путаницу, потому что трудно установить, какой за- грузочный модуль какой версии программы соответствует. Если для целей тестирования в исходную программу необходи- мо вставить дополнительные карты, они должны отличаться по цвету от основных. Кроме того, в каждой тестовой строке про- граммы целесообразно перфорировать в колонках 73—80 признак TEST. Тогда будут существовать одновременно два напоминания о том, что тестовые строки подлежат исключению из готовой про- граммы. На самых ранних этапах разработки программы необходимо сразу установить контроль за ее качеством. Для этого программа должна проверяться опытными программистами, в обязанности которых входит выявление пропущенных блоков, нерационально запрограммированных частей и отклонений от технических требо- ваний к программе. Раннее обнаружение потенциальных ошибок приносит несомненную выгоду автору программы, помогая ему избежать серьезных затруднений на стадии тестирования. Язык программирования должен выбираться соответственно ре- шаемой задаче. Учет этого фактора, как и надлежащий выбор ал- горитма, облегчает процесс тестирования программы. 5.4. НЕОБХОДИМАЯ ПОЛНОТА ТЕСТИРОВАНИЯ Начинающие программисты часто не понимают, каким обра- зом можно определить, что программа испытана достаточно пол- но. Это действительно трудная проблема, и даже многие умуд- ренные опытом программисты нередко обнаруживают, что слиш- ком рано прекратили тестовую проверку той или иной программы. Один полезный практический принцип сводится к тому, чтобы продолжать испытания до тех пор, пока каждая команда не бу- дет использована хотя бы один раз. Тестовые данные должны обеспечивать проверку всех возможных условий возникновения ошибок. Должна быть испытана каждая ветвь алгоритма. Это значит, что если в логическом блоке имеются два или три исхо- да, то каждый из возможных путей дальнейшей работы програм- мы необходимо проверить хотя бы по одному разу. Тестовая ин- формация должна включать в себя все типы данных; испытания подобного рода являются необходимыми, но не достаточными. Та- кие проверки, при которых тесты прогоняются по всем ветвям логической схемы программы, называются испытаниями ветвей. Те сегменты программ, которые интенсивно взаимодействуют друг с другом, должны проверяться с помощью такого набора данных, который позволил бы тщательно испытать эти взаимо- действия. В случае если на зависимые переменные влияет боль-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 247 шое количество факторов, полное тестирование становится труд- новыполнимой задачей. Необходимо заботиться о таком подборе контрольных примеров, чтобы не могли оставаться незамеченны- ми ошибки, которые непосредственно не влияют на результат, выдаваемый в каждом цикле проверки. Следует избегать искушения прекращать проверку программы, как только она покажется правильной. Программа, тестированная лишь наполовину, может принести программисту массу хлопот впоследствии, когда после тех или иных не внушающих доверия результатов выяснится, что она все еще нуждается в проверке. Поскольку в большой программе редко удается проверить все возможные комбинации факторов, влияющих на ее работу, зако- номерен вопрос: когда можно прекращать испытания? Ответ на него зависит от того, насколько важна точность работы програм- мы, как часто она будет использоваться и как долго предпола- гается ее эксплуатировать. Для программы, которую намерева- ются использовать всего один-два раза, не требуется такого объе- ма проверок, как для программы, которая должна работать еже- дневно в течение многих месяцев. Однако превалирующим факто- ром является требование к точности. Во многих случаях бывает допустим риск некоторой неточности, если стоимость дополнитель- ного тестирования чрезвычайно высока. 5.4.1. ЧИСЛО ТЕСТОВЫХ ПРОГОНОВ Само по себе число тестовых прогонов программы зачастую не является определяющим фактором. Дело в том, что программам не присуще явление старения, и поэтому, если некоторая програм- ма выполняет сложение нескольких чисел правильно, то она бу- дет выполнять его правильно и для других чисел, сколько раз ни производилась бы эта операция. Точно так же само по себе ко- личество проверок не может гарантировать полноты проведенных испытаний. Если, например, вы обрабатываете 100 тестовых запи- сей, содержащих некоторые реальные входные данные, то 90 из них будут вести себя одинаково, т. е. при этом проверяется лишь основная логическая ветвь программы. Остальные 10 записей, ве- роятно, позволят дополнительно испытать еще какие-нибудь две ветви, и, таким образом, 100 обработанных записей в данном слу- чае представляют собой лишь три контрольных примера. Еще одна причина нецелесообразности использования излиш- не больших объемов тестовых данных заключается в том, что ни- кому не хочется тратить время на анализ результатов 1000 ис- пытаний только для того, чтобы убедиться в их правильности. Поэтому никакой выгоды из большого числа испытаний не извле- кается. Очень часто прогон большого количества тестов просто позволя- ет убедиться, что программа хорошо выполняет одну и ту же сово-
248 ГЛАВА 6 купность операций над различными числами. Однако цель тести- рования состоит совсем не в этом, а в том, чтобы гарантировать живучесть программы. Мы всегда хотим, чтобы очередной тесто- вый прогон контролировал нечто такое, что не было проверено в предыдущих прогонах. И задача тестирования заключается в том, чтобы создать для программы предельно напряженный режим работы. А это требует от программиста хорошего воображения и недоверчивого отношения к своей программе. Обходитесь минимальным количеством контрольных примеров. 5.5. НЕВОЗМОЖНОСТЬ ИСЧЕРПЫВАЮЩЕГО ТЕСТИРОВАНИЯ Исчерпывающее тестирование программы неоправданно с эко- номической точки зрения и обычно неосуществимо на практике. Ни в какой нормальной программе нельзя проверить все возмож- ные ситуации. Предположим, мы имеем небольшую программу, схематически изображенную на рис. 5.1 и характеризующуюся двумя входными переменными и одной выходной. . Если бы мы могли проверить эту программу при всех возможных значениях X и У, то можно было бы с абсолютной точностью гарантировать ее правильность. Однако Рис. 5.1. даже для такой простой программы по- добная проверка неосуществима. Ведь> если, например, программа имеет дело с 32-разрядными двоичными числами, то существует 232 возможных значений для X и У и, следовательно, 232 х 232 _ 264 их различных сочетаний. При таком. количестве ситуаций програм- му, работающую одну миллисекунду, пришлось бы тестировать в течение 50 млрд. лет1*. Мы убедились, таким образом, что проверка всех возможных комбинаций входов нереализуема, но можно ли проверить все ло- гические ветви? Если обратиться, например, к блок-схеме, пока- занной на рис. 5.2, то здесь прослеживаются три самостоятельных логических пути. Совокупность двух таких сегментов (рис. 5.3) образует девять путей (отыщите их), а в случае, когда програм- ма, соответствующая блок-схеме рис. 5.3, работает дважды, их будет уже 9X9- Если же предположить, что эта программа явля- ется частью цикла, выполняемого 10 раз, то количество возмож- ных логических путей станет равным 910. Таким образом, даже в ’> Подсчет автора неточен: при круглосуточной работе в автоматическом ре- жиме понадобится всего лишь 300 млн. лет. — Прим, перво.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 249 таком простом примере исчерпывающее тестирование неосущест- вимо. Наконец, следует иметь в виду, что если бы и удалось про- вести полное тестирование программы, то необходимое для этого машинное время оказалось бы потраченным зря, по- скольку неизвестно, какими способами можно было бы вручную проверить достоверность всех выходных / \ результатов. / \ Мало того, что даже в простом случае практиче- \ " Л ски невозможно реализовать исчерпывающее испыта- \ / ние программы, но еще и в процессе ее эксплуатации \ / имеет место лишь ограниченное число комбинаций входных данных. Поэтому необходимо тщательно рИс. 5.2. подбирать контрольные примеры небольшого объема так, чтобы их значимость не вызывала сомнений. Вся- 7 k кий случайный выбор приводит к тому, что проверя- / \ ются малозначительные ситуации. / \ Учитывая, что исчерпывающее тестирование невоз- \ / можно, испытывайте программу разумно. V 5.6. ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ К ТЕСТИРОВАНИЮ / \ Тестовые испытания программы представляют со- \ / бой процесс, направленный на то, чтобы гарантиро- \ / вать ее нормальную работу во всех предполагаемых \/ практических ситуациях. При этом должны осущест- вляться два вида испытаний: на соответствие создан- Рис. 5.3. ной программы поставленной задаче и на правиль- ность ее функционирования. Первый вид испытаний невозможно провести, если задача оп- ределена не полностью, нечетко и непоследовательно. Все резуль- таты, соответствующие той или иной совокупности входов, должны быть предусмотрены. Требование четкости означает, что как по- становщик задачи, так и программист должны ясно представлять себе, чего они хотят добиться от программы. Требование 'последо- вательности предполагает недопустимость какой-либо неопределен- ности. Когда программная спецификация обладает свойствами полноты, четкости и последовательности, то человек, проводящий испытания программы, может рассматривать ее как «черный ящик» (не заботясь о ее внутренней структуре). Один из возможных способов контроля за соответствием спе- цификации программе состоит в том, чтобы поручать постанов- щику задачи формировать тестовые данные и указывать соответ- ствующие им правильные результаты. Эта мера позволяет, во- первых, гарантировать, что постановщики задач хорошо знают, чего хотят, и, во-вторых, проверять, правильно ли программист понимает задачу.
250 ГЛАВА б Ряд проверок можно провести еще до начала какой-либо ра- боты по кодированию программы. Например, некоторые ошибки алгоритма могут быть выявлены и легко устранены в результате «ручного моделирования» логики работы программы. В подходе к тестированию программ возможны две крайно- сти. Первая состоит в том, чтобы глубоко изучить программу и использовать ее в качестве основы для проведения испытаний. Однако в данном случае, если текст программы не соответствует ее спецификации, этот факт никогда не обнаружится, и тот, кто испытывает программу, будет вынужден для организации тести- рования обращаться к ее функциональному описанию. Вторая крайность имеет место тогда, когда для тестирования используется только программная спецификация, а сама про- грамма рассматривается как «черный ящик». Предположим, пе- ред вами стоит задача разработать тестовые данные для програм- мы, которая считывает некоторое целое число, обозначающее ра- диус круга, и затем вычисляет площадь последнего. Придумайте для этой программы контрольные примеры, считая программу «черным ящиком». А теперь допустим, выдвигается требование, чтобы программа выдавала результаты в следующем виде: если радиус равен еди- нице, то площадь равна ...; если радиус равен двум, то площадь равна ...; и т. д. Здесь уже контрольные примеры, разработанные вами для предыдущего случая, не смогут обеспечить адекватной проверки программы. Дело в том, что для организации такой про- верки необходимо совместное использование и программной спе- цификации и теста программы. 5.7. НЕОБХОДИМОСТЬ'РАННЕГО ТЕСТИРОВАНИЯ Тестирование программы на начальной стадии ее разработки является жизненной необходимостью, поскольку стоимость про- ведения проверок и устранения ошибок на каждом последующем этапе разработки программы увеличиваются на порядок. Затра- ты же на исправление той или иной ошибки на этапе проектирова- ния ничтожны. Не так уж трудно определить местоположение ошибки, обнаруженной при индивидуальном тестировании отдель- ного модуля, но для этого требуются уже значительно большие затраты, чем в предыдущем случае. Если же ошибка обнаружена, на этапе испытания всей программы как единого целого, ее устра- нение часто связано с привлечением к- этой работе целого ряда лиц и с организацией взаимосвязей между коллективами. Бес- спорно, в этом случае затраты увеличиваются на порядок по срав- нению с этапом тестирования отдельного модуля. Ошибка, обнаруженная в программе, уже находящейся в экс- плуатации, приводит к затратам, возрастающим еще на порядок или более относительно предшествующего случая. Это происходит
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 251 потому, что процесс исправления такой ошибки затрагивает инте- ресы пользователей, требует временной фиксации некоторого про- межуточного состояния системы и может быть связан с повтор- ными выполнениями заданий. Кроме того, требуется непосредст- венное общение программистов и находящихся от них, йозможно, на значительном расстоянии пользователей. Начинайте тестирование как можно раньше. Тестовые проверки следует осуществлять еще на этапе разра- ботки алгоритма программы, но на каком бы уровне они ни про- водились, следует иметь в виду, что результатом тестирования может стать частичное или даже полное перепроектирование си- стемы. Присутствие в составе бригады разработчиков системы лиц, занимающихся испытаниями программного обеспечения (назовем их испытателями), необходимо по двум главным причинам. Во- первых, испытатели способствуют более четкому формулированию требований к проектируемой системе, поскольку смотрят на эти требования с несколько иной точки зрения, чем разработчики. В результате испытатели часто обнаруживают ошибки в алгорит- ме и неполноту спецификаций. Тем самым они помогают улуч- шить программные средства и фактически ускоряют разработку проекта. Во-вторых, сами масштабы работы, которую предстоит совер- шить испытателям, делают целесообразным их подключение к разработке уже на этапе проектирования. Испытатели должны подготовить контрольные примеры с известными результатами; сформированные контрольные примеры должны быть затем за- писаны на каком-либо носителе информации для обеспечения воз- можности их обработки. Вся эта подготовительная работа может и должна выполняться в процессе разработки программы. И если тестирование осуществляется способом сверху вниз, тестовые дан- ные потребуются очень скоро. 5.8. ПРОВЕРКА ПРАВИЛЬНОСТИ ПРОЕКТНЫХ РЕШЕНИЙ В случае когда мы не знаем, для чего предназначена програм- ма и каким требованиям она должна удовлетворять, никакое, да- же самое большое количество тестовых проверок не может гаран- тировать ее правильной работы. Наоборот, хорошо продуманные требования могут сэкономить массу времени на стадии испыта- ний. Эта заключительная стадия разработки системы никак не предназначена для штопания дыр в проекте, поскольку к ее на- чалу большинство концепций бывает уже воплощено в програм- мы, и, если первоначальный замысел неверен, все эти программы могут оказаться бракованными. Во избежание такого положения
252 ГЛАВА Б дел правильность постановки задачи должна быть тщательно про- верена еще до начала программирования. Для выполнения указанной проверки необходимо убедйться, что группа тестирования, взяв описание задачи, может самостоя- тельно разработать на его основе адекватное множество контроль- ных примеров, рассматривая программу как «черный ящик». Обычно с первого раза этого сделать не удается, спецификация задачи должна, быть переделана, так как если она непонятна группе тестирования, то вряд ли в ней сумеет разобраться кто- либо еще. Иногда случается, что разработчики, пытаясь улучшить описание задачи, обнаруживают, что они не в состоянии это осу- ществить. Такая ситуация говорит о недостаточно глубоком пони- мании проблемы в целом и одновременно служит предупрежде- нием о предстоящих неприятностях, которые ожидают разработ- чиков в случае, если постановка задачи не будет уточнена. Целый ряд исследований показал, что источником примерно половины всех ошибок в программном обеспечении больших си- стем являются проектные недоработки. Следовательно, именно на стадии проектирования следует ликвидировать пагубные послед- ствия программных ошибок, которые могут проникать в проект либо по вине разработчиков, либо из-за расплывчатой формули- ровки целей заказчиками программного обеспечения. Однако, ка- ковы бы ни были причины, важно, чтобы они устранялись в самом, начале их возникновения. 5.8.1. сквозной -СТРУКТУРНЫЙ КОНТРОЛЬ Первым серьезным испытанием проектных решений должна быть их сквозная проверка некоторой группой специалистов. Часто на этом этапе могут быть вскрыты весьма существенные ошибки. Однако цель здесь состоит не в том, чтобы обязательно- доказать необходимость переделки программы, а в том, чтобы вы- явить вероятные ошибки. Если ошибки обнаружены, тогда разра- ботчики должны пересмотреть программу, и весь цикл может быть- повторен сначала. Не следует бояться многократного повторения этой последовательности действий, так как нужно помнить, что исправление ошибок на стадии проектирования обходится недоро- го. Гораздо дороже будет стоить ошибка, оставшаяся необнару- женной вплоть до заключительной проверки готовой программы. Поэтому важно выработать правильное отношение к процедуре- выявления ошибок в проектных решениях и не заниматься кри- тикой разработчиков, а помогать им получить «чистый» проект. Очевидно также, что при негативном отношении к проекту со сто- роны группы контроля гораздо труднее бывает обеспечить эф- фективное сотрудничество всех участников разработки. Именно по этой причине руководителей проекта обычно исключают из уча- стия в процессе структурного контроля. Поддержание деловой ат-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 253- мосферы, когда целью, проверки не является отыскание виновных в- допущенных ошибках, должно способствовать более тщательному поиску ошибок каждым специалистом, включая и самих разра- ботчиков. Прежде всего проводите ручную проверку. Проверка проектных решений должна охватывать лишь верх- ние уровни разработки, а именно контролировать правильность- системной логики и сопряжений модулей. Когда проект утверж- ден, он не должен содержать ошибок, исправление которых тре- бует корректировки сразу нескольких модулей программы. Такие- ошибки должны выявляться на этапе проектирования. Еще один широко распространенный способ проверки правиль- ности проектных решений — это предварительное написание про- грамм системы на языках APL или БЕЙСИК- Хотя такой подход связан с довольно большим объемом дополнительной работы, это окупается в случае необходимости внесения изменений в проект.. Старайтесь проверять правильность принципов построения системы на ее простом варианте. Структурный контроль должен тщательно планироваться самим разработчиком с учетом того, что необходимо иметь от четырех, до шести рецензентов, в качестве которых обычно выступают ис- пытатели, проектировщики и документалисты. Предполагаемые рецензенты должны получать материалы за 4—6 дней до прове- дения совещания по их рассмотрению, чтобы иметь возможность- ознакомиться с ними и быть готовыми к обсуждению любых про- блем. При проведении совещания всегда следует руководство- ваться определенной целью и ограничиваться по времени двумя, часами. Если этого времени недостаточно, то лучше запланиро- вать дополнительную встречу. Председательствующий должен обеспечить рабочую обстановку и составление полного списка ошибок, неувязок и противоречий, которые отнюдь не должны быть устранены сразу же, в ходе совещания. Разработчик обязан за- няться этим позже и сообщить о принятых мерах. В начале совещания по проверке структуры рецензенты харак- теризуют степень завершенности проекта в целом, точность вы- полнения выдвинутых требований и качество проектных решений.. Затем разработчик делает краткий обзор всех элементов програм- много обеспечения. При этом возможна демонстрация работы, программ на контрольных примерах, результаты которых подвер- гаются групповому анализу. По окончании совещания председа- тельствующий вручает каждому члену группы список выявленных проблем, требующих решения. Разработчик обязан затем разре- шить отмеченные проблемы и довести до сведения рецензентов^, какие были предприняты действия по их замечаниям.
254 ГЛАВА 6 Необходимо, однако, установить допустимое количество обна- руживаемых ошибок. Это означает, что, если выявлено 5—10 оши- бок, сквозной контроль должен быть приостановлен для проведе- ния тщательного анализа проекта и устранения основных ошибок. Только после этого имеет смысл продолжить сквозной контроль. Нецелесообразно тратить время на выявление двух-трех десятков •ошибок, поскольку такое их обилие будет говорить лишь о том, что весь проект нуждается в переделке и его частичная корректи- ровка не спасет положения. Стратегия проведения сквозного струк- турного контроля хорошо описана в работе [9]. 5.9. МЕТОДЫ ТЕСТИРОВАНИЯ Как правило, тестирование программы выполняется поэтапно, начиная с проверки каждого модуля (которую иногда относят к отладке) и кончая заключительной проверкой всей системы. Если при этом не придерживаться какой-либо четкой последовательно- сти действий, то вряд ли можно надеяться на создание надежно- го программного обеспечения. Стратегия тестирования обычно со- здается на основе одного из двух методов: либо традиционного ме- тода тестирования снизу вверх, либо более современного подхо- да— тестирования сверху вниз. 5.9.1. ТЕСТИРОВАНИЕ СНИЗУ ВВЕРХ Этот широко распространенный метод предполагает первона- чальное написание и проверку модулей самого нижнего уровня. Затем программируются и тестируются элементы более высокого уровня и так далее до тех пор, пока не будет завершено напи- сание всей программы. Тестирование по методу снизу вверх в настоящее время не на- ходит поддержки со стороны приверженцев проектирования и про- граммирования по методу сверху вниз. При этом основная крити- ка связана с тем обстоятельством, что метод снизу вверх не дает возможности, выявлять серьезные ошибки в алгоритме и интер- •фейсах почти до момента окончания разработки проекта. А это приводит к тому, что программу может лихорадить .от многочис- ленных переделок. Второй недостаток указанного метода тестирования заключа- ется в том, что при каждом новом тестировании элементов раз- личного уровня требуются новые тестовые средства, драйверы и тестовые данные. Этот процесс сам по себе требует большого объема работы по программированию. 5.9.2. ТЕСТИРОВАНИЕ СВЕРХУ ВНИЗ Этот метод тестирования является дополнительным этапом процесса проектирования сверху вниз, сквозного контроля и ко- дирования сверху вниз. При таком методе разработки програм-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 255 много обеспечения вначале пишется основная программа, а не- запрограммированные модули более низкого уровня заменяются имитирующими их подыгрывающими программами. Такая «скелет- ная» программа тем не менее может быть испытана в отсутствие вызываемых подпрограмм и даже без каких-либо данных. Эта- проверка должна выявить неточности языка управления задания- ми, который часто бывает далеко не тривиальным. Следующий шаг должен состоять в добавлении модуля, генерирующего вход- ные данные, — это может быть либо модуль ввода, либо некото- рый вспомогательный модуль, заменяющий его до момента окон- чания программирования последнего. Вслед за этим испытания проводятся с использованием каких-либо простых входных дан- ных. 5.9.3. ПРЕИМУЩЕСТВА ТЕСТИРОВАНИЯ СВЕРХУ ВНИЗ По мере того как «скелет» программы «обрастает» новыми мо- дулями, должны добавляться и новые тестовые данные, объем ко- торых увеличивается постепенно, одновременно с разрастанием программы. В результате появляется возможность накапливать тестовые данные вместо раздельного их формирования для каждо- го модуля. Еще одним плюсом тестирования по методу сверху вниз явля- ется то, что стержневая логика программы тестируется на ран- нем этапе, и эта проверка повторяется многократно с добавлени- ем новых модулей. При тестировании же снизу вверх стержневая логика программы испытывается в последнюю очередь; в этом случае при обнаружении в ней ошибки могут быть неверными прошедшие предыдущие проверки элементы более низких уров- ней, и огромная работа может оказаться выполненной напрасно. Обычным делом в разработке систем является такая ситуа- ция, когда две группы программистов разрабатывают две раз- личные подсистемы, которые должны взаимодействовать друг с другом или сходиться в верхнем узле. При использовании метода снизу вверх место связи подсистем испытывается в последнюю очередь. Метод сверху вниз позволяет проверить взаимодействие подсистем еще до того, как будут готовы модули нижних уровней. Немаловажным преимуществом метода сверху вниз является распределенное тестирование, проводимое фактически на протя- жении всей разработки проекта, когда модули тестируются по ме- ре их добавления. Наоборот, при использовании метода снизу вверх вся работа по тестированию скапливается к моменту окон- чания проектирования. Очень часто в этот момент бывает на- столько сильным внешнее давление на разработчиков с требова- нием быстрейшего завершения проекта, что тестирование делается кое-как, а это, как правило, приводит к катастрофическим послед- ствиям, когда неиспытанная система выходит из строя.
S56 ГЛАВА 5 Старайтесь применять тестирование по методу сверху вниз. Кроме всего прочего, тестирование сверху вниз дает возмож- ность получать результаты раньше, чем при использовании друго- го метода, причем программа может их выдавать, даже не будучи завершенной. С этими результатами можно ознакомить пользова- теля, который убедится не только в достижении прогресса в ра- боте, но и в том, правильно ли поняты программистами выдвину- тые требования. Любые обнаруженные при этом неточности или упущения могут быть устранены до того, как начнется интенсив- ное программирование, а не после его завершения. 5.9.4. КАКОЙ МЕТОД ЛУЧШЕ? . Ответ на этот вопрос зависит от конкретного проекта. Для не- больших программ между двумя рассмотренными методами прак- тически нет различий. В ряде случаев подыгрывающие модули, необходимые при тестировании сверху вниз, могут оказаться столь же сложными, сколь и настоящие модули, что исключает возмож- ность извлечения каких-либо выгод. При использовании метода -сверху вниз возможна и такая ситуация, когда проектируемый модуль нижнего уровня невозможно реализовать; если этот факт не обнаружить своевременно, могут возникнуть серьезные затруд- нения. На практике часто приходится останавливаться на комби- нированной стратегии, при которой часть программирования и те- стирования осуществляется по методу сверху вниз, а часть — по методу снизу вверх. 5.10. ТЕСТОВЫЕ ДАННЫЕ Правильный отбор данных для тестирования той или иной под- программы может значительно облегчить задачу обнаружения в ней ошибок. Прежде всего первый тест должен быть максимально прост, так как изначальная цель тестирования состоит в том, что- бы проверить, работает ли программа вообще. Такую проверку часто называют дымовым тестом, поскольку она напоминает вду- вание дыма внутрь и наблюдение за тем, не выходит ли он где- нибудь наружу. Несколько первых тестов предназначено для про- верки общей организации программы, поскольку, если она неудач- на, затруднительно, да и нецелесообразно пробираться сквозь ла- биринты программы. Тест, который используется для проверки основной ветви про- граммы, должен обнаруживать грубые ошибки. При наличии таких ошибок работа программы с огромной скоростью, равной несколь- ким миллионам операций в минуту, может пойти по любому на- правлению. Поэтому, если применить при первом испытании •сложный тест, а программа не будет нормально работать, мы не
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 257 сможем определить, что тому виной: именно этот специальный пример или какая-то ошибка, проявляющаяся при любых других комбинациях данных. Сложный контрольный пример выявляет либо элементарные ошибки, которые могли бы быть обнаружены и более простыми средствами, либо неисправность оказывается такой же сложной, как и сам тест, выходные данные которого не позволяют установить причину отказа. Упрощайте арифметические операции в тестах. Если, напри- мер, программа способна правильно складывать числа 11,11 -Ь 22,224-33,33, она, несомненно, столь же успешно будет выполнять и сложение 12,56+45,924-34,79. Простые тестовые данные облегчают ручной контроль резуль- татов, а сами контрольные примеры предназначаются для провер- ки правильности работы программы, а совсем не для того, чтобы испытывать способность машины к выполнению арифметических действий. Хотя для тестирования и рекомендуется использовать простые данные, но желательно также, чтобы каждый тестовый прогон давал максимально возможное количество информации, а общее число прогонов было незначительным. Однако при этом резуль- таты испытаний отдельных элементов программы должны быть хорошо различимы, чтобы в случае ошибок не возникало путани- цы. Хорошими следует считать такие тесты, которые помогают установить первопричину ошибок. В некоторых случаях тестирования требуется производить «ми- ниатюризацию» программы, т. е. разумно сокращать объем дан- ных по сравнению с реальным. Можно, например, представить се- бе программу, которая работает обычно с матрицей размерно- стью 50X50, однако проверка соответствующих вычислений вруч- ную при такой размерности неосуществима. Поэтому в качестве тестовых данных может быть использована матрица 5X5. Одна- ко если такая миниатюризация программы требует внесения в нее каких-либо изменений, то могут возникнуть две нежелательные ситуации: либо существующие в программе ошибки в результате упрощающих изменений могут стать неявными или временно ис- чезнуть, либо могут появиться новые ошибки. Точно так же, если некоторая подпрограмма работает в цикле 5 раз, она, безусловно, сможет работать и 105 раз (лишь бы хва- тало зарезервированного объема памяти). Часто бывает интерес- но посмотреть, какие результаты выдает программа при настрой- ке ее на нуль, один или отрицательное число циклов; однако в последнем случае требуется особая осторожность. Обычно, если некоторый цикл предполагает выполнение N итераций, то наибо- 17—899
258 ГЛАВА 5 лее распространенной ошибкой является организация N—1 или АН-1 итераций. Усложнение тестовых данных должно происходить постепенно. С каждым новым тестовым прогоном прибавляется еще один эле- мент программы, функционирующий правильно. Такая равномер- ность усложнений облегчает задачу распознавания ошибки при выдаче неверных результатов. Если же с помощью одного теста контролируются, сразу несколько непроверенных модулей про- граммы, то в случае ошибки бывает трудно определить, какой из модулей привел к отказу. Заключительные испытания должны обеспечить проверку всей программы в целом. Они предназначены для того, чтобы не про- пустить подпрограмм с ошибками, которые при конкретных те- стовых данных могли и не нарушить правильности результатов. Цель тестирования любой программы состоит не в том, чтобы проверить, работает ли она при различных комбинациях тестовых данных, а в том, чтобы убедиться в ее правильной работе. По- этому тестовые данные должны подбираться таким образом, что- бы программист был в состоянии вычислить правильный результат еще до начала тестового прогона. Если этого не сделать заблаго- временно, то потом очень легко поддаться соблазну считать ма- шинный результат достоверным. 5.10.1. ТИПЫ ТЕСТОВЫХ ДАННЫХ Можно выделить три типа тестовых данных: создаваемые про- граммистом, реальные модифицированные и реальные в полном объеме. Для тестирования обычно требуются данные всех трех типов. В первую очередь используют данные, создаваемые програм- мистом. Они подразделяются в свою очередь на контролируемые и случайные данные. Данные первого типа применяют для провер- ки общей работоспособности программы; они отличаются тем, что могут использоваться в редко встречающихся ситуациях. Благо- даря такой возможности контролируемые данные позволяют осу- ществлять максимально эффективные проверки. Кроме того, пра- вильный подбор тестовых данных должен способствовать умень- шению объема ручной работы по проверке машинных результатов. Недостатком контролируемых тестовых данных является то, что они предусматривают проверку лишь тех ситуаций, которые при- знаются существенными с точки зрения пользователя. Поэтому не следует забывать об использовании данных друго- го типа (случайных тестовых данных), для формирования кото- рых существуют специальные программы. Эти тестовые данные обладают тем преимуществом, что позволяют последовательно ис- ключать ошибки из программы, а также обнаруживать ошибки, не выявляемые с помощью контролируемых тестовых данных. От- рицательной стороной использования случайных тестовых данных
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 259 является трудность проверки правильности результатов. Хотя сле- дует заметить, что если ошибки настолько серьезны, что приводят к авостам, то они, как правило, обнаруживаются. Реальные модифицированные данные сочетают в себе до не- которой степени достоинства обоих отмеченных выше типов те- стовых данных, создаваемых программистом. Путем аккуратного целенаправленного видоизменения реальных данных возможно осуществить целый ряд специальных режимов тестирования про- граммы. Использование реальных данных вместо данных, созда- ваемых программистом, позволяет избежать специального форми- рования иногда весьма значительных по объему массивов инфор- мации. Еще одно преимущество использования реальных модифици- рованных данных состоит в том, что в этом случае процесс те- стирования приближается к практическим условиям работы. Это позволяет выявить множество проблем, которые не могут быть вскрыты с помощью данных, полученных искусственным путем. Одной из целей модификации реальных данных является провер- ка подпрограмм обработки ошибок. Умышленное введение ошибок в тестовые данные — это единственный способ проверить правиль- ность функционирования таких подпрограмм. Заключительные испытания проводятся на реальных данных в полном объеме. Если этот этап тестирования программного обес- печения может быть осуществлен параллельно с функционирова- нием старой, не автоматизированной системы, то работа по руч- ной проверке машинных результатов существенно сокращается. Обычно при тестировании реальных данных программиста ожида- ет множество сюрпризов. Именно здесь выявляется недопонима- ние («коммуникационный разрыв») между программистом и за- казчиком и часто становятся явными их обоюдные просчеты. Иногда параллельное функционирование новой и старой си- стем вскрывает те или иные дефекты старой системы. Но контроль результатов без такой параллельной работы становится чрезвы- чайно трудоемким, а то и вообще невозможным. Для того- чтобы испытания можно было бы считать завершенными, необходимо выполнить по крайней мере три параллельных тестовых прогона. Параллельная работа старой и новой систем приводит к та- кому объему выходной информации, что визуальное сравнение результатов оказывается невозможным. В этих случаях пишут специальные программы, которые сравнивают записанные на ленту или диск результаты с требуемыми и устанавливают име- ющиеся расхождения. Кроме того, эти программы позволяют со- поставлять старые и новые массивы данных результата в тех слу- чаях, когда в программное обеспечение вносятся изменения. Каждый из рассмотренных типов тестовых данных имеет свои достоинства и недостатки, но при совместном использовании они обеспечивают наиболее полное и качественное тестирование. 17*
260 ГЛАВА 5 В ходе ручной проверки машинных результатов следует акку- ратно проводить вычисления, чтобы в них не вкрались ошибки. В противном случае выходные данные, полученные с машины, бу- дут сравниваться с неверными результатами ручного счета, что,' может привести к большим затратам времени на поиск програм- мной ошибки, в то время как корнем зла являются неверные вы- числения. 5.10.2. КЛАССЫ ТЕСТОВЫХ ДАННЫХ В связи с тем что мы не располагаем средствами, Чтобы опро- бовать программу при всех возможных совокупностях данных, желательно подобрать наиболее характерные данные. Для этого каждый новый тест должен содержать вполне определенный класс данных. Этот аспект тестирования часто оказывается вне сферы внимания разработчиков. Между тем правильная работа програм- мы с данными конкретного класса позволяет надеяться на то, что она будет так же хорошо обрабатывать и любые другие данны^ того, же класса. Если, например, мы имеем дело с программой, которая должна считывать количество отработанных за неделю часов и начислять заработную плату, можно было бы установить несколько классов данных, характеризующих отработанное время: 0, 35, 40, 50, 100. Поскольку программа должна вычислять сверхурочную оплату, проверка ее работоспособности должна предусматривать задание времени, меньшего, большего и равного 40 ч. Кроме того, необ- ходимо удостовериться, что программа правильно работает, когда время равно нулю или некоторому большому числу. Следует так- же посмотреть, как ведет себя программа при некоторых непра- вильных значениях отработанного времени, например при отри- цательных величинах. Для правильного определения необходимых классов тестовых данных обычно требуется некоторое знание программы. В пре- дыдущем примере охвачены все возможные классы значений пе- ременной «время». Введение каких-либо дополнительных значе- ний, превышающих число 40, ничего не добавило бы к тому ре- зультату проверки, который получается при использовании чис- ла 50, позволяющему судить о том, правильно ли работает про- грамма при начислении сверхурочной оплаты. Однако, если бы какие-то из отмеченных классов были упущены, мы не могли бы иметь полной уверенности в работоспособности программы. Основ- ная задача состоит в том, чтобы определить необходимые классы данных и сформировать 1—2 контрольных примера для каждого из них. Программисты-непрофессионалы склонны к избыточной проверке программ на одних классах данных и недостаточной — на других.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 261 В каждом следующем тесте должен использоваться класс данных, отличный от предыдущего. Использование тестовых данных определенных классов отве- чает требованию экономичности тестирования. 5.10.3. АНАЛИЗ РЕЗУЛЬТАТОВ ТЕСТИРОВАНИЯ Каждый раз, когда используются тестовые данные, необхо- димо иметь какой-то способ проверки правильности машинных результатов. Существует несколько таких способов: 1) вычисление результатов вручную; 2) получение результатов из регистрационной книги, докумен- тации или совокупности таблиц и 3) получение результата с помощью некоторой другой машин- ной программы. Важно, чтобы проверяемые машинные результаты сопоставля- лись с соответствующими данными, взятыми из другого, незави- симого источника. Очевидно, что наименее желательно применение первого метода вследствие большого объема ручной работы. Все три отмеченных метода проверки основываются на знании конкретных значений результатов, которые должны быть получе- ны. Однако это не всегда возможно или целесообразно. Для та- ких случаев можно прибегнуть к двум дополнительным методам, применение которых, однако, требует от испытателя знания алго- ритма, реализованного в программе. Для использования первого из этих дополнительных методов необходимо знание пределов изменения выходных переменных. Обычно такие пределы можно довольно легко определить, если проанализировать программу, зная ее алгоритм. Любые значения, выходящие за ожидаемые пределы, должны подвергаться более тщательной проверке. Второй метод состоит в целенаправленном манипулировании тестовыми данными и предсказании на основе продуманного и осторожного их изменения направления и пределов изменения выходных переменных. Совпадение фактических и прогнозируемых изменений результатов может в какой-то мере говорить о пра- вильности работы программы. 5.104. ФОРМИРОВАНИЕ ТЕСТОВЫХ ДАННЫХ Усовершенствование процесса тестирования связано с приме- нением способов генерирования тестовых данных. В случае ис- пользования перфокарт такие методы очень просты и основы- ваются на введении специальных стандартных тестовых перфо- карт: 1. Перфокарт, заполненных нулями в позициях 1-9, единицами в позициях 10-19, двойками в позициях 20-29, и т. д.
262 ГЛАВА б 2. Перфокарт, содержащих 1 в позиции 1, 2 в позиции 2, 9 в позиции 9, О в позиции 10, 1 в позиции 11, 9 в позиции 19, 0 в позиции 20, 1 в позиции 21, и т. д. 3. Перфокарт, содержащих во всех 80 позициях нули, Перфокарт, содержащих во всех 80 позициях единицы, Перфокарт, содержащих во всех 80 позициях девятки. 4. Перфокарт, в которых единицы чередуются с нулями. 5. Перфокарт, содержащих во всех 80 колонках буквенные сим- волы: А в позиции 1, Б в позиции 2, В в позиции 3, и т. д. Тестовые данные такого вида могут применяться для контроля правильности идентификации полей данных. Например, если для ввода информации используются колонки 27-32, то в качестве те- ста целесообразно применять карты видов 1 и 2. В этом случае результат тестирования должен при правильной работе програм- мы выглядеть следующим образом: 222333 789012 где каждый столбец читается как двузначный номер, идентифици- рующий позиции входных данных (т. е. 27, 28,...,32). Этот тест обеспечивает проверку одного из самых важных условий — пра- вильности считывания программой нужных позиций перфокарт. После этого могут быть использованы тесты видов 3-5 для про- верки правильности обработки нулевых и буквенных данных. Еще один метод проверки правильности идентификации полей дан- ных заключается в использовании определенной буквы или циф- ры для каждого поля. При таком способе легче различать грани- цы полей данных.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 263 Конечно, наивно полагать, что применение тестовых перфокарт разрешает все проблемы тестирования. Однако с их помощью можно выполнить много разнообразных простых проверок. Такие карты должны находиться в ящике-стенде для хранения перфо- карт и быть легкодоступными для программистов. Для языков КОБОЛ и ПЛ/1 характерна интенсивная работа с файлами данных, поэтому необходимо иметь тесты для проверки файлов. Для формирования таких тестовых файлов обычно мож- но применять программы-утилиты. Например, фирма IBM пред- лагает программу-утилит IEBDG (генератор данных), которая обеспечивает получение контрольных примеров, могущих исполь- зоваться для тестирования рабочих программ. В ряде вычисли- тельных комплексов имеются специальные генераторы тестовых данных, которые формируют испытательные файлы. Основной принцип работы таких генераторов заключается в использовании описаний файлов, содержащихся в испытываемых программах для построения тестовых файлов. Тестовые генераторы могут как создаваться собственными силами пользователей, так и постав- ляться фирмами, разрабатывающими программное обеспечение ЭВМ. 5.10.5 . ЭТАПЫ ТЕСТИРОВАНИЯ Процесс тестирования программы можно разделить на три этапа: 1. Проверка в нормальных условиях. 2. Проверка в экстремальных условиях. 3. Проверка в исключительных ситуациях. Эти этапы должны быть обеспечены всеми необходимыми средствами тестирования. Каждый из трех этапов проверки должен гарантировать полу- чение верных результатов при правильных входных данных и вы- дачу сообщений об ошибках при неправильных данных. Проверка в нормальных условиях Проверка в нормальных условиях предполагает тестирование на -основе данных, которые характерны для реальных условий функционирования программы. Случаи, когда программа должна работать со всеми возможными данными, чрезвычайно редки. Обычно имеют место конкретные ограничения на область измене- ния данных, в которой программа должна сохранять свою рабо- тоспособность. Поэтому проверка в нормальных условиях долж- на показать, что программа выдает правильные результаты для характерных совокупностей данных.
264 ГЛАВА 5 Проверка в экстремальных условиях Этот этап тестирования должен идти сразу за проверкой про- граммы в нормальных условиях. Тестовые данные, этого этапа включают граничные значения области изменения входных пере- менных, которые должны восприниматься программой как пра- вильные данные. Для нецифровых данных необходимо использо- вать подобные типичные символы, охватывающие все возможные ситуации. Для цифровых данных в качестве экстремальных усло- вий следует брать начальное и конечное значения допустимой об- ласти изменения переменной при одновременном изменении дли- ны соответствующего поля от минимальной до максимальной. Типичными примерами таких экстремальных значений являются очень большие числа, очень малые числа и отсутствие информа- ции. Каждая программа характеризуется своими собственными экстремальными данными, которые должны подбираться програм- мистом. Процесс использования экстремальных значений переменных в качестве тестовых данных носит название граничных испытаний. Граничные испытания зачастую предоставляют наилучшие воз- можности для выявления ошибок. Если некоторая программа ра- ботает правильно в граничных условиях, обычно это означает, что она будет нормально работать и в любой другой области значений переменных. Еще один тип экстремальных условий — это граничные объемы данных, когда они состоят из слишком малого или, наоборот, слишком большого числа записей. Необходимо установить, что происходит с программой, если ей на обработку не поступает ни одного элемента данных или только один, и сохранит ли она в этих условиях свою работоспособность. Особый интерес представляют так называемые нулевые при- меры. Для цифрового ввода — это обычно нулевые значения вво- димых данных; для последовательностей символов — это цепочка пробелов или нулей; для указателей — нулевое значение указате- ля. Нулевые примеры представляют собой один из лучших те- стов, поскольку они имитируют состояние данных, которое время от времени имеет место в реальных условиях эксплуатации про- граммы. Если подобное тестирование не выполняется, то впослед- ствии часто приходится сталкиваться с непонятным поведением программы. Не всегда, однако, легко определить, какие значения данных являются экстремальными. Рассмотрим, например, программу, которая должна считывать четыре одноразрядных положительных целых числа. Поверхностный подход к построению тестов приво- дит в данном случае к формированию следующих экстремальных условий:
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 265 А в с D Тест 1 0 0 0 0 Тест 2 9 9 9 9 На первый взгляд эти условия действительно представляются экстремальными, хотя на самом деле таковыми не являются. Это хороший тест, но он является экстремальным только в том случае, когда выполняется следующее действие: t ANSWER = A-J-B + C-f-D Однако если предстоит вычислить ANSWER = (А±-®)* * D то наиболее вероятное значение ответа получится при С=1: ' ANSWER = (-1+9 *9 = i89 в то время как при С=9 ANSWER=* 9=2* Следовательно, правильный подбор численных значений перемен- ных для создания экстремальных условий возможен только в слу- чае знания алгоритма решения задачи. Приведенный пример иллю- стрирует часто допускаемую ошибку тестирования, когда забыва- ют, что существуют экстремальные значения как входных, так и выходных переменных. Главная цель использования экстремальных тестов состоит в установлении того факта, что поля данных про- межуточных результатов имеют размеры, достаточные для прове- дения требуемых вычислений. Условия, необходимые для тестирования программы, можно создавать также путем введения в нее определенных констант. Этот способ хорош тогда, когда возникает необходимость тести- рования экстремальных условий, являющихся результатом дли- тельных вычислений, но трудно подобрать контрольные данные для проверки тех ситуаций, которые представляют интерес. На- пример, если вы желаете установить момент, когда результаты вычислений становятся отрицательными, а эта ситуация возникает относительно редко, то можно добавить в программу оператор, который в конце проверки присваивает интересующей вас пере- менной отрицательный знак. Однако впоследствии надо не забы- вать об исключении этой искусственной модификации из про-
266 ГЛАВА 5 граммы. Один простой способ запоминания этого требования за- ключается в том, чтобы иметь в программе комментарий, который указывал бы, что данный оператор является тестовым. Можно также отперфорировать в позициях 73—80 тестовой строки исход- ной программы слово ТЕСТ как напоминание о необходимости исключения этой строки по окончании тестирования. Проверка в исключительных ситуациях. Последний этап тестирования программы проводится с ис- пользованием данных, значения которых лежат за пределами до- пустимой области изменения. Известно, что все программы разра- батываются в расчете на обработку какого-то ограниченного набо- ра данных. Поэтому важно получить ответ на вопросы: что прои- зойдет, если программе, не рассчитанной на обработку отрица- ; тельных или нулевых значений переменных, в результате какой- либо ошибки придется иметь дело как раз с такими данными? • Как будет вести себя программа, работающая с массивами, если количество их элементов превысит величину, указанную в описа- нии? Что случится, если цепочки символов окажутся длиннее или короче, чем это предусмотрено? Что произойдет, если числа будут слишком большими или, наоборот, слишком малыми? ? Наихудшая ситуация складывается тогда, когда программа • воспринимает неверные данные как правильные и выдает невер- ный, но правдоподобный результат. В этом случае представляются неубедительными отговорки, что программа не была предназна- чена для обработки данных, приведенных к такому результату, или что неверные данные не должны вводиться в машину. Такие « ограничения не могут решить проблему, поскольку неверные дан- < ные могут образоваться в результате опечаток или неправильного понимания оператором инструкций по вводу данных. Поэтому про- грамма должна сама отвергать любые данные, которые она не в состоянии обрабатывать правильно. Если вы являетесь единственным пользователем программы, то v можете не вводить в нее операторов, отвергающих недопустимые f данные. Однако, если та же программа предназначена еще и для ; других пользователей, такие операторы крайне необходимы. Они j должны проходить обязательную проверку в процессе тестирова- ( ния, чтобы программист убедился в наличии всех необходимых f операторов редактирования данных. J В отношении проверки программы в исключительных и экст- < ремальных условиях можно дать ряд рекомендаций, которые мо- '• гут сэкономить время программиста. Прежде всего, если предус- матриваются проверки правильности редактирования данных, бе- ; рите значения, расположенные на обеих границах допустимой об- |- ласти; именно эти значения являются наиболее вероятными ис * точниками затруднений при работе программы.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 267 Данные, содержащие пробелы, цифры и буквы, должны опро- боваться в самых разнообразных сочетаниях. Пробуйте запол- нять пробелами и буквами те поля, которые требуют цифровых данных. При проверке взаимных связей между различными поля- ми данных попытайтесь перемежать верные и неверные данные. Иногда большие затруднения вызывают данные, содержащие сразу несколько ошибок. Еще один вид ошибок, представляющих особый'интерес, — это ошибка в первом или последнем элементе обрабатываемой информации. Важно, чтобы при наличии такой ошибки программа не заканчивала работу, как в нормальной си- туации. Для этого достаточно при тестировании сформировать файл, состоящий из единственного элемента, который неверен. Если такая работа не проделана на стадии тестирования програм- мы, трудности неизбежно возникнут при ее эксплуатации. По окончании отмеченных выше проверок наступает время удовлет- ворения любознательности и свободного полета фантазии про- граммиста. Попробуйте использовать в качестве входных данных программы необычную информацию: небольшую пачку карт из приемного кармана, неупорядоченную колоду перфокарт, ленту с данными, не относящимися к вашей программе. Помните, что первое правило в системах обработки данных гласит: при исполь- зовании неверных входных данных результаты будут неправиль- ными. Если ваша программа не защищена от использования не- верной информации, вы можете потратить массу времени на по- иски программных ошибок, в то время как истинной причиной отказа являются сами обрабатываемые данные. Хотя оператор, допускающий из-за небрежности ошибки при подготовке данных, достоин порицания, но и программисту непростительно создавать программу, которая воспринимает неверные данные как пра- вильные. Испытывайте программу в нормальных, экстремальных и исключительных условиях. 5.10.6 . ТЕСТИРОВАНИЕ ВЕТВЕЙ .Большую пользу в процессе тестирования приносят блок-схе- мы программ, показывающие возможные пути работы алгоритма. Блок-схемы можно использовать для установления критических ветвей программы и определения необходимых классов данных для проверки каждой ветви. Применение методов структурного программирования позволяет упростить тестирование ветвей, по- скольку последних при этом становится меньше. Подготавливайте тестовые данные для проверки каждой ветви алгоритма. Проверку ветвей легче всего осуществлять при тестировании отдельных модулей. Если же ее отложить до этапа испытаний
268 ГЛАВА 6 связанных модулей, тестирование ветвей становится более труд- ным делом. Рассмотрим модуль, в котором проверке подлежат п ветвей. Предположим далее, что существует второй модуль, ко- торый обращается к первому в т точках. Если каждый из этих модулей допускает независимую проверку, то необходимо опро- бовать всего т+п. путей. Если же эти два модуля испытываются совместно, то число подлежащих проверке путей работы програм- мы становится равным т\п. Кроме того, чем больше размер ис- пытываемой части программы, тем труднее определить местона- хождение обнаруженной ошибки. Следует, однако, помнить, что простое прослеживание всех возможных путей работы программы не обеспечивает необходи- мой полноты ее тестирования. Предположим, например, что про- грамма должна определить, равны ли между собой три числа, и в ней использован следующий алгоритм: IF ((X+Y+Z)/3=Y) THEN PRINT 'X, Y, Z, EQUAL' ELSE PRINT 'X, Y, Z, NOT EQUAL' Ясно, что тестовые данные, которые лишь проверяют факт рабо- тоспособности каждой из двух ветвей этой программы, приведут к тому, что она ошибочно будет считаться правильной. 5.11. ПРИМЕРЫ ТЕСТОВ Очень простую для тестирования задачу представляет провер- ка модуля, вычисляющего длину диагонали параллелепипеда, ко- торый показан на рис. 5.4. Длина диагонали в данном случае равна V V (А2+С2)2+В2. Необходимо сформировать тестовые данные.для нормальных, экстремальных и исключительных условий. Соответствующий контрольный пример приведен ниже. Номер теста Стороны па- раллелепипеда Примечания 1. 1 1 1 Хороший начальный тест 2. 1 2 3 Проверка в нормальных условиях 3. 0 0 0 Результат должен быть равен нулю 4. 0 1 2 Не параллелепипед. Что произойдет? 5. 1 0 3 Не параллелепипед. Что произойдет? - 6. 2 1 0 Не параллелепипед. Что произойдет? 7. 1—6 3 Неверные данные
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 269 В двух первых случаях программа проверяется в нормальных условиях функционирования. Тесты 3-7 относятся к экстремальным и исключительным условиям. Если при прогонке всех семи те- стов программа выдает правильные результаты, у нас есть все основания полагать, что модель работает верно. Можно было бы дополнительно проверить поведение программы при поступлении на вход больших и малых чисел. И хотя никогда нельзя со сто- процентной уверенностью гарантировать, что программа будет ра- ботать в любых ситуациях, в данном случае можно с высокой ве- роятностью считать, что это действительно будет так. Рис. 5.4. В качестве второго примера тестирования рассмотрим задачу нахождения корней квадратного уравнения ax2+bx-j-c=0. Пред- назначенная для решения этого уравнения программа считывает коэффициенты а, b и с и для вычисления двух корней использует формулу ___—&± —4ас Х~ 2а В этом примере мы имеем немного больше проверок, чем в пер- вом. Если при проверке всех девяти условий программа выдает Номер теста Коэффициенты Примечания 1. 11—2 Хороший начальный тест 2. 1 0 0,25 Проверка в нормальных условиях 3. ООО Что произойдет здесь? 4. 0 2 1 Должен получиться только один корень 5. 2 1 0 Все должно быть в порядке 6. 1 1 1 Комплексные корни 7. 0 0 2 Неправильное уравнение 8. 0 2 0 Должен быть один корень 9. 2 0 0 Должно быть два корня
270 ГЛАВА 5 правильные результаты, мы имеем все основания надеяться, что она будет работать правильно и в других ситуациях. Здесь уместно сделать два замечания. Во-первых, мы рас- смотрели подпрограммы, тестирование которых не вызывает ни- каких трудностей. Если вы будете применять модульное програм- мирование, ваши программы тоже можно будет делать простыми. Во-вторых, проведенное нами тестирование было целенаправлен- ным и систематизированным, так как случайный выбор чисел при- вел бы к большим трудностям в определении ручным способом ре- зультатов, которых следует ожидать при прогоне тестов. Кроме того, при случайном выборе тестовых данных могут оказаться непроверенными многие ситуации. Лучше всего преднамеренно подбирать такие данные, которые позволили бы проверить все возможные при функционировании программы ситуации. Только доскональная проверка программы может считаться хорошим те- стированием. 5.12. ТЕСТИРОВАНИЕ ПРОГРАММ МАТЕМАТИЧЕСКИХ ВЫЧИСЛЕНИЙ Е тестировании программных средств, работающих с цифровы- ми данными, необходимо принимать во внимание три главных ис- точника ошибок в численных результатах. Во-первых, это ошиб- ки, связанные с первоначальными данными (исходные ошибки}, примерами которых являются ошибки ввода, а также ошибки, связанные с ограниченной точностью измерения значений пере- менных (унаследованные ошибки). Последний тип ошибок связан с неопределенностью значений входных переменных и не может быть устранен никакими ухищрениями в вычислительном про- цессе. Вторым источником неверных результатов являются так назы- ваемые ошибки аналитического усечения, когда бесконечный про- цесс математических вычислений заменяется конечным алгорит- мом. Примером может служить использование вместо некоторой бесконечной числовой последовательности только шести ее членов. Третий источник ошибок в результатах — это ошибки округле- ния, вызываемые ограниченной точностью выполнения арифмети- ческих операций в машине. На величину таких ошибок влияют разрядность машины и основание системы счисления, в которой представляются числа в машине. Ошибки округления называют- ся также генерируемыми или порождаемыми ошибками в связи с тем, что они вносятся машинной программой при правильных входных данных. Проверка программ на отсутствие ошибок округления производится путем использования данных, для кото- рых заранее известны результаты обработки, и сопоставления этих результатов с теми, которые выдаются машиной в ходе те- стирования. Эталонные результаты определяются либо на основе
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 271 некоторых теоретических соображений, либо путем выполнения вычислений на другой машине, обладающей большей точностью представления чисел. Вопросы точности арифметических опера- ций особенно важно учитывать при построении тестов, поскольку почти правильные программы ведут себя особенно коварно. Процесс сопоставления машинных результатов с заранее за- данными (до начала тестирования) называется прямым анализом ошибок. Однако прямой анализ не всегда возможен или целесо- образен. Иногда бывает удобнее показать, что полученное ма- шиной решение задачи является точным решением исходной за- дачи. Такой способ действий называется обратным анализом оши- бок. В этом случае измеряются не расхождения между решением математической задачи и заранее определенным результатом, а различия между поставленной и решенной задачами. В качестве примера можно привести процедуру нахождения обратных зна- чений. Мерой близости решения к точному может в этом случае служить величина произведения исходного числа на обратное. Тем, кто интересуется проблемами программирования математических вычислений, можно рекомендовать книгу под редакцией Дж. Райса1. 5.13. МОДУЛИ Вероятно, некоторые из программистов склонны считать при- меры тестирования, приведенные в данной главе, изящными, но неприемлемыми для тех сложных программ, которые разрабаты- вают они сами. Не следует, однако, забывать, что, используя мо- дульный принцип построения, можно делать программы просты- ми. Только неопытные программисты пишут неделимые, громозд- кие и неудобные для тестирования программы. Большую моно- литную программу практически невозможно испытать достаточно полно из-за наличия в ней огромного количества логических пу- тей. Чем квалифицированнее программист, тем короче создавае- мые им модули. Основная цель модульного построения программы — это обес- печение легкого тестирования ее элементарных блоков. Каждый модуль должен предназначаться для выполнения одной функции, тогда в процессе его испытаний необходимо будет только убе- диться в том, что он правильно решает именно эту единственную задачу. Сборка программы из модулей, прошедших тщательную индивидуальную проверку, дает большую уверенность в том, что она будет функционировать нормально. Испытания отдельных модулей должны включать проверку связей и взаимодействий между модулями. Несмотря на то что области значений данных должны быть проконтролированы еще ’> Rice J. R. (ed.), Mathematical Software, Academic Press, New York, 1971.
272 ГЛАВА 5 на стадии отладки программы, нелишне еще раз проверить пра- вильность значений данных, передаваемых от одного модуля к другому, поскольку именно этот аспект функционирования про- граммы является обычно источником затруднений. В связи с тем что взаимодействие между модулями ограничивается передачей известных параметров, тестирование большой программы, ском- понованной из модулей, легче, чем большой программы, не содер- жащей модулей. Однако обязательным условием должно быть отсутствие в модулях логических ошибок; если же такие ошиб- ки выявляются на этапе тестирования всей программы, это зна- чит, что тестирование модулей было несовершенным. Кроме того, при использовании небольших модулей уменьша- ется вероятность возникновения ошибок в программе при внесе- нии изменений. Модули легко также включать в библиотеку про- грамм, а использование такой библиотеки автоматически сокра- щает объем работ, по тестированию, поскольку стандартные про- граммы, включаемые в библиотеку, предварительно испытывают- ся тщательным образом. 5.13.1,, ИМИТАЦИЯ РАБОТЫ МОДУЛЕЙ Часто возникает ситуация, когда нужный модуль не готов к моменту завершения испытаний какого-либо другого модуля, и поэтому требуется имитировать работу первого. Это может быть осуществлено двумя способами: посредством фиктивного модуля и посредством замещающего модуля. Фиктивный модуль — это такой модуль, который состоит толь- ко из одной точки входа и одной точки возврата. Используется для тестирования модулей более высокого уровня, если нужный реальный модуль еще не создан или не требуется. Часто оказыва- ется полезным наличие в фиктивном модуле некоторого операто- ра, сигнализирующего об обращении к нему. Замещающий модуль — это модуль, который выполняет ряд вычислений, но в очень упрощенной форме. Такие вычисления бы- вают необходимы в тех случаях, когда модулю более высокого уровня требуются для завершения процесса тестирования некото- рые величины, определяемые в реально отсутствующем модуле нижнего уровня. Применяется в тех случаях, когда использование фиктивного модуля не может дать желаемого эффекта. Замещающий модуль может использоваться и тогда, когда ре- альный модуль требует большого количества машинного времени или особых данных на выходе вызывающего модуля, что увеличи- вает продолжительность испытаний программы. 5.13.2. РЕКОМЕНДАЦИИ ПО ТЕСТИРОВАНИЮ МОДУЛЕЙ Легче всего тестировать модули, задавая контрольные данные на вход модуля самого высокого уровня. При таком подходе все данные оказываются внешними по отношению к модулям ниж-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 273s них уровней, подлежащим проверке. Может, например, оказаться,, что модуль ввода-вывода данных требует очень сложного про- граммирования, которое должно занять много времени, а многие другие модули могут быть запрограммированы гораздо раньше. В этом случае нецелесообразно задерживать тестирование гото- вых модулей и имеет смысл использовать для формирования те- стовых исходных данных замещающий модуль. В связи с тем что все данные сосредоточиваются в модуле высокого уровня, создание' такого замещающего модуля является несложным делом и обес- печивает тестирование готовых модулей в отсутствие реального модуля ввода-вывода. Для того чтобы можно было полностью охватить программу, каждый модуль следует делать небольшим по размеру, стремить- ся к тому, чтобы он не содержал большого числа логических вет- вей и работал с небольшими объемами данных. При соблюдении всех этих условий модуль получается сравнительно простым для тестирования, так как становится возможным учесть все экстре- мальные ситуации и проверить каждую ветвь процесса обработки данных. Один из методов тестирования модулей состоит в использова- нии специального «инициирующего» оператора, встраиваемого в модуль и генерирующего начальные тестовые данные. Такой ме- тод позволяет обойтись без фактических передач данных на вход модуля и сосредоточить внимание именно на тестировании самого модуля. Однако необходимость последующего тестирования опера- ций передач данных на вход модуля все же остается, так как этег операции могут быть связаны с ошибками, которые не обнару- живаются при применении инициирующего метода тестирования. 5.14. БИБЛИОТЕКА ПРОГРАММ После того как конкретные подпрограммы отлажены и испыта- ны, они подлежат включению в библиотеку программ. При нали- чии в вашей организации специальных бригад программистов эта работа может быть поручена библиотекарю. Наличие библиотеки программ сокращает объемы тестовых колод перфокарт и, что еще важнее, гарантирует использование всеми рабочими програм- мами одной и той же версии библиотечных программ. В некоторых случаях бывает желательно иметь одновременно две библиотеки: тестовую и рабочую. При этом тестовая библио- тека может содержать подпрограммы со встроенными средствами отладки и тестирования. Однако того же самого результата мож- но достичь, имея одну общую библиотеку, в программах которой используются операторы IF TESTING THEN ... подобные отладочным операторам, описанным в гл. 4. 18—899
274 ГЛАВА 6 5.15. ТЕСТИРОВАНИЕ ФАЙЛОВ При обработке записей, подверженных изменениям, каждая из них должна выдаваться на печать до и после каждого произве- денного изменения. Точно так же, если данные, которые выдает система при поступлении какого-либо сообщения, определяются содержанием последнего, такое сообщение должно печататься. Такой порядок действий облегчает тестирование, поскольку позво- ляет испытателю не прибегать к чтению обширных распечаток содержимого файлов. Массив тестовых записей должен быть сформирован в самом начале процесса тестирования, чтобы избежать в дальнейшем многократного повторения этой работы. Обычно эту функцию мо- гут выполнять программы-утилиты, которые удобно использовать для генерирования тестовых записей. Значительно облегчает те- стирование программ применение специальной программы — ге- нератора записей, которая формировала бы тестовые записи на основе заданных ей форматов, включая длину полей, их характе- ристики и содержание в режиме нормального функционирования. 5.16. СИСТЕМНЫЕ ИСПЫТАНИЯ До сих пор речь шла о тестировании программ. Однако не- редко существует и более крупная проблема — тестирование си- стемы программного обеспечения, или системные испытания. Тестирование системы осуществляется в направлении от про- стого к сложному. Обычно процесс испытания больших систем подразделяется на следующие этапы: Тестирование элементов. Это самый нижний уровень испыта- ний. В первую очередь тестируются подпрограммы, находящиеся на наибольшем удалении от начала основной программы. Тестиро- вание должно включать проверку при всех возможных правиль- ных входах и при достаточно большом наборе неправильных вхо- дов в целях испытания всех подпрограмм обработки ошибок. . Тестирование модулей. Модуль может представлять собой от- дельную рабочую программу системы, или системную подпрограм- му. Программисты должны сами тестировать создаваемые ими модули, прежде чем передать их для испытания на следующем, более высоком уровне. , Системные испытания. На этом этапе осуществляется сборка и групповая проверка модулей. Здесь обнаруживаются основные ошибки в согласовании функционирования элементов системы. Тесты для этого этапа должны подготавливаться теми специали- стами, которые разрабатывали программные спецификации. Приемочный контроль. Этот этап предусматривает проверку за- вершенного пакета программ, включая и его документацию. Полевые испытания. На данной стадии система демонстриру- ется узкому кругу заказчиков и осуществляется тщательное на-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 275 блюдение за ошибками. Устраняются недоработки в системной до- кументации. Заказчикам должна быть предоставлена возможность задания системе собственных контрольных примеров, а также (что им обычно предлагают) произвести попытку вызова аварийной си- туации в системе. Промышленные испытания. На этом этапе система передается заказчикам в постоянную эксплуатацию. Эти заключительные ис- пытания всегда завершает пользователь. Именно на этой стадии появляется возможность судить о том, насколько тщательно были проведены все предыдущие испытания. Уместно сделать еще несколько полезных замечаний относи- тельно организации рассмотренных выше этапов тестирования: 1. Выполняйте каждый раз только один этап тестирования, поддерживая постоянными возможно большее количество внеш- них факторов. 2. Переходите от простых тестов к сложным, постепенно уве- личивая объем входных данных. 3. Если программа регулярно отказывает на некотором уровне тестирования, следует провести ее дополнительные испытания на предыдущем уровне. 4. Помните правило, что количество трудностей, возникающих на каждом этапе тестирования, находится в обратной зависимо- сти от тщательности контроля программы на предыдущих этапах. 5. Потеря контроля над программированием и отладкой при- водит лишь к отсрочке решения неизбежных проблем (их пере- носу на стадию тестирования). 6. Небрежное тестирование программы непременно даст о се- бе знать при рабочих прогонах. 5.17. СРЕДСТВА ТЕСТИРОВАНИЯ Средства тестирования представляют собой программы, кото- рые помогают автоматизировать процесс испытаний системы; их часто называют средствами автоматического тестирования. Нали- чие таких средств дает возможность небольшой группе програм- мистов провести анализ программ большого объема, что в про- тивном случае было бы неосуществимо. Но прежде чем повести разговор об автоматических средствах тестирования, целесообраз- но рассмотреть ряд других вспомогательных средств, чрезвычайно облегчающих этот процесс. Прежде всего необходим хороший от- ладочный компилятор, способный выявлять ошибки, которые в противном случае должны были бы обнаруживаться при тестиро- вании. Такие компиляторы рассматривались в гл. 4. Необходимым условием получения правильной готовой программы является устранение синтаксических ошибок. Чем больше ошибок обнару- живается компилятором, тем меньше их остается на стадии те- стирования. В настоящее время наблюдается благоприятная тен- 18»
:276 ГЛАВА 5 денция расширения возможностей диагностики ошибок в языках программирования. Так, язык ПЛ/1 насчитывает порядка 1000 диагностических средств, включая предупреждения об ошибках. Второй тип вспомогательных средств тестирования — это па- кет подпрограмм, встраиваемых в рабочую программу для авто- матического исправления ошибок. Современные языки програм- мирования предоставляют возможность обнаруживать переполне- ние, потерю значимости, деление на нуль и недопустимые условия вызова подпрограмм. Более, современные компиляторы, такие, как WATFIV для ФОРТРАНа, и WATBOL для КОБОЛа, позволяют также обнаруживать нарушение правил индексирования масси- вов, неиспользуемые ветви программы и неправильно указанные параметры вызова подпрограммы. В связи с тем, что использова- ние этих дополнительных средств требует дополнительных затрат машинного времени, их применение ограниченно (возможности по обаружению 136 типов функциональных ошибок предоставляет язык ПЛ/1). Ошибки, которые способен обнаружить компилятор, неизбежно выявляются; поэтому применение компилятора позво- ляет не ждать, когда ошибка проявит себя в результате сочета- ния определенных условий, приводящих к отказу программы. Компилятор должен обладать также способностью слежения за ходом выполнения программы и контроля за определенными пере- менными. Оба эти свойства чрезвычайно полезны при тестирова- нии, так как дают возможность отслеживать в программе логи- ческие пути и последовательные изменения значений переменных. Еще одно необходимое средство тестирования — это возмож- ность хранения тестовых входных данных в целях их повторного использования. Таким средством в простейшем случае, когда про- грамма невелика, является колода перфокарт, а для сложной си- стемы это может быть целый набор программ. При этом необходи- мо иметь некоторый метод обеспечения хранения, дополнения и модифицирования тестовых данных. Все это требуется для того, чтобы иметь возможность повторного запуска тестов после вне- сения изменений в программу. 5.17.1. ГЕНЕРАТОР ТЕСТОВЫХ ДАННЫХ Хотя рассмотренные выше средства полезны и необходимы, они все же не решают полностью проблемы проверки программ. Первым действенным средством автоматического тестирования является генератор тестовых данных (ГТД), который формирует данные для использования в проверяемой программе. Такие дан- ные часто оказываются полезными как первый тест программы, позволяющий определить, пригодны ли конкретный модуль или подпрограмма к стыковке с другими элементами. Здесь, однако, имеется целый ряд трудностей. Во-первых, необходимо уметь пользоваться ГТД. Но изучение его может потребовать стольких
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 277 Таблица 5.1 Генератор тестовых данных Фирма-изготовитель IEBDG (в составе вспомогатель- ных программ операционной системы OS) PRO/TEST МетаКОБОЛ IBM Synergetics (One Garfield Circle, Burling- ton, Mass., 01803) Applied Data Research (11661 San Vicente Blvd., Los Ange- les, Cal. 90049) усилий, что целесообразнее окажется создавать тестовые данные вручную (особенно для небольших систем). Во-вторых, основное назначение ГТД — формирование больших массивов тестовых дан- ных. Однайо для тестирования нам требуется не просто произ- вольный набор данных, а хорошо подобранные данные для все- стороннего испытания тестируемой программы. Более того, те- стовые данные обычно должны формироваться в определенной последовательности; часто между ними могут существовать очень сложные зависимости (как, например, между главным и вспомо- гательным файлами). В одних тестовых пакетах эта проблема решается лучше, в других — хуже. В табл. 5.1 приведены сведе- ния о некоторых доступных для широкого применения генерато- рах тестовых данных. Некоторые другие ГТД обсуждаются в кни- ге Нэфтэли0. 5.17.2. ВСПОМОГАТЕЛЬНЫЕ ПРОГРАММЫ РАСПЕЧАТЫВАНИЯ ФАЙЛОВ (ПРОГРАММЫ-УТИЛИТЫ) Эти средства тестирования используются для выдачи на пе- чать ленточных или дисковых файлов, создаваемых испытываемой программой, и позволяют осуществлять визуальный контроль файлов, часто желательный и даже необходимый. Обычно про- граммы такого типа имеются в составе стандартного програм- много обеспечения, поставляемого изготовителями ЭВМ. Жела- тельные характеристики этих программ — распечатывание каж- дой записи с новой строки, способность напечатать информацию в различных формах представления, например в символьной, деся- тичной и шестнадцатеричной, возможность печатать требуемое количество записей, а также начинать и прекращать этот про- цесс на любой указанной записи. ° Naftaly S. M., Cobol Support Packages, Wiley, New York, 1972.
278 ГЛАВА 5 5,17.3. КОМПАРАТОР ФАЙЛОВ Компаратор файлов представляет собой программу, которая считывает два файла и выводит на печать их различающиеся эле- менты. Он может использоваться для сопоставления результатов, выданных при двух различных тестовых прогонах, с целью выяв- ления каких-либо расхождений. В связи с тем что программы пос- ле внесения в них дополнений или изменений должны тестиро- ваться повторно, лучше всего хранить тестовые входные данные в отдельном файле и вновь использовать их для проверки модифи- цированной программы. Однако в этом случае кто-то должен сидеть и вручную контролировать выдаваемые программой резуль- таты, чего обычно не делают из-за значительной трудоемкости этой неинтересной работы. Тогда и приходит на помощь компаратор файлов, используе- мый для сравнения каждого нового массива результатов с выве- ренным массивом, полученным при последнем тестовом прогоне. При этом программа сравнения файлов распечатывает только об- наруженные различия, и ручная проверка требуется только для несовпадающих элементов. В заключение новый файл результа- тов записывается вместо прежнего с тем, чтобы служить этало- ном при последующем сравнении. 5.17.4. ПРОГРАММЫ-ПРОФИЛИРОВЩИКИ О программах-профилировщиках речь уже шла в гл. 3. Профи- лирование программы оказывается весьма полезным для отладки и тестирования, так как дает информацию о том, какие операторы и сколько раз выполнялись. Профилирование выявляет модули или части программы, которые оказались неиспользованными, и указывает на необходимость продолжения тестирования. Средст- ва профилирования программ предусматривают возможность на- копления статистических данных о работе тестируемой системы. Такие статистические данные позволяют испытателям выявлять те блоки программ, которые недостаточно проверены. Поэтому обра- тите внимание на средства профилирования, приведенные в разд. «Проекты». 5.17.5. ТЕСТОВЫЙ МОНИТОР Тестовый монитор — это программа, которая пересылает нуж- ные данные на вход тестируемого модуля и накапливает выходные данные, выдаваемые на печать или записываемые в файл. Про- грамму тестового монитора часто называют тестовым драйвером. Назначение этой программы состоит в том, чтобы создавать нуж- ные условия для проверки модулей. Иногда монитор представля- ет собой небольшую программу, предназначенную для тестирова-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 279 ния отдельных подпрограмм, а порой он может быть очень слож- ной программой. Хороший тестовый монитор должен предусмат- ривать распечатывание входных параметров и всех переменных, формируемых проверяемым модулем. Тестирование по принципу сверху вниз в какой-то мере исключает необходимость использо- вания тестового монитора благодаря выполнению его функций главной программой. 5.18. КОНТРОЛЬ РЕЗУЛЬТАТОВ С ПОМОЩЬЮ ПРОВЕРЯЕМОЙ ПРОГРАММЫ Проверка результатов тестирования может осуществляться са- мой тестируемой программой. Этот процесс часто называют са- мопроверкой программой обеспечения. Так, например, если про- веряемый модуль должен извлекать квадратный корень из числа, то лучший путь тестирования этого модуля состоит в том, чтобы вызвать его с помощью вспомогательной подпрограммы и затем выдать на печать вычисленное значение квадратного корня, квадрат его величины и разность между значением входного ар- гумента и квадратом вычисленного значения корня. Описанная процедура называется реверсивной проверкой. Другими областя- ми ее применения могут быть обращение матриц, преобразования Фурье, шифрирование — дешифрирование и ряд других преобразо- ваний. В целях сокращения объема ручной работы по контролю результатов реверсивной проверки модуля можно применять те- стовый драйвер или монитор. Это особенно удобно при больших объемах информации, когда драйвер может быть настроен на фик- сацию только неблагоприятных исходов тестирования, что сокра- щает общее количество выдаваемых результатов. Иллюстрацией реверсивной проверки является контроль пра- вильности решения системы уравнений с помощью подстановки решения в исходную систему. В некоторых случаях между вы- ходными переменными может существовать простая зависимость, и тогда правильность результатов может устанавливаться на ос- нове контроля этой зависимости. Примерами таких ситуаций яв- ляются упорядоченные массивы или какие-либо групповые итого- вые суммы, которые должны взаимно балансироваться (перекрест- ные итоги). В большинстве случаев для проверки правильности выходных результатов тестирования требуется иметь алгоритм, который по своей сложности не уступает самой испытываемой программе. Однако даже и в такой ситуации часто существует возможность установить правдоподобность результатов. Например, начисляе- мые денежные суммы должны быть не меньше или не больше не- которых установленных пределов, и этот факт можно использо- вать для контроля результатов вычислений. Особенно важно проявлять аккуратность в оценке результатов и не допускать, чтобы неверные результаты принимались за пра-
280 ГЛАВА 5 вильные. Если вы имеете некоторые промежуточные результаты, выданные машиной, и затем проверяете каждый шаг расчетов, то все, что вы делаете в данном случае, — это проверку способности машины правильно умножать. Но непроверенными остаются кор- ректность выбора математических операций и правильность по- рядка их выполнения. В этом отношении описанная выше процеду- ра реверсивной проверки модуля для извлечения квадратного кор- ня свободна от указанного недостатка, так как вычисления были проделаны в обоих направлениях и результаты сопоставлены меж- ду собой. И хотя эта процедура не дает абсолютной гарантии от ошибок, она является примером полного контроля. 5.19. ОКОНЧАТЕЛЬНОЕ УТВЕРЖДЕНИЕ ПРОГРАММЫ В какой-то момент времени тестирование считается закончен- ным. Однако до этого программа должна быть всесторонне испы- тана с использованием тестовых и реальных данных и признана завершенной ее рабочая документация. . Если имеется возмож- ность, предназначайте программу первоначально для ограничен- ного числа заказчиков. Это позволит ввести еще один этап испы- таний перед тем, как программа будет признана пригодной для широкого применения. Все заключительные испытания должны проводиться заказ- чиком, подобно тому как проверяются пользователем програм- мные средства, поставляемые изготовителями ЭВМ. Ведь сам программист редко способен предугадать все возможные затруд- нения и ликвидировать их посредством объективного тестирова- ния. Заказчики же, как правило, регистрируют каждую обнару- женную ошибку. Ошибки эти могут затем исправляться, а про- грамма тестироваться заново с использованием прежних тестовых данных. Если программа успешно прошла указанные испытания у огра- ниченного числа заказчиков, она готова для окончательной пере- дачи в эксплуатацию. При этом не следует забывать о корректи- ровке документации по результатам внесения изменений. 5.20. ПЛАНИРОВАНИЕ ТЕСТИРОВАНИЯ ПРОГРАММ Одно из необходимых условий надлежащего тестирования про- грамм— это выделение на него в плановом порядке достаточных ресурсов времени, которые, как показывает практика, примерно равны затратам времени на программирование. Поскольку суще- ствует тенденция планировать только работу по программирова- нию, нетрудно понять, отчего все проекты выполняются с нару- шениями сроков. Это говорит о необходимости раздельно плани- ровать затраты времени на разработку, кодирование, отладку и тестирование программ и отдавать себе отчет в том, что сдвиг сро-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 281 ков окончания разработки алгоритма на неделю будет приводить к такому же сдвигу сроков по всем остальным этапам. Системам, для которых крайне необходимы всесторонние ис- пытания, требуется еще и дополнительный резерв времени. На- пример, по оценкам НАСА, стоимость испытаний программ в рам- ках проекта «Аполлон» составила 80% общей стоимости разработ- ки программного обеспечения. Конечно, эта цифра столь велика потому, что указанный проект требовал всеобъемлющего тести- рования, однако подобные оценки по операционным системам ЭВМ показывают, что на их тестирование приходится от 30 до 60% общих затрат на разработку. 5.20.1. ГРАФИКИ ПРОВЕДЕНИЯ ИСПЫТАНИИ Гораздо легче контролировать испытания,- если они проводятся по этапам. Очень часто пренебрегают делением на этапы, а выде- ляют общее время на проведение всех испытаний. В результате затруднения, возникающие на каком-либо одном этапе, погло- щают время, требуемое для других этапов. Планируйте надлежащим образом затраты времени на испытания. Необходимо выделять определенное время для каждого этапа испытаний. Тогда двухнедельная задержка в тестировании какого- либо элемента программы будет своевременным предупреждением руководителям проекта о том, что если не выделить дополнитель- ных ресурсов, то вся работа будет выполнена с отставанием от графика на две недели. К сожалению, часто в целях соблюдения установленного графика в случае возникающих задержек после- дующие этапы испытаний проводятся поверхностно. Планирова- ние испытаний позволяет строже контролировать процесс тести- рования и заблаговременно узнавать о возможных нарушениях графика. 5.20.2. ПЛАНИРОВАНИЕ ПРОГРАММНЫХ ИСПЫТАНИИ Очень часто бывает желательно получать информацию о ходе тестирования программ. При этом нас интересуют ответы на два вопроса: выдерживается ли график? достаточно ли полно прове- рена программа? Получить ответы на эти вопросы столь же важ- но, сколь и трудно. Однако существуют методы работы, позволяю- щие следить за ходом тестирования программ. Как известно, большие системы обычно разделяют на подси- стемы или компоненты. Компоненты в свою очередь состоят из элементарных блоков, представляющих собой наименьшие раз- личные элементы программ. Может существовать много уровней такого разбиения и множество элементов на каждом уровне. Тра-
282 ГЛАВА 5 диционный подход к тестированию предполагает первоначальную проверку наименьших, элементов с последующим объединением их в более крупные блоки и повторением проверки постепенно укруп- няемых узлов до тех пор, пока не пройдет испытания вся систе- ма в целом. Для придания тестированию целенаправленного характера не- обходимо уже в процессе программирования сформировать биб- лиотеку контрольных примеров. Эта работа должна выполняться не программистами, а группой испытателей, работающей парал- лельно с ними; однако необходимым условием является использо- вание обеими группами одних и тех же исходных требований (о группах испытателей, или тестирования, речь будет идти ниже). Если требования сформулированы нечетко, то разработанные те- сты не будут согласовываться с программой, что, вообще говоря, помогает своевременно уточнять проектную документацию. Парал- лельная разработка тестов служит не только целям подготовки программных испытаний, но и целям проверки правильности сфор- мулированных требований к системе. Поэтому, если какая-либо программа не работает при прогонке предназначенных для нее тестов, этот факт свидетельствует скорее об ошибке в специфика- ции, чем в тесте или в программе. Когда контрольные примеры проектируются специальной груп- пой испытателей, может быть поставлен вопрос об отчетности этой группы по показателям, характеризующим процент опробованных тестов и процент тестов с благоприятными результатами испыта- ний. Располагая ежедневной информацией о результатах тестиро- вания, руководство может контролировать ход программных ис- пытаний. Если не предпринимать никаких попыток систематизации контрольных примеров, то сомнительна сама возможность отыс- кания каких-либо объективных способов оценки текущего состоя- ния и успешности проведения испытаний. Немало систем програм- много обеспечения потерпело неудачу из-за пренебрежения руко- водством процедурой долгосрочного планирования и из-за недо- оценки напряженности и многосторонности усилий, необходимых для исчерпывающих испытаний системы. 5.21. ОЦЕНКА ПОЛНОТЫ ПРОВЕРКИ ПРОГРАММЫ Иногда желательно знать, насколько полно выполнено тести- рование той или иной программы. Если программа велика по объе- му и содержит много логических блоков, то имеются основания полагать, что она будет проверена не особенно хорошо. Однако и в этом случае полезно иметь представление о степени полноты выполненной проверки программы. Элементами такой оценки мо- гут служить следующие данные: 1) доля проверенных частей программы; здесь следует стре- миться к 100%;
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 283 2) процент условных переходов программы, которые при те- стировании проверялись по обоим логическим направлениям; здесь тоже следует стремиться к достижению предела 100%; 3) степень сегментированности программы; хорошо сегменти- рованные программы обычно легче тестировать, а следовательно, и само тестирование выполняется более тщательно; 4) степень проверки различных ситуаций взаимодействия от- дельных частей программы. Самым уязвимым местом тестирования является, как правило, последний элемент оценки. В больших программах нет возможности проверить все логи- ческие пути и опробовать все мыслимые сочетания данных, однако все главные ветви подлежат обязательной проверке. Что касается неосновных ветвей, то здесь программисту следует, полагаясь на свою интуицию, решить, какие из них могут обеспечить наиболь- ший эффект тестирования (т. е. определить пути, наиболее под- верженные ошибкам). На этом этапе процесс тестирования про- граммы превращается из науки в искусство. В связи с тем что число сочетаний и перестановок логических путей может до- стигать астрономических значений, большая программа никогда не может быть испытана до конца. Однако особый интерес долж- ны представлять два режима тестирования: минимальные и мак- симальные значения входных переменных. Обычно и те и другие определяются довольно легко. Основные факторы, которые испы- татель программы должен принимать во внимание, — это важ- ность конкретных логических путей, объем предварительного те- стирования модулей, уязвимость программы для ошибок, затраты на испытания и возможная отдача. 5.22. ПОВТОРНОЕ ТЕСТИРОВАНИЕ Обычным делом в процессе тестирования программы является внесение в нее изменений, связанных, с исправлением обнаружен- ных ошибок. Эти изменения особенно опасны из-за возможности возникновения новых ошибок, так как программист, вносящий из- менения, бывает озабочен лишь устранением выявленной ошибки и легко упускает из виду другие факторы, которые могут приве- сти к новым затруднениям. Другим опасным в этом смысле дей- ствием является модифицирование программы. Учитывая эти об- стоятельства, следует сохранять старые тестовые данные для того, чтобы, используя их повторно для проверки измененной програм- мы, приобрести уверенность в отсутствии новых ошибок. С каждой программой должен быть постоянно связан некото- рый набор контрольных примеров для ее первоначального тести- рования, а также для повторных проверок после каждого внесе- ния изменений. Если такой набор тестов доступен для многократ- ного использования и может по требованию запускаться автома-
284 ГЛАВА б тически, вряд ли станет программист пренебрегать тестированием в любых условиях. Все сохраняемые для многократного использования контроль- ные примеры должны вводиться в работу по повторному тестиро- ванию возможно раньше. Неразумно было бы прогонять несколь- ко тестов, устранять обнаруженные ошибки, а повторное тести- рование откладывать на более позднее время. Если программа» подлежащая тестированию, плоха, лучше обнаружить это немед- ленно. Кроме того, гораздо выгоднее исправлять сразу большое число ошибок, чем устранять их поодиночке, или, если брать худ- ший случай, дешевле переписать сразу весь неработоспособный модуль, чем делать это в разное время по частям. Библиотеки тестовых входных и выходных данных иногда на- зывают сценариями. Создание хороших контрольных примеров яв- ляется трудоемким делом, поэтому тестовые данные не следует уничтожать после использования — их необходимо сохранять для повторного применения в дальнейшем. Для того чтобы будущие пользователи сценариев хорошо понимали их и могли применить на практике, сценарии необходимо полно документировать. Это жизненно важно, поскольку потребность в их применении может возникнуть через несколько лет, когда после модификации соот- ветствующей программы необходимо будет выполнить ее повтор- ное испытание. Повторное тестирование иногда называют избыточным или регрессивным. Его отличие от первоначального тестирования за- ключается в том, что работа ведется в направлении от сложных тестов к простым, причем прогон последних имеет место лишь в; том случае, если предшествующие более сложные тесты дали не- благоприятные результаты. Тогда возврат к более простым те- стам способствует выявлению ошибок. Повторяйте тестирование после каждого случая внесения изменений в программу.. 5.23. ГРУППА ТЕСТИРОВАНИЯ Хорошо зарекомендовал себя такой подход к тестированию, когда создается специальная группа из опытных программистов, в задачу которой входит испытание всех программ и систем, преж- де чем-они будут окончательно переданы в постоянную эксплуа- тацию. Такой подход, однако, не освобождает программиста — автора программы — от обязанности ее первоначального тестиро- вания. Группа тестирования обеспечивает только заключительную- проверку программы уже после того, как программист гаранти- ровал ее правильность. При этом группа тестирования может рассматривать програм- му как «черный ящик», который при подаче на его вход конкрет-
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 285» ных входных данных должен выдавать известные результаты. Иногда людей, входящих в группу тестирования, называют про- фессиональными идиотами (отнюдь не в оскорбительном смысле). Такое название вошло в обиход потому, что люди, входящие в группу тестирования, хорошо умеют формировать неверные входные данные, т. е. такие, которые только идиоту (теперь уже в буквальном смысле этого слова) могли бы прийти в голову за- дать на вход программы. Иначе говоря, работа профессиональных испытателей должна носить разрушительный характер. Их цель- заключается в том, чтобы попытаться привести систему к отказу. Отказ системы считается для них благоприятным исходом. В про- тивоположность этому программист хочет, чтобы его программа в процессе испытаний работала безотказно. Таким образом, про- граммист и испытатель преследуют разные цели. При создании постоянной группы тестирования у последней’ имеется возможность накапливать практический опыт по тести- рованию программ. Кроме того, свежий взгляд на программу часто может выявить такие проблемы, о которых сам программист мог и не задумываться. Следовательно, всегда целесообразно при- влекать к тестированию своей программы кого-нибудь посторон- него. Тестирование и утверждение, осуществляемые группой тести- рования, иногда называют приемочным контролем. Если програм- мисты проверяют свои программы, используя дедуктивно-анали- тические методы и свое знание их внутренней логики, то профес- сионалы-испытатели применяют эмпирический подход к тестиро- ванию, поскольку имеют лишь общее представление о внутренней логике испытываемой программы. По своему положению испыта- тель находится где-то посередине между разработчиком и заказ- чиком программы. Испытатель должен полагаться на программные специфика- ции, и, если последние нечетки или непоследовательны, этот факт' выявляется при испытаниях программы. Специалист, проводящий' приемочный контроль, должен вынести свое суждение о том, со- ответствует ли программа целям и требованиям, сформулирован- ным в программной спецификации. Если же группа тестирования не может уяснить этих целей и требований из спецификации, то- ни о каком тестировании не может быть и речи. Испытатель должен также дать ответы на следующие вопросы: 1. Достаточно ли точны полученные результаты? 2. Обладает ли программная документация той полнотой, ко- торая необходима для обеспечения удобства эксплуатации про- граммы? 3.. Достаточно ли уделили внимание устранению ошибок? Преимущества создания специальной группы тестирования со- стоят в следующем:
286 ГЛАВА 5 1. Эта группа не расформировывается по окончании каждого конкретного проекта и потому может накапливать опыт по те- стированию и разрабатывать специальные методы и средства, ко- торые обеспечивали бы более полную и эффективную проверку программ. 2. Эта группа лояльна по отношению не только к пользовате- лям, но и к коллегам, разрабатывающим программы. Задача груп- пы тестирования заключается не в том, чтобы доказать правиль- ность выполнения разработчиками своей работы, а в том, чтобы показать пригодность программы для конечного ее пользователя. 3. В связи с тем что группа тестирования в какой-то мере вы- ражает интересы заказчика, она имеет возможность склонить разработчика программы к ее надлежащему тестированию. 5. 24. ЗАКЛЮЧИТЕЛЬНЫЕ ЗАМЕЧАНИЯ О квалификации программиста судят по числу ошибок, обна- руживаемых в его программе после того, как она передана для широкого использования. Поэтому лучше иметь репутацию мед- лительного программиста, но создающего хорошие и тщательно выверенные программы, чем репутацию быстро работающего про- граммиста, но разрабатывающего программы, испещренные ошиб- ками. Создание программ без ошибок — трудоемкая работа. Традиционный вопрос программирования состоит в следующем: почему у нас никогда нет времени делать сразу хорошую програм- му, но зато всегда находится время на ее исправление? Торопли- вые и недобросовестные программисты подрывают репутацию других программистов, которые полагаются на достоверность ре- зультатов, выдаваемых чужими программами. Недаром все важ- ные программы поручают разрабатывать программистам, которые известны как авторы правильных, тщательно выверенных про- грамм. И если вы обладаете именно такой репутацией, то разум- но всегда настаивать на выделении необходимого времени для обеспечения надлежащего качества тестирования программ. На- рушение этого условия приводит к неполному тестированию, в то время как его соблюдение дает хорошие результаты. 5. 25. СОВЕТЫ ПРОГРАММИСТУ Обходитесь минимальным количеством контрольных примеров. Учитывая, что исчерпывающее тестирование невозможно, испыты- вайте программу разумно. Начинайте тестирование как можно раньше. Прежде всего проводите ручную проверку. Старайтесь проверять правильность принципов построения систе- мы на ее простом варианте. Старайтесь применять тестирование по методу сверху вниз.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 287 В каждом следующем тесте должен использоваться класс данных, отличный от предыдущего. Испытывайте программу в нормальных, экстремальных и исклю- чительных условиях. Подготавливайте тестовые данные для проверки каждой ветви алгоритма. Планируйте надлежащим образом затраты времени на испытания. Повторяйте тестирование после каждого случая внесения измене- ний в программу. 5. 26. УПРАЖНЕНИЯ ПОВТОРЕНИЕ ПРОЙДЕННОГО 1. Что означают следующие термины: а) живучесть программы, б) тестирование ветвей, в) тестирование элементов, г) приемочный контроль, д) промышленные испытания, е) избыточное тестирование, ж) проверка спецификаций, з) дымовой тест, и) системные испытания, к) полевые испытания, л) утверждение, м) профессиональный идиот. 2. Чем отличается отладка программы от тестирования? Счи- таете ли вы, что тестирование и отладка программы должны быть самостоятельными стадиями? Если да, то почему? 3. Какие обычно выдвигаются оправдания при отказе про- граммы? 4. Назовите ряд преимуществ планирования испытаний. 5. Когда возникает потребность в тестировании программы профессиональным испытателем? 6. Перечислите преимущества использования специальных под- программ при проведении тестирования. 7. Назовите три способа определения результатов, которые должны соответствовать тестовым данным. 8. Почему важно выполнять повторное тестирование? ЗАДАНИЯ 9. Как вы думаете, достаточно лн, чтобы контрольные приме- ры проверяли каждый исключительный случай отдельно или не- обходимо создавать еще и комбинации исключительных условий? 10. Почему целесообразно обрабатывать большие объемы ре- альных тестовых данных, даже если нет возможности проверить достоверность этих результатов?
288 ГЛАВА 5 11. Подыщите пример проекта, который был забракован на стадии тестирования. Проанализируйте, что явилось причиной такого положения: плохая подготовка тестов, неудачные проект- ные решения, изменение первоначальных требований или какие- либо другие факторы? 12. Возьмите какую-нибудь простую программу и подсчитайте в ней количество различных логических путей. Возможно ли для этой программы исчерпывающее тестирование? Какие вам извест- ны способы уменьшения количества тестируемых путей? 13. Разработайте контрольные примеры для тестирования про- граммы, которая а) считывает два целых числа и определяет их наибольший общий делитель, б) считывает два целых числа и определяет их наименьшее об- щее кратное. 14. Разработайте контрольные примеры для проверки одной из программ, приведенных ниже. Определите нужные для каждой программы классы тестовых данных. Каково минимальное число тестов, необходимое для каждой программы? Что представляют собой их граничные точки? а) Программа извлечения квадратного корня. * 6) Программа, которая считывает три числа, являющиеся по предположению длинами сторон некоторого треугольника, и за- тем печатает сообщение о типе треугольника (с разными сторона- ми, равнобедренный или равносторонний). в) Программа, которая считывает две даты и вычисляет коли- чество прошедших дней. г) Программа, которая считывает значение числа N, затем считывает N чисел и печатает максимальное и минимальное зна- чения чисел в группе. д) Программа, которая считывает некоторый элемент данных и проверяет, нет ли такого же элемента в хранимом массиве. 15. Обратитесь к имеющимся у вас руководствам по програм- мированию и определите допустимые области значений аргумен- тов для следующих функций: логарифма с основанием 10, логарифма с основанием е, синуса, косинуса, гиперболического синуса, гиперболического косинуса, Напишите программу для вычисления одной из этих функций, по- пробуйте нарушить допустимые границы аргумента и понаблю- дайте, что при этом произойдет. 16. Любая функция, описываемая математическим выражени- <ем, вычисляется не совсем точно вследствие ошибок, свойствен- ных вычислительному процессу. Попробуйте найти в руководстве данные о величине ошибки, возникающей при вычислении значе- ний таких функций, как SQRT, SIN, TAN и др.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 289. 17. Подготовьте для какой-нибудь программы а) тестовые данные, создаваемые программистом; б) реальные модифицированные данные; в) реальные данные в полном объеме. 18. Дана формула у ab c + d cd a — b Подготовьте контрольные примеры для проверки соответствую- щей программы а) в нормальных условиях, б) в экстремальных условиях, в) в исключительных ситуациях. 19. Вспомните случай отказа какой-нибудь программы при ра- бочем прогоне. Определите, могли ли хорошо спланированные испытания выявить ошибку, вызвавшую отказ. 20. Какие подпрограммы автоматического тестирования до- ступны в используемом вами языке программирования? 21. Какие у вас имеются программы-утилиты, пригодные для использования при тестировании рабочих программ? 22. Напишите программу для системы резервирования мест на авиалиниях. Известно, что ежедневно выполняются пять рейсов под номерами 142,. 148, 153, 181 и 191. Заказы принимаются толь- ко за неделю вперед. Ваша программа должна удовлетворять за- явки, аннулировать заказы и отказывать в резервировании мест, если самолет уже полностью укомплектован пассажирами. Для упрощения считайте, что самолет вмещает шесть пассажиров. Име- ются три класса мест: первый, туристский и студенческий. Если хотите, программу можно усложнить, введя правило, по которому пассажиры первого класса имеют более высокий приоритет по сравнению с туристами, туристы — более высокий приоритет по сравнению со студентами и т. д. Разработайте несколько конт- , рольных примеров для тестирования этой программы. 23. Исходя из собственного опыта программирования, опреде- лите, какой процент вашего времени уходит на написание, от- ладку и тестирование программы. 24. Возьмите какую-нибудь хорошо знакомую вам программу и разработайте тесты для проверки ее а) в нормальных условиях, б) в экстремальных условиях, в) в исключительных случаях. 25. Напишите программу нахождения кубического корня из заданного числа и используйте ее же для проверки точности вы- числений. 26. Напишите программу нахождения корней квадратного урав- нения ах2 + 6х + с=0. 19—899
290 ГЛАВА 5 Корни определяйте по формуле ___ —b ± Ь2— 4ас х~ 2а Используйте ту же программу для проверки правильности ре- шения путем подстановки найденных корней в исходное уравне- ние и сравнения результата с нулем. Выполните тестирование про- граммы вначале с помощью собственных тестовых данных, а за- тем примените для тестирования следующие контрольные при- меры: а b с а b с 1 2 1 6 5 —4 1 —1 —6 6*1030 5*1030 —4*10*> 1 ' —10 1 Ю-зо — Юзо Юзо 1 —1000 1 1.0 —4.0 3.9999999 1 —10000 1 L0 —4.0 4.0 1 2 3 1.0 100.0001 0.01 В заключение перепрограммируйте задачу заново с использо- ванием данных повышенной точности и отметьте, улучшилось ли качество результатов. 27. Разработайте тестовые данные для программы «естествен- ного отбора», работающей по следующему алгоритму: вычисляет- ся среднее значение и и стандартное отклонение s видового при- знака t по формулам П- (п. S 1=1 1=1 \ 1/, (*<-«)* у П I и затем объекту присваивается оценка А, если видовой признак принимает значения, боль- шие или равные «-j-2s; оценка В, если видовой признак больше или равен u-j-s, но меньше «+2«; оценка С, если видовой признак больше или равен и—s, но меньше u-J-s; оценка D, если видовой признак больше или равен и—2s, но меньше и—s; оценка F, если видовой признак принимает значения мень- ше и—2s.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 291 28. Предположим, что ваша ЭВМ может работать с числами, содержащими не более шести значащих разрядов, но вам необ- ходимо выполнить сложение с сохранением восьми значащих раз- рядов. Вы решили эту проблему путем разбиения задаваемых чи- сел на две тетрады и последующего раздельного сложения соот- ветствующих тетрад. Так, например, число 00123456 было бы пре- образовано в две тетрады 0012 и 3456 и сложение чисел 00123456 и 65432100 приняло бы вид 0012 3456 6543 2100 6555 5556 Ваша программа, предназначенная для выполнения сложения восьмиразрядных чисел указанным способом, должна работать как с положительными, так и с отрицательными числами. Вначале разработайте для этой задачи тестовые данные; затем напишите программу и испытайте ее с помощью ваших тестовых данных. ПРОЕКТЫ 29. В этой главе было рассмотрено несколько генераторов те- стовых данных. Исследуйте, какие еще аналогичные пакеты про- грамм доступны для использования, и оцените их полезность. Раз- работайте набор требований, которым должен удовлетворять хо- роший генератор тестовых данных. 30. Выше были описаны некоторые дополнительные средства тестирования. Оцените степень их доступности и полезности. Ряд пакетов тестирования рассматривается также в книге под редак- цией Гетцеля [7], посвященной методам тестирования программ. 31. Компаратор файлов. Напишите или возьмите готовую про- грамму, которая считывала бы два файла, сравнивала их между собой и выводила на печать лишь различающиеся элементы. Оформите необходимую программную документацию и сделайте вашу программу общедоступной для пользователей вашего вычис- лительного комплекса. Подобная программа (FILCOM) на языке БЕИСИК-ПЛЮС имеется в составе программного обеспечения машин PDP. 32. Соберите статистические данные, характеризующие а) время, затрачиваемое на разработку, кодирование, отладь ку и тестирование программ, б) объем программ, в) используемый язык программирования, г) квалификацию программиста. Попытайтесь установить, существуют ли между этими харак- теристиками корреляционные связи; например, оказывает ли влия- ние используемый язык или время, затраченное на разработку про- граммы, на продолжительность стадий тестирования или отладки. 19*
292 ГЛАВА S Тем, кто интересуется подобными вопросами, рекомендуется озна- комиться со статьей Боэма и книгой Вейнберга1*. 33. Для оценки надежности программного обеспечения могут использоваться следующие характеристики: а) среднее время наработки между ошибками, характеризую- щее средний интервал времени между прерываниями по програм- мным ошибкам; б) среднее время восстановления, показывающее как быстро выполняется необходимая корректировка программы при обнару- жении в ней ошибки; в) коэффициент готовности, определяющий долю времени, в течение которого программное обеспечение находится в работоспо- собном состоянии; г) график распределения числа ошибок по календарным ме- сяцам, позволяющий по крутизне кривой приближенно судить о достигнутом прогрессе. Проанализируйте указанные характеристики и примените ка- кие-нибудь из них для оценки надежности используемых вами программных средств. 34. Напишите программу, которая генерировала бы достаточно полный набор тестовых данных для проверки способности исполь- зуемого вами компилятора выявлять синтаксические ошибки в арифметических выражениях, операторах вызова подпрограмм и циклах оператора DO. 35. Разработайте тестовые данные для детальной проверки од- ной из программ вычисления значений стандартных функций, та- ких, как абсолютная величина переменной, квадратный корень, синус, косинус и т. д. Какими должны быть критические значе- ния тестовых данных? 36. Многие пытаются доказывать правильность программы, ис- пользуя те же приемы, что и математики при доказательстве пра- вильности математических построений. Изучите современное со- стояние проблемы доказательства правильности машинных про- грамм и подумайте, какое влияние на программирование может оказать применение подобных доказательств? 37. Представляется полезным иметь на вооружении какой-ли- бо метод определения степени полноты тестирования систем про- граммного обеспечения. Ряд принципов, относящихся к этому во- просу, изложен в данной главе. Попытайтесь на основе изложен- ных представлений разработать свой способ оценки полноты про- верки программ. Для выполнения этой работы рекомендуется так- же обратиться к материалу гл. 4, касающейся проблем отладки. 38. Журнал программных испытаний. Подобно обычному днев- нику, этот документ фиксирует ход тестирования программ. Он может использоваться на заключительной стадии осуществления *> Boehm В., Software and Its Impact: A Quantitative Assessment, Datamation (May 1973); Weinberg G., Psychology of Computer Programming.
ТЕСТИРОВАНИЕ (ИСПЫТАНИЕ) ПРОГРАММ 293 проекта для оценки успеха реализации плана испытаний. Кроме того, при наличии такого журнала никому не захочется оставлять в нем запись, аналогичную следующей: «Тест номер 2 должен быть повторен, так как был составлен мною неправильно». По- пробуйте вести такой журнал в процессе испытаний какой-либо программы. 39. Программы-профилировщики. Существует целый ряд паке- тов программ, предназначенных для анализа последовательности действий, выполняемых рабочей программой. Эти служебные про- граммы подсчитывают, какое количество раз выполнялся каждый оператор, а иногда еще и проверяют программу на наличие оши- бок и непредусмотренных свойств. Вот некоторые из упомянутых пакетов: а) Пакеты COTUNE— профилировщик КОБОЛа и FOTUNE — профилировщик ФОРТРАНа (CAPEX Corp., 2613, North Third Street, Phoenix, Ariz. 85004). б) Пакет PROFILE (CACI, Inc., 12011 San Vincente Blvd., Los Angeles, CA. 90049). в) Пакет метаКОБОЛ (Applied Data Research, Route 206 Cen- ter, Princeton, N. J. 08540). г) Пакет RXVP (General Research Corp., 5383 Hollister Ave., Santa Barbara, Ca). д) Пакет PET — Program Evaluator and Tester (McDonnell Douglas Astronautics Company). Последние два пакета описыва- ются Гилбом в статье, вошедшей в Труды симпозиума IEEE 1973 г. по вопросам надежности программного обеспечения ЭВМ и в книге Software Metrics. Ознакомьтесь с описаниями перечис- ленных выше пакетов и отыщите информацию о других подобных пакетах. Определите их сходство и различия и укажите полезные свойства этих программных средств применительно к их использо- ванию для тестирования программ. 40. Двойное программирование. Если определение результатов контрольных примеров вызывает серьезные затруднения, то мож- но применить подход, при котором две различные группы програм- мируют одну и ту же задачу на разных языках. В связи с тем, что тестирование — процедура довольно дорогостоящая, а процес- сы разработки алгоритма и анализа при рассматриваемом подхо- де являются общими для обеих программ, введение дополнитель- ного программирования обычно не очень сильно влияет на общие затраты. Попробуйте для какой-нибудь задачи применить двойное программирование; более детально с этим методом можно ознако- миться в гл. 4 вышеупомянутой книги Гил ба. 41. Соберите данные о различных типах ошибок программиро- вания. Предварительно изучите две полезные статьи на эту тему1). *> Endres A., An Analysis of Errors and Their Causes in System Prog- rams, IEEE Transactions of Software Engineering (June 1975); Boehm B., Soft- ware and Its Impact: A Quantitative Assessment, Datamation (May 1973).
294 ГЛАВА 5 42. Степень сложности. Степенью сложности программы могут служить следующие ее характеристики: а) число различных логических путей в программе, б) число операторов IF и их процент относительно использо- вания всех операторов, в) средний размер модуля. Эти характеристики можно опеределять -с помощью специаль- ной программы. Напишите такую прорамму. Используя ее на прак- тике, вы можете прийти к выводу, что некоторые программы из- лишне сложны и неудобны для тестирования или эксплуатации и поэтому они должны быть забракованы и переписаны заново. Предложите принцип подобного отбора. 43. В книге Гилба Software Metrics в качестве меры надежно- сти / программного обеспечения предлагается величина, равная у__। Количество прогонов, во время которых' выявились ошибки Общее количество прогонов Оцените достоинства и недостатки этой меры как характери- стики программного обеспечения. 44. Контроль за тестированием. Это мероприятие представляет собой коллективную оценку качества тестирования программного обеспечения. Список данных, необходимых для осуществления та- кого контроля, разработан Ларсоном и приведен в приложении В упомянутой книги Гилба. Установите контроль за тестированием каких-нибудь программ для оценки полноты их проверки. ЛИТЕРАТУРА 1. Boehm В. W., Some Information Processing Implications of Air Force Space Missions in the 1970’s, Astronautics and Aeronautics (January 1971). 2. Boehm B. W., McClean R. K., Urfrig D. B., Some Experience with Automated Aids to the Design of Large-Scale Software, IEEE Transactions on Software Engineering, March 1975. 3. Conway R., Gries D., Program Testing, Primer on Structured Programming using PL/1, Cambridge, Mass., Winthrop Publishers, 1976. 4. Elmendorf W. R., Controlling the Functional Testing of an Operating System, IEEE Transactions on Systems Science and Cybernetics, October 1969. 5. Gruenberger F., Program Testing and Validating, Datamation (July 1968). 6. A Guide to Testing in a Complex System Environment, IBM Corporation, GH20-1628. 7. Hetzel W. C. (ed.), Program Test Methods, Englewood Cliffs, N. J., Prentice- Hall, 1973. 8. Hice G. F., Turner W. S., Cashwell L. F., System Development Methodology, New York. American Elsevier, 1974. 9. Hughes J. K., Michtom J. I., A Structured Approach to Programming, Engle- wood Cliffs, N. J., Prentice-Hall, 1977. 10. Management Planning Guide for a Manual of Data Processing Standards, IBM Corporation, C20-1670. 11. Maynard J., Modular Programming, Philadelphia, Pa., Auerbach Publishers, 1972. 12. Rustin R. (ed.), Debugging Techniques in Large Systems, Englewood Cliffs, N. J., Prentice-Hall, 1971.
Если программу стоит писать, то следует это делать правильно. Ведь если вы не сделали ошибок, то получите пра- вильный результат. Спешишь программировать — отлаживай в часы досуга. Глава 6 101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 6.1. ЧИСЛЕННЫЕ ЗАДАЧИ 1. Автоморфными называются числа, которые содержатся в последних раз- рядах их квадрата, например: 52 = 25 252 = 625 Напишите программу для нахождения нескольких автоморфных чисел. 2. Выпишите все простые числа, находящиеся в интервале между 100 и 300. Простым называется число, которое делится только на единицу и на само себя, 3. Любое целое число может быть единственным образом разложено на простые сомножители. Напишите программу для выполнения такого разложения. 4. Согласно гипотезе Гольдбаха, любое четное число может быть представ- лено в виде суммы двух простых чисел. Проверьте эту гипотезу для первых 500 четных чисел. 5. Напишите программу, которая считывала бы два целых числа /и опреде- ляла бы, являются ли они взаимно простыми. (Два числа называют взаимно простыми, если они не имеют общих делителей). 6. Парные простые числа. Парными простыми числами называют два про- стых числа, разность которых равна двум, например: 3 5 11 13 Напишите программу нахождения 15 таких пар. 7. Числа Мерсенна. Простое число Мерсенна — это число, которое может быть представлено в виде 2^— 1, где р — тоже простое число. Напишите программу для нахождения ряда таких чисел. 8. Числа Пифагора определяются соотношением С2=а2_|_^2> где а, b и с — целые числа. Напишите программу для нахождения пяти чисел Пифагора. 9, Совершенным числом называется целое число, которое равно сумме всех его сомножителей, за исключением самого этого числа. Напишите программу получения трех совершенных чисел. Например: 28=1+2 + 4 + 7+14
296 ГЛАВА 6 10. Напишите программу, которая считывала бы действительное число и печа- тала его в виде мантиссы и порядка, например: 123,42 --> . 12342Е + 03 11. Числа Фибоначчи. Формирование чисел Фибоначчи осуществляется по следующим правилам: Л=1 f2 = i Fi^ = Fi^ + Fi (i>l) Таким образом, /7з=2, /4=3. а) Выдайте на печать первые 15 чисел Фибоначчи. б) Выдайте на печать значения частного от деления каждого последующего числа Фибоначчи на предыдущее. в) Было замечено, что если некоторое число Фибоначчи умножить на 1,618, то можно с определенной точностью получить следующее число Фибоначчи. Про- верьте это предположение и вычислите разность каждой пары чисел Фибоначчи. 12. Напишите программу, которая считывает десятичное число и печатает его двоичный, восьмеричный и шестнадцатеричный эквиваленты. 13. Составьте программу вычисления точной арифметической суммы и разно- сти дйух целых чисел, содержащих 100 разрядов. 14. Разработайте программу, которая считывает два двоичных числа и вы- полняет двоичное сложение. Попробуйте также реализовать и двоичное деление. 15. Напишите программу, которая считывает числа N и М и находит частное N/М с точностью до 25 знаков. 16. Составьте программу, которая считывает два целых числа и находит их наибольший общий делитель (т. е. наибольшее число, на которое делятся оба исходных числа). Произведение двух чисел, деленное на их наибольший общий делитель, есть их наименьшее общее кратное. Сделайте так, чтобы программа определяла и наименьшее общее кратное считываемых чисел. 17. Два числа называют дружественными, если каждое из них равно сумме всех делителей другого, кроме самого этого числа. Например, 220 и 284 есть дружественные числа, так как делителями первого являются числа 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 и НО, сумма которых равна 284, а делителями второго — числа 1, 2, 71 и 142, которые в сумме дают 220. Напишите программу для отыскания некоторых других дружественных чисел. 18. Напишите программу для нахождения всех собственных делителей целого числа N (т. е. всех чисел, меньших N, на которые оно делится). 19. а) Составьте программу отыскания наименьшего целого числа, которое может быть представлено в виде суммы кубов двух чисел двумя различными способами. Например, если кубы чисел I3, 23, З3, ..., то 9=13 + 23, однако это число не может быть представлено никакой другой суммой двух кубов чисел. б) Сделайте предыдущую задачу более общей, а именно напишите про- грамму для нахождения наименьшего целого 2И, такого, что оно может быть представлено суммой n-х степеней двух чисел по крайней мере двумя различными способами. Например: для п=\ М= 2=01+21 = 11 +11, для п=2 М=25=02+52=32+42, для n=3 М= ?=/3+J3=№+£3, для л=4 20. Напишите несколько таких программ, в которых возникало бы арифме- тическое переполнение: а) при выполнении арифметических операций над целыми числами; б) при выполнении арифметических операций над действительными числами Один из возможных способов решения этой задачи — последовательное вы- числение степеней двойки. Еще быстрее можно добиться переполнения, если вы-
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 297 числять факториалы (например, 51 = 5*4*3*2* 1). Напишите также программу, приводящую к арифметической потере значимости при выполнении арифметиче- ских операций над действительными числами. 21. Для целых значений У, /, К, L найдите несколько чисел, чтобы выполня- лось равенство /з- Л> = (К* 2 + £2)2 6.2. ИГРОВЫЕ ЗАДАЧИ 22. Вот список некоторых легко программируемых игр: a) Тик-так-тоу (крестики и е) Кости. нолики)0. ж) Игровые автоматы. 6) Б лэк-джек2). з) Рулетка. в) Ханойская башня3 *). и) Китайская игра в карты фэн-тэн4). r) Угадывание слова путем к) Магические квадраты. постепенного добавления л) Бросание кубика с продвижением по одной букве («балда»). фишек. д) Ним. м) Калах. Можно составить программу игры так, чтобы человек вел ее с машиной или машины играли между собой. Интересно запрограммировать для одной играющей стороны какую-нибудь сложную стратегию, а для другой — совершенно иную стратегию или вообще случайные ходы. Выдавая ходы на печать, часто можно наблюдать при этом довольно интересную игру. Вопросы теории игр с вычисли- тельными машинами обсуждаются в книге Д. Спенсера5). 23. В урне находятся 10 черных шаров и 5 красных. Шары вытаскивают по одному и затем кладут обратно. При этом в урну каждый раз добавляется шар того же цвета, что и вынутый. Напишите программу для моделирования 100 из- влечений шаров из урны. 24. Миссионеры и людоеды. На одной стороне реки находятся три миссионера, три людоеда и одна лодка, которая вмещает не более двух человек. Если в какой-то момент времени на любом берегу число людоедов превысит количество миссионеров, последние будут съедены. Напишите программу, которая определяла бы стратегию перевозки через реку всех миссионеров и людоедов, обеспечиваю- щую сохранение жизни миссионерам. 25. Шахматные задачи. а) Пять ферзей. Пять ферзей можно расположить на шахматной доске та- ким образом, что каждое поле будет находиться под ударом по крайней мере одного ферзя. Напишите программу, определяющую такую расстановку. б) Двенадцать коней. Двенадцать коней могут быть расставлены на шахмат- ной доске так, что каждое поле будет находиться под ударом одного из них. Напишите программу, реализующую подобное размещение коней. в) Восемь слонов. Напишите программу, аналогичную предыдущей, но для случая восьми слонов. 26. Кроссворды. Хорошим считается такой кроссворд, который посвящен опре- деленной теме и имеет минимум неиспользуемого пространства. Напишите про- грамму, которая составляет кроссворд, исходя из заданного списка слов. 27. Любителям шахмат. Напишите программу, реализующую передвижение коня по всем 64 клеткам доски так, чтобы он сходил на каждое поле по одному *’ 3) Подробное описание игр этого класса можно найти в книге Бенерджи Р., Теория решения задач. — М.: Мир, 1972.—Прим, перев. 2’ 4) Описания этих игр в литературе по теории игр не встречаются. — Прим, перев. 1 «5) ^Ре,псег Game Playing with Computers, Hayden Book Company, Ro- chelle Park, N. J., 1976. 7 .
298 ГЛАВА 6 разу, или так, чтобы он прошел максимальное расстояние, не пересекая собствен- ных путей. Эти задачи называют «путешествием коня». 28. Представьте, что вы ректор учебного заведения и должны направить каждому студенту персональное письмо. Вам необходимо знать для этого фами- лию студента, адрес, группу, в которой он обучается, и его специализацию. Напи- шите программу, которая считывает указанные данные о студентах и составляет приветственные письма с поздравлениями по случаю начала нового учебного года. 29. Случайные числа. Один простой способ проверки случайности числовых последовательностей состоит в подсчете частоты появления в них каждой цифры. Если последовательность случайна, то вероятность появления каждой из цифр одинакова и равна 0,1. Напишите программу, считывающую 100 случайных цифр и печатающую относительную долю появления каждой из них в общей последо- вательности. 30. Напишите простую программу, подбирающую брачные пары. 31. Напишите программу, имитирующую 1000 бросаний пары игральных ко- стей и подсчитывающую количество различных выпадающих комбинаций. Срав- ните результаты работы программы с теоретическими частотами. 32. Напишите программу для вычисления шансов получить определенный расклад при игре в покер. 33. Напишите программу, имитирующую раздачу карт при игре в покер. Промоделируйте 1000 раздач и накопите статистические данные по числу случаев, в которых на одной руке оказывались две, три, четыре и т. д. карт одной масти. Сравните полученные машинные результаты с таблицей теоретических частот воз- можных раскладов. 34. Любителям бриджа. Напишите программу, имитирующую раздачу карт при игре в бридж и объявляющую масть. 35. Экологическая игра. В журнале Scientific American за февраль 1971 г. описана следующая интересная модель. Клетка квадрата заполняется на шаге t в случае а) если на шаге t— 1 она была пустой, но были заполнены три соседние клетки или б) если на шаге t—1 она была заполнена и были заполнены две или три со- седние клетки. Во всех остальных случаях клетка остается незаполненной или становится пустой. Каждая клетка имеет восемь соседних, как показано на рисунке: X X X X Y X X X X Здесь клетка У граничит с восемью соседними клетками X, а вся структура пред- ставляет собой уменьшенную модель шахматной доски. Важно при каждом про- смотре наиболее эффективным способом определять пространственное положение занятых и свободных клеток. Напишите программу для моделирования поля раз- мером 15X15 при условии, что в исходной позиции заняты клетки с координатами (3, 8), (4, 7), (5, 7), (5, 8), (5, 9), (10, 7), (10, 8), (10, 9), (11, 7), (12, 8), (3,2),(4, 3), (5, 1), (5, 2), (5, 3), (9, 1), (9,2), (9, 3), (10, 3), (11,2). Промоделируйте 5 шагов. 36. Задача об инфекции стригущего лишая. Промоделируйте процесс рас- пространения инфекции стригущего лишая по участку кожи размером 11X11 кле- ток. Предполагается, что исходной зараженной клеткой кожи является централь- ная. В каждый интервал времени пораженная инфекцией клетка может с веро- ятностью 0,5 заразить любую из соседних здоровых клеток. По прошествии шести единиц времени зараженная клетка становится невосприимчивой к инфекции, .возникший иммунитет действует в течение последующих четырех единиц времени, а затем клетка оказывается здоровой. В ходе моделирования описанного процесса
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 299 обеспечьте выдачу на печать текущего состояния моделируемого участка кожи в каждом интервале времени, отмечая зараженные, невосприимчивые к инфек- ции и здоровые клетки. 6.3. ГРАФИЧЕСКИЕ ЗАДАЧИ 37. Рисунки и графики. С помощью построчно-печатающего устройства вы- полните следующие операции: а) начертите прямоугольник больших размеров; б) начертите треугольник; в) изобразите шахматную доску с клетками размером 25x25 мм; зачерните нужные клетки. г) считайте слово и напечатайте его буквами высотой 50 мм; д) постройте какую-нибудь про’стую кривую; е) по заданному радиусу начертите окружность; ж) используйте запечатку одних символов другими для создания шкалы по- лутонов; затем с помощью этой шкалы попытайтесь напечатать какую- нибудь простую картинку. 38. Для фигуры напишите программу, которая впечатывает в ее клетки цифры 1—8 (по пять цифр сразу) при соблюдении условия, что в смежных клетках не находятся по- следовательные цифры. 39. Шахматистам. Известно, что ферзь может передвигаться по вертикали, по горизонтали и по диагонали на любое желаемое число полей шахматной доски. Напишите программу, которая считывает в качестве входных данных номера вер- тикали и горизонтали, определяющих местоположение ферзя, и отмечает соответ- ствующее поле буквой Q. Поля, на которые ферзь может перемещаться, должны отображаться звездочками *, а все остальные клетки — знаками +. Например, шахматная доска с ферзем на пересечении второй горизонтали и третьей верти- кали должна изображаться машиной следующим образом: + *** + + + + **Q***** + *** + + + + *+*+* + + + + + * + + * + + + + * + + + * + + + * + + + + * + + * + + + + + 6.4. КРИПТОГРАФИЧЕСКИЕ ЗАДАЧИ 40. Допустим, что вы шифров а лыцик-любитель и хотите прочесть какой-либо текст на английском языке, чтобы определить частоту, с которой встречается каждый символ. Напишите программу, которая считывает текст и выдает на пе-
300 ГЛАВА 6 чать вычисленные частоты появления каждой из 26 букв английского алфавита и пробела. В английском языке наиболее распространены буквы ETOANIRSH, указанные здесь в порядке убывания частот их появления. Сравните полученные вами результаты с приведенной здесь последовательностью букв. 41. ЭВМ представляет собой идеальное устройство для кодирования и деко- дирования секретных сообщений. Напишите программу, выполняющую эти дей- ствия с использованием следующих алгоритмов: а) цезаревской подстановки, при которой буква А заменяется буквой С, бук- ва В заменяется буквой D, буква С заменяется буквой Бит. д.; б) транспозиции по типу «частокола», состоящей в том, что выбираются 1, 4, 7-й и т. д. символы и располагаются единой группой; за ними помещаются символы 2, 5, 8-й и т. д.; затем идут символы 3, 6, 9-й и т. д.; в) метода Гронсфельда, основанного на использовании некоторого цифрового ключа и модификации обычной цезаревской системы. Так, например, применяя в качестве ключа число 31206, можно получить шифрограмму слова ПРОГРАМ- МИРОВАНИЕ следующим образом: Ключ 31206 31206 31206 3 Шифрируемый текст ПРОГР АММИР ОВАНИ Е Шифр ТСРГЦ ГНОИЦ СГВНО 3 Для того чтобы зашифровать букву П с использованием цифры 3, отсчитывается третья по порядку после П буква алфавита Т и подставляется вместо буквы П; шифрирование буквы Р с помощью цифры 1 выполняется посредством отсчета одной буквы после Р и подстановки ее вместо Р. Для того чтобы дешифрировать полученный набор символов, достаточно вести отсчет букв в обратном направле- нии. Очевидны преимущества этого метода по сравнению с цезаревской подста- новкой: во-первых, для каждой буквы возможны 10 разных замен (0—9), а не одна и, во-вторых, труднее подобрать ключ для дешифрирования; г) транслитерации, при которой для перемешивания символов сообщения используются прямоугольные матрицы. Например, можно вписать алфавит в прямоугольник, расположив буквы следующим образом: 1 АДЗЛПУЧЫЯ 2БЕИМРФШБ 3 ВЁЙНСХЩЭ 4 ГЖКОТЦЪЮ Шифрирование осуществляется путем последовательной выборки элементов из строк матрицы в определенном порядке. Например, если выбираются элементы строк 2, 4, 3, 1, получается следующее зашифрованное сообщение: БЕИМР ФШЬГЖ КОТЦЪ ЮВЁЙН СХЩЭА ДЗЛПУ ЧЫЯ ) Ключом в данном случае являются размеры прямоугольника и порядок переста- новки строк. Напишите программу для кодирования <и декодирования сообщений этим методом. 6.5. ЗАДАЧИ НА ФОРМИРОВАНИЕ ПОСЛЕДОВАТЕЛЬНОСТЕЙ СИМВОЛОВ 42. Напишите программу, считывающую строку символов и печатающую ее в обратной последовательности. 43. Напишите программу, считывающую последовательности из пяти букв и печатающую все возможные их перестановки. , ,44. Задан список студентов, который отперфорирован на картах по следую- щему образцу: фамилия, имя, отчество.
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 301 Напишите программу, считывающую эти карты и печатающую элементы списка в последовательности: имя, отчество, фамилия. 45. Напишите программу, которая считывает цепочку символов и конкретный заданный символ, а выдает число, характеризующее количество появлений дан- ного символа в цепочке. Модифицируйте затем программу таким образом, чтобы юна решала ту же задачу не для одного символа, а некоторой совокупности сим- волов. 46. Напишите программу, которая считывает цепочку чисел и печатает наи- более длинную, монотонно возрастающую их подпоследовательность. 47. а) Операции с римскими цифрами. Напишите программу сложения чисел, записанных римскими цифрами. Сделайте то же самое для операции умножения. Таблица взаимного соответствия чисел, записанных в римской нотации и деся- тичной системе, приводится ниже. Римская нотация | Десятичная запись I 1 II 2 III 3 IV 4 V 5 VI ' 6 IX 9 X 10 L 50 С 1С0 D 500 М 1000 Ограничьтесь числами, не превышающими МММ (3000). Исходные данные и ре- зультаты должны представляться в римской нотации. б) Действия с кодом Морзе. Напишите программу, которая считывала бы точки и тире, представляющие код Морзе, и переводила бы их в соответствующие буквенные символы. Сделайте то же самое для обратного перевода. 6.6 СТАТИСТИЧЕСКИЕ ЗАДАЧИ 48. а) Напишите программу, которая считывает заданное число А, затем вводит N чисел и печатает среднее арифметическое, стандартное отклонение, мак- симальное и минимальное значения и размах выборки. б) Дополните предыдущую программу так, чтобы она выдавала на печать моду, первый квартиль, второй квартиль (медиану) и третий квартиль. 49. Напишите программу для получения статистических данных о клиентах. Вид исходных перфокарт показан на стр. 302. Обеспечьте выдачу на печать а) процента клиентов в возрасте до 21 года; б) процента клиентов в возрасте 21 года и старше; * 1 - - : -
302 ГЛАВА 6 в) процента мужчин и женщин; г) процента одиноких; д) процента состоящих в браке; е) процента разведенных или проживающих раздельно. Снабдите все результаты какими-либо метками. Поле данных Позиции перфокарты Учетный номер-клиента Возраст (в годах) Пол (0 — женский, 1 — мужской) Семейное положение (0 — одинок, 1 — состоит в браке, 2 — разведен или проживает отдельно от семьи) 1—4 6—7 9 11 50. Группа студентов проходит тестовую проверку, по результатам которой выставляются баллы от 0 до 100. Напишите программу, которая выдавала бы гистограмму распределения числа студентов, получивших оценки от 0 до 10, от 11 до 20 и т. д. 51. Напишите программу, считывающую массив фамилий студентов и полу- ченных ими оценок. Группа студентов состоит из 15 человек; каждый студент имеет пять оценок. Вычислите среднюю оценку по каждому тесту, затем опре- делить среднюю оценку каждого студента по всем тестам. Подсчитайте также средний балл всех студентов с учетом лишь четырех лучших результатов. В за- ключение выдайте на печать фамилии студентов, средняя оценка которых превы- шает общий средний балл, вычисленный по четырем лучшим результатам. 6.7. ЗАДАЧИ НА КОМПИЛИРОВАНИЕ 52. Напишите программу для компилятора, которая считывает с перфокарт арифметические выражения и выявляет несогласованные скобки. 53. Напишите программу для компилятора, которая считываетхарифметиче- ские выражения и определяет /их правильность. Ограничьтесь переменными, со- стоящими только из одной буквы. Для проверки программы используйте три неверных выражения, приводимых ниже: А==В/*С D = E*(A —В F=G+5+ 54. Напишите программу, которая считывает арифметические операторы и затем выполняет их в правильном порядке. Набор операций определяется знаками ( )= - + *[/ Например, если задано выражение А = 5 * 3—4/2 то первой должна выполняться операция умножения, потом деления и в послед- нюю очередь операция вычитания. 6.8. ЗАДАЧИ ПО СОРТИРОВКЕ 55. Напишите программу, которая считывает три числа, расположенных в любой последовательности, упорядочивает их и печатает результат упорядочения.
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 303 После этого модифицируйте программу таким образом, чтобы она считывала константу N, затем вводила N чисел, выполняла их сортировку и выдавала на печать упорядоченную последовательность. 56. Напишите программы сортировки, реализующие следующие методы: а) пузырьковый, заключающийся в том, что вначале берется минимальный элемент массива и помещается по адресу А1, затем выбирается второй минималь- ный элемент и помещается по адресу А2, после этого отыскивается третий мини- мальный элемент и пересылается по адресу АЗ и т. д.; б) слияния, при котором требуются четыре массива А, В, С, D; сортировка по методу слияния начинается с записи всех элементов в массив А; затем каждый элемент а/, начиная с первого, сравнивается с элементами fli+i и осуществляется пересылка ai в массив С до тех пор, пока не окажется, что сц > сч+ъ Как только это произойдет, элемент az+i засылается в массив D и последующие ai записы- ваются в этот же массив до тех пор, пока не окажется, что си > ф+ь после чего снова осуществляется переключение на массив С. Когда массив А будет исчер- пан, в массивах С и D будет находиться ряд отсортированных подпоследователь- ностей элементов. Затем берутся первые по счету подпоследовательности масси- вов С и D, объединяются в соответствующем порядке и записываются в массив А. После этого объединяются вторые по счету подпоследовательности массивов С и D и записываются в массив В. Затем выбираются третьи подпоследовательности элементов массивов С и D и после слияния помещаются в массив А. Описанный процесс поочередного занесения подпоследовательностей в массивы А и В про- должается до тех пор, пока не будут исчерпаны массивы С и D. Как только это произойдет, аналогичным образом объединяются массивы А и В, пока все сорти- руемые элементы не окажутся сосредоточенными в единственном массиве; в) цифровой, состоящий в том, что упорядочение начинается с младших двоичных разрядов сортируемых чисел; далее то же самое проделывается со сле- дующим, более старшим разрядом без нарушения предыдущего порядка. Для упрощения задачи используйте только положительные целые числа; если вы хо- тите усложнить проблему, последнее ограничение можно снять. В программах карточной сортировки применяется именно этот метод; г) какой-либо метод, придуманный вами; д) сравните различные методы сортировки по скорости. Один из возможных способов сравнения заключается в том, чтобы сформировать 1000 случайных чисел, отсортировать их различными методами и определить, какой из них быстрее всего приводит к конечному результату. 6.9. ВЫЧИСЛИТЕЛЬНЫЕ ЗАДАЧИ 57. Напишите программу, которая считывает три числа и определяет: а) не являются ли эти числа длинами сторон треугольника, б) не является ли треугольник равносторонним, в) не является ли треугольник равнобедренным, г) не является ли треугольник правильным. 58. Напишите программу, считывающую четыре числа и определяющую, мо- гут ли они’быть длинами сторон четырехугольника, квадрата, прямоугольника, параллелограмма или ромба. 59. Напишите программу, которая считывает коэффициенты трех линейных уравнений и находит значения трех неизвестных. 60. Знаменитый ученый в области вычислительной техники профессор Неудач- ников занимается испытаниями своей новой машины, пытаясь найти с ее помощью 81* 109 возможных вариантов решения задачи восстановления показанной ниже операции деления, в которой все цифры, кроме одной цифры частного, заменены звездочками:
304 ГЛАВА 6 **8** 1 *** /"*#******“ *** **** *** **** ' **** Каждая звездочка обозначает цифру от 0 до 9, а делимое и делитель не содер- жат нулей. а) Найдите какое-нибудь решение этой задачи. б) . Определите, сколько решений имеет приведенная задача. 61. Напишите программы для вычисления 20 членов следующих рядов: а) л=4(1—1/3+1/5—1/7+ ...) б) е=1 + 1/1 + 1/2! + 1/3! + 1/4!+ ... в) shx=x1/l!+x3/3!+x5/5!+ ... г) е*=1+х/1!+х2/2! + х3/3!+х4/4! + ... д) sin-1x=x+l/2-x3/3+l/2-3/4-x5/5+l/2-3/4-5/6-x7/7+ ... 62. Напишите четыре отдельные подпрограммы, которые выполняли бы арифметические действия с комплексными числами: сложение, вычитание, умно- жение и деление. 63. Напишите программу, которая может производить сложение, вычитание и умножение многочленов. Для хранения коэффициентов каждого многочлена используйте векторную форму записи. 64. Метод Ньютона — Рафсона. Квадратный корень из числа А можно найти с помощью итерационной формулы 1 f А \ *i+i = 2 (Xi + xi ) ’ где А — положительное число, Xi — текущее приближение квадратного корня,, Xr+i — очередное приближенное значение квадратного корня из числа А. Примените эту формулу для нахождения квадратных корней из заданных чисел, предусмотрев прекращение итеративного процесса при у 2—А xl+l “^<0,001. / Сопоставьте полученные результаты с ответами, которые мы имеем при исполь- зовании функции SQRT. 65. Напишите программу, определяющую корни уравнения 2х — 1 — 2sin х = 0 с помощью следующих методов: а) половинного деления, б) итеративного » в) Ньютона — Рафсона. 66. а) Напишите программу вычисления приведенного ниже интеграла по методу Симпсона 2 J In У1 + х dx. о
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 305 б) Напишите программу, вычисляющую приведенный ниже интеграл по ме- тоду трапеций лц Р sina х | - -....==- dx J У 1 + cos2 X о для а=1,0; 1,1; 1,2; ... 2,0. 6. 10. ЗАДАЧИ С МАССИВАМИ 67. Напишите программу, которая считывает массив мощностью 100 элемен- тов. Определите с помощью этой программы, сколько содержится в массиве групп из трех последовательных положительных чисел. Группы из пяти последо- вательных чисел считать группами из трех чисел. 68. Напишите программу, которая считает вектор-строку, состоящую из 30 элементов. Каждый элемент должен быть преобразован по следующему алго- ритму: а) если значение элемента меньше, чем значение индекса, значение элемента возводится в квадрат; б) если значение элемента равно значению индекса, изменяется знак эле- мента; в) если значение элемента больше, чем значение индекса, из значения эле- мента вычитается единица. ’69. Напишите программу, вычисляющую определитель массива. 70. Напишите программу сложения массивов любых мощностей. Составьте также программу перемножения массивов. 6.11. ЗАДАЧИ С ПРОЦЕНТНЫМИ ДОХОДАМИ 71. Определите новое состояние счета, на который внесены 1000 долл, сроком на 10 лет под 5% ежеквартальных с начислением сложных процентов. Проделай- те то же самое при условии ежемесячного и ежедневного начислений процентов. Сравните полученные результаты. 72. Определите, каким должен быть ежегодный вклад, если вы хотите нако- пить 1000 долл, к концу пятого года и банк выплачивает 5% годовых, начисляе- мых один раз в год (проценты сложные). Вклады вносятся равными суммами в начале каждого года. 73. Остров Манхэттен был приобретен поселенцами за 24 долл, в 1926 г. Каково было бы в настоящее время состояние их счета, если бы эти 24 долл, бы- ли помещены тогда в банк под 6% годового дохода (проценты сложные)? 74. Предположите, что вы служащий банка, клиентом которого является миллиардер. Напишите программу ежемесячного начисления сложных процентов (при ставке 6%) по его счету. Примите во внимание то обстоятельство, что если этот вкладчик получит не всю полагающуюся ему сумму, он переведет свой счет в другой банк, а вас уволят. 75. Если некто берет ссуду в размере 500 долл, на условиях выплаты 1,5%- в месяц, какая сумма долга окажется погашенной по истечении года, и сколько он еще останется должен, если платежи вносятся равными долями по 7,5 долл, в месяц? Как изменится положение, если ежемесячно будет вноситься по 10 долл.? 76. Для условий предыдущей задачи определите, сколько месяцев потребует- ся получившему ссуду для погашения долга, если он будет выплачивать по. 10 долл, в месяц? 6Л2. ЗАДАЧИ, СВЯЗАННЫЕ С ДЕЛОВОЙ И БЫТОВОЙ СФЕРАМИ 77. Определите размер подоходного налога служащего при условии умень- шения налога на 750 долл, на каждого иждивенца и стандартного налогового удержания 10%. В качестве входных данных используйте фамилию, регистр а ци- 20—899
306 ГЛАВА 6 •онный номер в системе социального обеспечения, размер ежегодного заработка и количество иждивенцев. 78. Напишите программу вычисления среднего расхода бензина на один кило- метр пробега автомобиля. В качестве входных данных используйте показания спидометра в момент остановки и начала движения и количество литров при- обретенного бензина. 79. Принимая во внимание скудость своих доходов, вы можете составить программу собственных расходов. Проанализируйте с помощью такой программы расходы за три месяца. Очевидные статьи затрат — это питание, квартирная плата, транспорт и культурные нужды; дополните этот список по своему усмотре- нию. а) Определите общие затраты за каждый месяц по каждой статье расходов. б) Найдите среднюю, максимальную и минимальную величины затрат по каждой статье расходов за весь трехмесячный период. 80. Напишите программу ведения банковских счетов на основании чеков. Структура исходных записей такова: Фамилия клиента 20 букв Номер счета 5 цифр Текущее состояние счета 6 цифр Сведения об операциях содержат следующие данные: Номер счета клиента 5 цифр Сумма прихода или расхода 6 цифр Если сообщаемая сумма положительна, это означает вклад, отрицательный знак суммы свидетельствует о выдаче ее по чеку. За каждый выписанный чек с кли- ентов удерживается сбор в размере 15 центов при общей плате за предоставле- ние банковских услуг не более 3 долл. С клиентов, на счету которых имеется не более 300 долл., чековый сбор не взимается. Напишите программу, которая балансирует счета, фиксируя все производимые клиентами операции. Если ока- жется, что какой-либо чек выписан на сумму, превышающую остаток счета, программа должна печатать соответствующее уведомление, удерживать с кли- ента 5 долл, за неправомерную выписку чека и не производить обработку чека. 81. Напишите программу, которая начисляет еженедельную заработную пла- <гу на основании следующих данных, вводимых с перфокарт: Имя и фамилия служащего 20 буквенных позиций, Табельный номер 9 цифровых позиций, Число проработанных часов 4-позиционное поле формата 99.9 Почасовой тариф 5-позиционное поле формата 99.99 Количество иждивенцев 2-позиционное поле формата 99 Рабочее время свыше 40 ч в неделю считается сверхурочным и оплачивается *в полуторном размере. Программа должна производить отчисления на социальное обеспечение в размере 5%, но не более 20 долл, за каждую неделю, а также удерживать налог в размере 2%, взимаемый властями штата, и налог, взимаемый ^федеральным правительством в соответствии со следующей шкалой: Количество ижди венцев Процент налогообложения, % ’ 0 16 1 12 2 9 3 6 4 и более 5
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 307 Предусмотрите выдачу на печать отчета, содержащего всю существенную инфор- мацию. 82. Фирма «Щедрость», обеспечивающая водоснабжением население, устано- вила следующие размеры платы за воду: 1) 0,004 долл, за литр для первых 100 л, 2) 0,003 долл, за каждый очередной литр. Напишите программу, выдающую на печать следующую информацию: Фамилию потребителя Регистрационный номер потребителя Прежнее показание расходомера Новое показание расходомера Потребленное количество воды Размер платы по первому тарифу Размер платы по второму тарифу Общую плату Структура считываемых входных данных такова: Фамилия потребителя 20 букв Регистрационный номер потребителя 4 цифры Прежнее показание расходомера 4 цифры Новое показание расходомера 4 цифры При появлении отрицательного номера потребителя программа должна прекра- щать работу и печатать общие итоги: Суммарное потребленное количество воды Суммарная плата за воду по первому тарифу Суммарная плата за воду по второму тарифу Итоговая плата 83. Некоторая компания выписывает счета своим заказчикам в последний день каждого месяца. Если счет оказывается оплаченным до 10-го числа следую- щего месяца, заказчик получает скидку в размере 1 % от суммы счета, но не более 2 долл. В случае платежа от 10-го до 20-го числа следующего месяца с заказчика взимается полная сумма счета. При оплате после 20-го числа следующего месяца с заказчика удерживаются пени в размере 1% от суммы счета, но не менее 1 долл. Напишите программу, вычисляющую сумму необходимых платежей и выдающую на печать разность между фактически уплаченной суммой и суммой счета. Выходные данные имеют следующую структуру: Номер заказчика Дата выписки счета Дата фактического платежа Сумма по счету Уплаченная сумма 5 цифр 6 цифр (число, месяц, год) 6 цифр (число, месяц, год) ххх.хх ххх.хх 84. Окружное налоговое управление нуждается в услугах по определению размеров налогообложения земельной собственности, зависящих от типа земель- ных участков. Управлением установлена следующая налоговая ставка: Тии земельного участка Налоговая ставка (в долларах на гектар) 1 2,50 2 2,98 3 3,67 4 4,61 5 5,70 20'
308 ГЛАВА 6 Входные данные включают Номер земельного участка Тип участка Длину в метрах Ширину в метрах Вычислите и выдайте на печать суммы налогов по каждому земельному участку, указав его площадь в квадратных метрах и гектарах. Затем напечатайте сведения о суммарном налоге по участкам каждого типа, включающие: Тип земельного участка Количество участков Общую площадь в квадратных метрах Общую площадь в гектарах Общую сумму налогообложения Процент от всей площади, находящейся в пользовании 85. Национальное общество содействия потерпевшим убытки выделяет по- ощрительные стипендии студентам университетов и колледжей, утратившим воз- можность продолжать учебу. Минимальный перечень требований, которым должен удовлетворять претендент на такую стипендию, следующий: а) индекс доходов равен или ниже среднего, б) возраст — свыше 30 лет, в) наличие собственной семьи (семейные имеют в анкетных данных при- знак 1, одинокие—0), г) курс — предпоследний (третий) или старше. , Напишите программу, которая считывает фамилии студентов (20 буквенных символов), перечень установленных требований и выдает на печать фамилии всех тех, кто может претендовать на получение поощрительной стипендии. 86. Торговая фирма выплачивает продавцам комиссионное вознаграждение в размере 5%, если товара продано на сумму менее 1000 дол,, и 6%, если выруч- ка составляет 1000 долл, и выше. Продавцы, проработавшие в фирме более 10 лет, получают комиссионные на 1 % больше, их отличительным признаком служит чет- ный табельный номер. Данные о продавце включают его имя и фамилию, табель- ный номер и общую сумму выручки. Напишите программу начисления комиссион- ного вознаграждения. Выдайте на печать все входные данные и размер комиссион- ных по каждому продавцу, а также общие итоги по сумме выручки и сумме комиссионного вознаграждения. 87. Пара носков стоит 1,05 долл.; при покупке дюжины пар взимается плата в размере 10,25 долл., а при покупке 12 дюжин пар — НО долл. Напишите про- грамму, которая рассчитывает стоимость закупаемых партий носков. (Например, 13 пар должны стоить 10,25 долл.4-1,05 долл.= 11,30 долл.). Владельцы этого товара настроены благожелательно по отношению к покупателям и хотят преду- преждать их обо всех случаях нерациональных объемов закупок, таких, как при- обретение И пар носков, что обходится дороже, чем покупка 12 пар. Составьте программу таким образом, чтобы она печатала предупреждающие сообщения о каждом Случае выбранного объема закупок. 88. Напишите программу обработки данных, связанных с учетом денежных -средств, находящихся в обращении. Состав входных данных: Номер счета 5 цифр Имя и фамилия владельца 20 букв Домашний адрес 20 букв Город и штат 20 букв Почтовый индекс 5 цифр Прежний баланс 5 цифр Платежи 5 цифр Стоимость покупок 5 цифр Если за клиентом обнаруживается долг, то в каждом таком случае с него взимается плата за услуги, равная от предшествующей суммы текущего
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 309 счета, но не менее 50 центов. Программа должна печатать новое состояние сче- тов для рассылки этой информации соответствующим клиентам. 89. Фирма обрабатывающей промышленности покупает оборудование, наме- реваясь- использовать его в течение года для производства товаров широкого потребления. Источником доходов фирмы является реализация произведенных изделий. Определение прибыли от капиталовложений осуществляется путем вы- читания суммы всех затрат из суммы доходов. Деление полученной разности на первоначальную величину капиталовложений дает коэффициент окупаемости затрат. Стоимость оборудования предполагается равной 8000 долл., доходы от продажи изделий составляют 12 000 долл., а производственные затраты, включая стоимость обслуживания агрегата, 10 800 долл. Напишите программу, которая определяет и печатает величину прибыли и коэффициент окупаемости затрат, а также выдает на печать информацию о капиталовложениях, доходах и общих производственных затратах в следующей форме: Стоимость оборудования Доход от реали- зации продукции Производствен- ные затраты Прибыль Коэффициент окупаемости затрат 8000.00 12000.00 10800.00 1200.00 0.15 90. Рассмотрите задачу 89 в расширенной постановке, когда намеченный к покупке агрегат должен служить более одного года. При таком варианте задачи системный аналитик вынужден иметь дело с неопределенностью, характерной для оценок величин будущих объемов реализации и затрат. Из паспортных данных и прошлого опыта использования оборудования из- вестно, что его срок службы равен 10 годам, объем реализации продукции увели- чивается ежегодно в среднем на 5%, а производственные затраты возрастают на 8% в год. Хотя указанные цифры и верны в среднем для длительного периода времени, действия конкурентов, экономические условия и тому подобные фак- торы приводят к тому, что реальные темпы изменения производственных затрат и дохода, получаемого в результате реализации продукции, довольно сильно отклоняются от средних значений. Влияние отмеченных внешних факторов может быть промоделировано с помощью генератора случайных чисел, формирующего случайные отклонения затрат и доходов от средних величин. Несмотря на то что эти отклонения могут оказаться довольно значительными в каждом конкретном случае, в среднем на моделируемом интервале времени они будут равны нулю. Поскольку правомерно предположить, что изменения доходов и затрат взаи- мосвязаны, достаточно формировать лишь одно случайное число для каждого рассматриваемого года. Это число должно затем умножаться на среднюю величи- ну изменения доходов и затрат, в результате чего будет формироваться значение соответствующей переменной для данного года. Генерируемые случайные числа должны лежать в интервале 0,5—1,5. Дополните формат выходных данных задачи 89 порядковым номером года эксплуатации оборудования (введите для этого колонку слева) и печатайте соот- ветствующие данные по годам под первой строкой. Заметьте, что для первого года эксплуатации результат получается тот же самый, что и в задаче 89. 91. Некоторых руководителей не может удовлетворить простой метод оценки окупаемости затрат, изложенный в задаче 90. Существует еще один метод, кото- рый получил признание у практиков, но не у теоретиков экономического анализа, основанный на определении срока окупаемости затрат, т. е. времени, необходимого для компенсации первоначальных капиталовложений. Указанный подход предполагает кумулятивную форму вычисления эффекта, т. е. суммирование ежегодной прибыли (притока наличных денег) до тех пор*, пока накопленная сумма не перекроет полностью первоначальные затраты.
310 ГЛАВА б Например, если сумма первоначальных капиталовложений составляет 31 000 долл, и чистая прибыль по годам равна 10 000, 20 000, 10 000 и 10 000 долл., то приходим к следующему результату: Год . Сумма капиталовложений Годовая прибыль Возмещенные затраты 0 31 000 — 1 — 10 000 10 000 2 — 20 000 30 000 3 — 10 000 31000 4 — 10000 — В данном случае, если считать, что доходы в течение года поступают равно- мерно, анализ данных третьего года показывает, что последняя тысяча долларов, необходимая для полного возмещения первоначальных капиталовложений, посту- пает за 0,1 г. Следовательно, срок окупаемости капиталовложений составляет 2,1 г. Определите срок окупаемости затрат на оборудование для условий задачи 90. Выдаваемый результат должен полностью сохранять информацию этой зада- чи и содержать дополнительную строку с данными о сроке окупаемости. 92. Расширьте постановку задачи 91, включив в нее несколько статей капи- таловложений. Входные данные по каждому объекту капиталовложений были отперфорированы служащими фирмы на перфокартах по следующему образцу: Позиции карты Содержимое 1-2 Номер объекта 3-4 5-14 Число периодов эксплуатации Первоначальные затраты, равные стоимости оборудования 15-24 25-34 3^44 45-54 Объем реализации Производственные затраты Изменение объема реализации Изменение объема затрат Не исключена возможность, что в колоде содержатся карты с ошибками, по- этому ваша программа должна контролировать правильность вводимых данных. Данные считаются неверными, если а) номер объекта отрицательный, равен нулю или больше десяти; б) число периодов эксплуатации оборудования отрицательно, равно нулю или больше 15; в) первоначальные затраты меньше 1000 долл.; г) начальная оценка объема реализации меньше начальной оценки произ- водственных затрат.
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 311 Когда имеет место любая из этих ситуаций, карта, в которой встретилась ошибка, должна игнорироваться и программа должна переходить к чтению сле- дующей перфокарты. Признаком конца колоды информационных карт служит карта, в которой номер объекта капиталовложений равен 99 и число периодов эксплуатации равно 99. Результаты должны выдаваться по следующему образцу: Номер объекта 2 Число периодов эксплуатации 10 Первоначальные денежные затраты 8000.00 Первоначальный доход от реализации продукции 12000.00 Первоначальные производственные затраты 10800.00 Темп изменения объема реализации 0.050 Темп изменения производственных затрат 0.080 Период Доход от реализации продукции Производственные затраты Прибыль Коэффициент окупаемости 1 12000.00 10800.00 1200.00 0.15 2 12127.42 10983.49 1143.93 0.14 3 11995.32 10792.05 1203.26 0.15 4 11538.07 10133.84 1404.22 0.18 5 12457.86 11426.41 1031.45 0.13 6 12536.89 11542.39 994.50 0.12 7 12600.14 11635.56 964.57 0.12 8 13096.13 12368.40 727.73 0.09 9 13919.05 13611.91 307.14 0.04 10 13540.53 13019.64 520.88 0.07 Срок окупаемости 7.08 93. Рассмотрите задачу 92. Замените процедуру расчета значений прибыли в каждый конкретный год на основе оценок затрат и доходов процедурой оценки ожидаемых прибылей и ввода этих данных с перфокарт. Для этого была подго- товлена колода перфокарт, содержащих информацию об объектах капитальных затрат и связанных с ними ожидаемых прибылях. Карта первого типа (единственная) не нуждается в контроле достоверности данных и содержит следующую информацию: Позиции карты Содержимое 1-2 3-4 5-14 Номер объекта Срок службы Первоначальные затраты (про- бивка с десятичной точкой) Карты второго типа (число которых заранее неизвестно) содержат оценки ожидаемых прибылей и перфорируются следующим образом:
312 ГЛАВА 6 Позиции карты Содержимое 1-2 3-12 Год эксплуатации Годовая прибыль (пробивка с десятичной точкой) Эти карты подлежат проверке. Содержащиеся в них данные считаются невер- ными, если а) значение порядкового номера года эксплуатации объекта отрицательно или равно нулю; б) значение порядкового номера года больше, чем срок службы оборудо- вания; в) размер прибыли меньше нуля. Признаком конца колоды информационных карт служит карта с 99-м годом эксплуатации оборудования. Благодаря тому что на картах перфорируется год, нет необходимости в упо- рядочении колоды. Ваша программа должна считывать перфокарты второго типа, проверять достоверность содержащихся в них данных и записывать значения прибыли в одномерный массив последовательно по годам. Поскольку никакое оборудование не может иметь предполагаемый срок использования 99 лет, раз- мер массива ограничивается 99 записями. Если для какого-то года отсутствует соответствующая карта с данными о прибыли, последней должно присваиваться значение, равное нулю. После того как будут рассчитаны все значения прибыли, выдайте на печать только ту часть сформированного массива, которая охватывает предполагаемый срок использования оборудования и срок окупаемости затрат. Результат должен быть подобен следующему: 1 4 3 8 16000.00 Прибыль Коэффициент окупаемости 3600.00 1900.00 8000.00 Период 12 1 7 1000.00 6300.00 1 1000.00 0.06 6 6700.00 2 0.00 0.00 8 5 5200.00 —1400.00 3 1900.00 0.12 5 5400.00 4 3600.00 0.22 99 0.00 5 5400.00 0.34 6 6700.00 0.42 7 6300.00 0.39 8 5200.00 0.32 Срок окупаемости 5.61 94. Местная фирма, снабжающая потребителей газом, выписывает им счета в соответствии со следующим тарифом: Первые 14 м3 1,100 долл. Следующие 85 м3 по 0,130 долл, за каждые 3' м3 Следующие 900 м3 по 0,125 долл, за каждые 3 м3
101 ЗАДАЧА ПО ПРОГРАММИРОВАНИЮ 313 Следующие 2800 м3 по 0,120 долл, за каждые 3 м3 Следующие 4300 м3 по 0,100 долл, за каждые 3 м3 Следующие 11 000 м3 по 0,095 долл, за каждые 3 м3 Следующие 19 000 м3 по 0,087 долл, за каждые 3 м3 Поля входных данных имеют следующую структуру:' Имя и фамилия 16 букв Домашний адрес (название улицы 16 букв и номер дома) Город и штат 16 букв Начальное показание счетчика 7 цифр Конечное показание счетчика 7 цифр Начальная дата 6 цифр Конечная дата 6 цифр Номер счетчика 6 цифр Напишите программу, которая формирует счета для клиентов газовой ком- пании. 6.13. ЗАДАЧИ НА РАЗМЕН ДЕНЕГ 95. Напишите программу, реализующую правило, по которому покупателю всегда выдается сдача наименьшим количеством монет. Например, если вы даете продавцу 1 долл, при покупке товара стоимостью 21 цент, он, руководствуясь этим правилом, должен дать вам сдачу 50-центовой монетой, монетой достоинст- вом в 25 центов и четырьмя 1-центовиками. 96. Напишите программу, которая определяет различные возможные способы размена доллара. Предполагается, что в обращении имеются 1-, 5-, 10-, 25- и 50-центовые монеты. 97. Старая английская система денежных единиц состояла из фунтов, шил- лингов (12 шиллингов = 1 фунту) и пенсов (20 пенсов =1 шиллингу). Напишите программу для автоматического кассового аппарата, которая считает уплаченную сумму, стоимость приобретенного товара и вычисляет необходимую сдачу. 6.14. ЗАДАЧИ О КАЛЕНДАРЕ Года называются високосными (месяц февраль включает 29 дней, а не 28, как обычно), если обозначающие их числа делятся на четыре, за исключением годов, кратных 100. Последние являются високосными лишь в случае их кратно- сти 400. Решите следующие задачи, если 1 января 1800 г. была среда. 98. Напишите программу, которая считывает любую дату (число, месяц, год) и печатает соответствующий ей день недели. 99. Напишите программу, определяющую очередной год, в который 4 июля будет приходиться на пятницу. 100. а) Напишите программу для составления календаря текущего года. б) Напишите программу, которая считывает две даты и вычисляет коли- чество дней, прошедших между ними. в) Напишите программу, которая считывает заданный год и формирует для него календарь. 101. а) Какова вероятность того, что 13-е число определенного месяца будет пятницей? б) Напишите программу, определяющую количество пятниц, приходив- шихся на 13-е числа в XX столетии. в) Почему вероятность того, что 13-е число будет пятницей, выше, чем вероятность того, что это число будет приходиться на любой другой день недели?
Приложение ГРУППОВАЯ РАЗРАБОТКА ПРОЕКТОВ Сложные проекты программирования обычно выполняются на различных стадиях разными людьми. Только разработку небольших проектов ведет один специалист, выполняющий самостоятельно разработку алгоритмов, кодирование и отладку всех частей программы. Поэтому необходимо учиться умению работать вместе с другими людьми и находить с ними общий язык. Это обязательное условие успешного выполнения проекта. Групповая разработка учебного проекта позволяет ощутить реальную обста- новку проектирования систем программного обеспечения. При обучении про- граммированию слишком долго господствовал миф о том, будто программирова- ние является родом индивидуальной деятельности. Даже самый беглый анализ показывает, что большинство проектов есть продукт коллективного труда. В конце каждой из пяти первых глав книги даны специальные задания, пред- усматривающие групповую разработку проектов. В качестве таких заданий могут быть использованы и другие задачи по программированию, приведенные в книге. Каждая группа должна выполнить весь проект от начала до концам исполь- зованием изложенных в данной книге методов^ таких, как проектирование сверху вниз, рациональный выбор алгоритма, структурное программирование, создание групп отладки и тестирования. В зависимости от особенностей конкретных групп разработчиков может оказаться удобной организация бригады главного програм- миста. Здесь уместно также напомнить о необходимости планирования работы группы и четкого формулирования стоящих перед ней задач (см. гл. 2). Часто бы- вает интересно поручить разработку одних и тех же программ или модулей разным лицам и затем сравнить результаты. Можно надеяться, что групповая разработка проекта будет хорошим сред- ством проверки методов работы, описанных в этой книге. Конечно, рассмотренные здесь методы не являются последним словом в искусстве программирования. Поэтому групповое проектирование призвано не только помочь исследованию проблем, изложенных в книге, но также и создать основу для самостоятельного изучения методов читателем, что может привести к появлению даже лучших предложений и методов. Кроме того, групповая разработка проекта создает возможность более широкого вовлечения студентов в процесс создания систем, чем это обычно имеет место при выполнении индивидуальных заданий. Прежде чем приступить к разработке того или иного проекта, произведите стоимостную оценку предполагаемого решения. Иными словами, подумайте, что получит пользователь в результате реализации проекта. Если затраты на раз- работку слишком велики, может быть, вообще не следует начинать работу над проектом. Наоборот, если определенная вами стоимость проекта низка, вашей фирме может оказаться невыгодно тратить усилия на подобную разработку. И в том, и в другом случае вы можете лишиться работы. При групповом проектировании возможно заключение контрактов с другими группами на выполнение каких-то частей большого задания. Полезно при этом вести записи о ходе разработки с тем, чтобы сравнить потом ваши прогнозы и оценки с реальными затратами. Ряд рекомендаций, касающихся оценки стоимо- сти разработки программного обеспечения, можно найти в работе Р. Уолвертона1). Тем, кто связан с групповой разработкой проектов, можно рекомендовать ознакомиться также с работами, приведенными ниже в списке литературы. ЛИТЕРАТУРА 1. Baker F. Т., Mills Н., Chief Programmer Teams, Datamation (December 1973). 2. Brooks F. P., Jr., The Mythical Man-Month, Reading, Mass., Addison-Wesley, 1975. 3. Weinberg G., The Psychology of Computer Programming, New York, Van Nost- rand Reinhold Company, 1971. !) Wolverton R. W., The Cost of Developing Large-Scale Software, IEEE Transactions on Computers (June 1974).
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ Аббревиатура 23, 29 Алгоритм сокращения 23 Анализ результатов тестирования 261 — ошибок обратный 272 ----прямой 271 Безрезультатная работа программы 188 Библиотека поддержки разработки 102— 104 — программ 59, 60 Брандмауэр 209 Бригада главного программиста 96—101 Компилятор, ограничения на использова ние 241, 242 — оптимизирующий 118—120 — отладочный 118, 185 Контроль за тестированием 294 — приемочный 274 — структурный сквозной 252 Критическая область 116 — точка 122, 167 Листинг ассемблера 139, 168 Взрыв 189 Волновой эффект 74 Воронка 189 Воспроизведение значений переменных 218 Выбор данных 50—56 — имен 18—23, 35 ----переменных 18—21 ---- разделов 35 — — файлов 21, 22 Гипотеза Гольдбаха 295 — Симона 111 Глитч 181 Группа тестирования 284—286 Матрица диагональная 169 — разреженная 170 — симметрическая 170 — тетраэдная 170 — треугольная 170 — трехдиагональная 170 Метод Ньютона — Рафсона 304 — сортировки слиянием 303 ---пузырьковый 303 ---цифровой 303 Модуль загрузочный 463, 164 — замещающий 94, 272 — рекомендации по тестированию 273 — фиктивный 94, 272 Модульная прочность 72 — сцепление 72 Монтор тестовый 278 Дефект хронический 121 Документация полная 37 Живучесть программы 244, 245 Журнал программных испытаний 293 Набор правил сокращения 23 Наглядность программ 46 Неверные результаты 189 Нумерация меток 28 — операторов 27 Задача банковский вклад ПО — башня ханойская 172 — игра в палочки 172 ---экологическая 171, 298 — коммивояжер 111 — кубики разноцветные 173 ---сома 173 — лабиринт Минотавра 411 — миссионеры и людоеды 297 — об инфекции стригущего лишая 298 — решето Эратосфена ПО Задачи шахматные 168, 173, 297, 299 Закон Мэрфи 224 Зацикливание 189 Изолирование ошибки 209 Информация отладочная 202—206 Испытания ветвей 246 — граничные 264 — график проведения 281 — полевые 274 — программные 281 — промышленные 275 — системные 274 Оглавление 14, 15 Оператор инициирующий 273 Операторы отладочные 201—205, 207 — условные 14 Описание данных 55, 56 — задачи 48—50, 252 Оптимизация длины оператора 47 — программ 120—125, 141, 142, (144—148 Останов аварийный 187 — преждевременный 189 Ошибки аналитического усечения 270 — арифметические 198, 199 — генерируемые 270 — исчезающие 215 — математических вычислений 270 — общего характера 212—214 — округления 270 — семантические 214 — • синтаксические 183—187, 243 — системные 188, 189, 200» — ситуационные 214 — специального вида 214, 215 — точка обнаружения 202 — точка происхождения 202 Клинч 214 Кодирование сверху вниз 90, 91 — структурное 66, 78—88 Комментарии 11—16, 38 — вводные 12 — пояснительные 14, 15 — расположение 15, 16 Компаратор файлов 278, 291 Пакеты отладки 240 Палиндром 43 Партнер по отладке 207 Переполнение 196, 233 Печать выборочная 204, 205 Подпрограммы обработки ошибок 240 Полнота логических решений 225 Постановка задачи 48—50 Потеря значимости 196, 233 — разрядов 199
316 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ Правило детерминированности 192 Приемы кодирования 47 Признаки закрепленные 210 Примеры нулевые 264 Принцип МЗМП 210 Проверка в исключительных ситуациях 266—267 ---нормальных условиях 263 _ —« экстремальных условиях 264—266 — индексов 218 — реверсивная 279 Проверки автоматические 210, 220 Программа избыточная 210 — мобильная 104 — печатаемая 35—37 — подыгрывающая 94 — самомодифицирующаяся 231 Программа-профилировщик 278, 293 Программирование без ошибок 220—222 — бесхитростное 182 — бригадное 100 — двойное 293 — защитное 207—210 — модульное 66, 70—78 — структурное 16, 65—90, 96, 267 Проектирование модульное 65 — сверху вниз 67—70 Прослеживание ветвей 206 Простота- программ 45—47 Профилирование программ 122, 123 Псевдокоды 91, 92 Псевдоотладка 222, 228 Разрыв коммуникационный 259 Редактирование входных данных 227 Рецензенты 253 Средства отладки 216—226 Стандартизация стиля программирования 10, 1'1 Стопоры ошибок 207 Тест дымовой 296 Тестирование модулей 274 — от простого к сложному 274 — элементов 274 Тестовых данных генератор 276, 277 ---- классы 260 ----типы 258, 259 ----формирование 261—263 Удобочитаемость программ 17, 18, 34, 37— 40, 117, 118, 140 Универсальность программы 57—59 Утверждения 211 Фильтр информационный 210 Форматы входных данных 61 — выходных данных 61, 62 Цели проекта 63 Числа дружественные 296 Число Армстронга 171 — Мерсенна 295, 296 — совершенное 295 — Фибоначчи 296 Чистка циклов 145 Самозамыкание 214 Сжатие циклов 147 Сложность программы 64, 65, 294 --- распределенная 72 — — функциональная 72 Список имен переменных 25, 26, 29, 196 — параметров процедур 25, 26 Эффект побочный 72 Эффективность программ 115—166 Эхо-проверка 197, 226 Язык программирования 56 Ящик Пандоры 178 — черный 250, 284
ОГЛАВЛЕНИЕ Предисловие редактора перевода......................... Предисловие.................................... Глава 1. Стиль программирования................................... 1.1. Стандартизация стиля ...................................... 1.2. Комментарий .................................... 1.3. Пропуск строк . . ................................... 1.4. Пробелы.................................................... 1.5. Идентификация и последовательная нумерация................. 1.6. Выбор имен переменных...................................... 1.7. Имена файлов............................................... 1.8. Стандартные сокращения ................... ................ 1.9. Перенос................................................. 1.10. Размещение операторов..................................... 1.11. Упорядочение списков по алфавиту......................• • 1.12. Скобки.................................................... 1.13. Отступы от начала строки............................... 1.14. Выбор имен разделов....................................... 1.15. Нечитаемые программы.................................. 1.16. Заключение ............................................... 1.17. Советы программисту.......................... . 1.18. Упражнения................................................ Литература...................................................... Глава 2. Проектирование программ......................................45 2.1. Стремление к простоте........................................ 46 2.2. Чтение программ.............................................. 47 2.3. Описание задачи.............................................. 48 2.4. Выбор алгоритма............................................. 50* 2.5. Описание данных.............................................. 55 2.6. Выбор языка программирования............................. . 56 2.7. Универсальность ............................................. 57 2.8. Библиотеки................................................... 59 2.9. Форматы ввода-вывода......................................... 61 2.10. Создание условий для работы оператора...................... 62 2.11. Скромные цели............................................... 62 2.12. Установление целей.......................................... 63 2.13. Сложность . ................................................ 64 2.14. Структурное программирование................................ 65 2.15. Размышления о структурном программировании...................89' 2.16. Кодирование сверху вниз ......................................90 2.17. Бригада главного программиста.................................96 2.18. Библиотека поддержки разработки..............................102 2.19. Использование программ ......................................104 2.20. Документирование........................................ . Ю6 2.21. Переписывание программ ......................................107 2.22. Советы программисту..........................................107 2.23. Упражнения................................................. 108 Литература....................................................... 116 Глава 3. Эффективность программ......................................П5> 3.1. Отношение к эффективности ....................................117 3.2. Эффективность или удобочитаемость?...........................117*
318 ГРУППОВАЯ РАЗРАБОТКА ПРОЕКТОВ 3.3. Оптимизирующие компиляторы......................... 3.4. Оптимизация программ............................. . . . 3.5. Эффективность выполнения программ..................... . 3.6. Память.................................................... 3.7. Вычисление констант...................... ................ 3.8. Инициирование переменных............................ . . 3.9. Арифметические операции......................... 3.10. Обращения к функциям ........................ . . . . 3.11. Оптимизация в процессе компилирования.................... 3.12. Исключение циклов ................................... . 3.13. Организация циклов.................................’• 3.14. Оптимизация циклов ...................................... 3.15. Условные выражения ...................................... 3.16. Логические выражения..................................... 3.17. Индексация............................................... 3.18. Ввод-вывод............................................... 3.19, Изучение новых операторов................................ 3.20, Предупреждающие сообщения................................ 3.21. Загрузочные модули....................................... 3.22. Модули .................................................. 3.23. Использование сведений о машине и компиляторе............ 3.24. Заключение............................................... 3.25. Советы программисту................................ . . 3.26. Упражнения............................................... Литература t .................................................. 118 120 125 127 132 132 133 141- 141 143 143 144 149 150 156 162 162 163 163 165 166 166 166 174 Глава 4. Отладка программ ...........................................175 4.1. Различие между отладкой и тестированием.......................176 4.2. Отладочный барьер ............................................177 4.3. Ошибки в описании задачи......................................178 4.4. Ошибки в выборе алгоритма .....................................179 4.5. Ошибки анализа............................................ . 179 4.6. Ошибки общего характера .......................................180 4.7. Ошибки физического характера...................................181 4.8. Разметка программной колоды.............................. . 182 4.9. Бесхитростное программирование.................................182 4.10. Правильность программ ........................................183 4.11. Синтаксические ошибки....................................... 183 4.12. Виды отладки.................................................187 4.13. Общие рекомендации............................................190 4.14. Неопределенные переменные ....................................192 4.15. План распределения памяти.....................................193 4.16. Таблица перекрестных ссылок...................................193 4.17. Опечатки . 194 4.18. Проверка программы за столом................................ 195 4.19. Описание переменных . 196 4.20. Ошибки ввода-вывода......................................... 197 4.21. «Психология» программных ошибок ..............................198 4.22. Патология чисел...............................................198 4.23. Обнаружение ошибок....................................." . 199 4.24. Защитное программирование ....................................207 4.25. Утверждения . ’........................................211 4.26. Список характерных ошибок.....................................211 4.27. Двумерность программ..........................................215 4.28. Средства отладки . 216 4.29. Отладка в интерактивном режиме ...............................218 4.30. Отладочные модули для проверки программ.......................220 4.31. Автоматические проверки..................................... 220
ГРУППОВАЯ РАЗРАБОТКА ПРОЕКТОВ 319 4.32. Программирование без ошибок..................................220 4^33. Псевдоотладка............................................... 222 434. Время, необходимое для отладки...............................223 4.35. Предотвращение ошибок...................................... 224 4.36. Заключение................................................. 225 4.37. ^Советы программисту ...................................... 226 4.38. Упражнения................................................. 226 Литература....................................................... 242 Глава 5. Тестирование (испытание) программ...........................243 5.1. Небрежность начинающих программистов......................... 244 5.2. Проблема живучести программы . 244 5.3. Общие рекомендации..........................................245 5.4. Необходимая полнота тестирования............................ 246 5.5. Невозможность исчерпывающего тестирования...................248 5.6. Технические требования к тестированию.........................249 5.7. Необходимость раннего тестирования..........................250 5.8. Проверка правильности проектных решений.....................251 5.9. Методы тестирования........................................ 254 5.10. Тестовые данные............................................ 256 5.11. Примеры тестов............................................. 268 5.12. Тестирование программ математических вычислений .... 276 5.13. Модули ....................................................271 5.14. Библиотека программ......................................... 273 5.15. Тестирование файлов .......................................274 5.16. Системные испытания . 274 5.17. Средства тестирования .......................................275 5.18. Контроль результатов с помощью проверяемой программы . . 279* ' 5.19. Окончательное утверждение программы ...................280 5.20. Планирование тестирования программ . 280 5.21. Оценка полноты проверки программы............................282 5.22. Повторное тестирование.................'.....................283 5.23. Группа тестирования........................................ 284 5.24. Заключительные замечания.....................................286 5.25. Советы программисту..........................................286 5.26. Упразднения..................................................287 Литература....................................................... 294 Глава 6. 101 задача по программированию..............................295 6.1. Численные задачи . 295 6.2. Игровые задачи................................................297 6.3. Графические задачи ...........................................299 6.4. Криптографические задачи......................................299 6.5. Задачи на формирование последовательностей символов . . . 300 6.6. Статистические задачи.........................................301 6.7. Задачи на компилирование . 302 6.8. Задачи до сортировке..........................................302 6.9. Вычислительные задачи . . 303 6.10. Задачи с массивами...........................................305 6.11. Задачи с процентными доходами . 305 6.12. Задачц, связанные с деловой и бытовой сферами................305 6.13. Задачи на размен денег..................................... 313 6.14. Задачи о календаре......................................... 313 Приложение. Групповая разработка проектов............................314 Литература....................................................... 315 Предметный указатель............................................ 315
Ван Тассел Д. СТИЛЬ, РАЗРАБОТКА, ЭФФЕКТИВНОСТЬ, ОТЛАДКА И ИСПЫТАНИЕ ПРОГРАММ Научный редактор Л. А. Паршина Младший научный редактор Е. П. Орлова Художник А. В. Шипов Художественный редактор Л. Е. Безрученков Технический редактор Н. Д. Толстякова Корректор В. С. Соколов ИБ № 2245 Сдано в набор 01.08.80 г. Подписано к печати 18.11.80 г. Формат 60X90V16. Бумага типографская № 2. Гарнитура латинская. Печать высокая. Объем 10 бум. л. Усл. печ. л. 20. Уч.-изд. л. 20,30. Изд. № 20/0731. Тираж 20 000 экз. Зак. 899. Цена 1 р. 70 к. ИЗДАТЕЛЬСТВО «МИР». Москва, 1-й Рижский пер., 2. Московская типография № 14 Союзполиграфпрома при Государственном комитете СССР по делам издательств, полиграфии и книжной торговли. Москва, 113105, Нагатинская ул., д. 1.