Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Программирование для начинающих #27


Служба Рассылок Subscribe.Ru

Программирование для начинающих

Выпуск 27

2 NOV 2001

 
 
 
Ведущий рассылки: Вячеслав Мацнев
e-mail: stac@stacmv.net
Здравствуйте, уважаемые подписчики!

В этом выпуске читайте:

ОТСЕБЯТИНА .::. Возвращаясь к DOS...

В прошлом выпуске было опубликовано несколько ссылок на DOS ресурсы.

Я и сам туда сходил, посмотрел "что-чего". Скачал почти все пакеты к FreeDOS, установил их.

Обнаружил ряд интересных программ в этих пакетах: текстовые редакторы, ассемблеры, компилятор и интерпретатор Бейсика, другие.

К этому вопросу еще вернемся.

С сайта FREEDOS удалось выйти еще на ряд интересных сайтов (посмотрите раздел Links! на http://www.freedos.org) с программами для ДОС, с играми. Причем многие программы довольно современные: 1999-2001 годов, это говорит о том, что ДОС жив не только в наших головах, но и в головах множества энтузиастов со всего мира.

Среди последнего моего "улова" я хотел бы выделить программы XTC-PLAY и QuickView. Первая - это музыкальный плеер, поддерживающий различные форматы оцифрованной и модульной музыки (MOD, IT, S3M, XM, ..., WAV, MP3,...). Причем, качество звучания субъективно лучше, чем у того же Winamp'a. Может быть, потому что XTC-PLAY напрямую поддерживает мою не-SB звуковую карту.

QuickView это программа для просмотра картинок нескольких распространенных форматов, просмотра видео (AVI, MOV) и прослушивания цифровых записей (WAV, MP3). Естественно (точнее, удивительно, но для меня очень приятно) имеется прямая поддержка моего GUS PnP.

Кстати насчет видео. Недавно появилась новая версия кодека DivX :-), четвертая. Говорят, что работает быстрее (как кодер, так и декодер). Посмотрел я это дело. Да, немного быстрее. Но я на своем "старом" компе, как смотрел MPEG4-фильмы в маленьком окошке, так и смотрел бы, если бы ... QuickView не поддерживала DivX :-) (3 и 4).

Вот под ДОС все это работает, действительно быстро. Признаюсь, я был несказанно рад такому, прямо сказать, продлению жизни моего PC.

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

DOS .::. BAT-файлы. Окончание.

Константин Даниленко

BAT-файлы

Продолжение. Начало в Выпуске 25.

Законы ВАТ-файлов

Закон Первый. ВАТ-файл – это программа на алгоритмическом языке DOS;

Закон Второй. ВАТ-файл может принести вреда больше, чем пользы;

Закон Третий. ВАТ-файл не умеет больше того, что умеет DOS.

Последней темой станет конструкция цикла. Формат ее таков:

for %%буква in (парам1 парам2 [парам3]...) do команда

Цикл работает следующим образом. Переменной %%буква присваивается значение парам1, после чего выполняется команда. Последняя должна зависеть от переменной %%буква, в противном случае цикл теряет смысл. Затем то же происходит для парам2 и так далее, пока список в скобках не будет исчерпан.
Рассмотрим примеры.
Цикл удаляет из текущей директории файлы с расширениями TMP, OBJ, BAK. Проверка на существование этих файлов не производится:

for %%b in (tmp obj bak) do del *.%%b

Цикл переименовывает файлы 01.ТХТ, 02.ТХТ ... 99.ТХТ и THREE.TXT в 001.ТХТ, 002.ТХТ ... 099.ТХТ и 0THREE.TXT соответственно, иными словами, дописывает символ 0 в начало имени:

for %%a in (*.txt) do ren %%a 0%%a

Цикл копирует пять первых томов, созданных архиватором RAR, из директории C:\TEST в директорию, заданную первым параметром ВАТ-файла:

for %%c in (rar 00 01 02 03) do copy c:\test\*.r%%c %1


Может показаться, что количество параметров ВАТ-файла не должно превышать девяти. В самом деле, ведь они снабжаются именами %1...%9. Однако в действительности ВАТ-файл не может обрабатывать более девяти параметров ОДНОВРЕМЕННО. Согласитесь, это не одно и то же.
Мне просто страшно представить себе ВАТ-файл, которому может потребоваться 10 и более формальных параметров. Мало того, мне самому не приходилось обрабатывать более пяти. Даже среди ВАТ-файлов, написанных другими, мне не встречались такие монстры.
Итак, вообразим невообразимое. Анализ алгоритма показывает, что ВАТ-файл, который вы намереваетесь сваять, потребует 16 (для примера) параметров. Назовем его THIRD.BAT. Прежде всего убедитесь, что одновременно (в пределах одной команды) не потребуется более девяти формальных параметров. Если нет, придется упростить алгоритм, например, путем разбиения этой команды на несколько. Если да, то проблему можно решить. Прежде всего нужно расположить параметры в "порядке надобности". Что это такое?
Вернемся к файлу SECOND.BAT и каждому параметру поставим в соответствие номер строки, где этот параметр упоминается последний раз. Получится вот что:
%0: [4]
%1: [30]
%2: [30]
%3: [24]
При анализе надо учитывать, что благодаря команде goto ВАТ-файл не всегда выполняется строго сверху вниз, поэтому анализ может оказаться весьма нелегким делом, особенно для больших сложных алгоритмов, а наш гипотетический файл THIRD.BAT, судя по всему, именно таков. Анализ файла SECOND.BAT показывает, что он по сути заканчивается на строке [31], а далее идут подпрограммы, информирующие пользователя об ошибке и параметров не содержащие. Поэтому приведенные выше результаты можно считать верными.
Параметры следует расположить в порядке возрастания номеров строк, где параметр упоминается последний раз. То есть, формально говоря, мы должны были поставить размер тома первым параметром (%1). Но ведь если число параметров не превышает девяти, то надобность в таком анализе отпадает и параметры можно располагать согласно здравому смыслу. Мы так и сделали. Вначале нужно найти заданную директорию (%1), а затем создать архив (%2) заданного объема (%3). Если файлы занимают небольшой объем, то %3 вообще не потребуется.
Однако для THIRD.BAT все может оказаться сложнее. Вначале пронумеруйте параметры произвольным образом, составьте алгоритм и сопоставьте параметры с номерами строк последнего упоминания. Осталось лишь расположить параметры по возрастанию этих номеров и затем перенумеровать. Это и будет искомый порядок надобности.
В начале выполнения ВАТ-файла у нас доступны параметры %0...%9, остальные 7 параметров ВАТ-файл пока "не видит". Как только потребуется параметр, следующий после %9, нужно дать команду shift. В переводе это означает "сдвиг", а именно: номер каждого параметра, начиная с %1, уменьшается на единицу, а параметр %0 становится недоступным. То есть после выдачи команды shift мы уже не сможем манипулировать именем THIRD.BAT, но зато увидим параметр, следующий после %9. Представьте себе окно шириной в 10 параметров. Командная строка имеет вид:

ВАТ-файл парам1 парам2 ... парам16

Изначально окно занимает крайнее левое положение. Каждая команда shift сдвигает окно на один параметр вправо. Следовательно, чтобы увидеть парам16 , надо выполнить команду shift... сколько? Правильно, 7 раз. При этом фактическому парам9 будет соответствовать формальный %2, а фактические парам1 ... парам6 окажутся безвозвратно забыты.
Обратим внимание на то, что действие команды shift необратимо, то есть окно умеет двигаться только вправо. Если анализ проведен правильно и, следовательно, параметры располагаются в порядке надобности, то жалеть о сдвиге не придется.


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

  • Если в имени файла имеется символ %, то при обработке в ВАТ-файле его следует указать дважды. Например, имя MY%BANK.TXT надо записывать так: MY%%BANK.TXT. Отсюда следствие: если не хотите лишних проблем, не используйте символ % в именах файлов.
  • Аналогично, воздержитесь от присваивания файлам имен REM, ECHO, CTTY, PAUSE, IF, GOTO, CLS, CALL, FOR, SHIFT.
  • Не следует также начинать имя файла с символа @.
  • Код выхода обнуляется при выполнении любой команды или программы. Поэтому, если необходимо проанализировать код выхода некоторой программы, то ее запуск нужно производить только из ВАТ-файла, а оценку кода выхода проводить сразу после вызова программы.
  • Проводя аналогии с алгоритмическими языками, можно сказать, что ВАТ-файл для DOS - своего рода процедура. В самом деле, процедура есть ни что иное как "пользовательский оператор", вызов которого ничем не отличается от вызова оператора стандартного. И, по аналогии с Первым Законом , можно утверждать: ни одна процедура не делает того, чего не умеет сам язык.
  • Начиная с DOS 6.2, пошаговое (построчное) выполнение ВАТ-файла можно осуществить командой

    command /y /c ВАТ-файл [параметры]

  • Тем, кто еще сомневается в том, что модульное программирование свойственно DOS, будет полезно узнать о нововведении версии DOS 3.30. Команда CALL выполняет функции, аналогичные одноименному оператору алгоритмических языков. То есть один ВАТ-файл вызывает другой командой CALL, после чего выполнение возвращается к вызвавшему файлу и продолжается со строки, следующей после команды CALL:

    call ВАТ-файл [параметры]

    "Безвозвратный" вызов другого файла производится указанием его имени.

  • Диалог ВАТ-файла с пользователем может быть организован с помощью одной из утилит: CHOICE из пакета DOS или BE из пакета Norton Utilities. Обе утилиты действуют одинаково: выдают запрос и в зависимости от ответа возвращают код выхода, который затем обрабатываются известным нам способом. Если у вас есть обе утилиты, рекомендую BE (Batch Enhancer). Кроме возможности диалога, она предоставляет массу дополнительных услуг, и создана специально для расширения возможностей ВАТ-файлов.
  • Тем, кто хочет совместить приятное с полезным, то есть работу в Windows NT с разработкой ВАТ-файлов, рекомендую пакет TakeCommand. Найти его можно на ftp:/ftp.std.com/vendors/jpsoft/tcmd32 . Следует отметить, что многие команды работают не совсем так, как под DOS.
  • Питер Нортон (тот самый) как-то заметил, что однажды ему пришло в голову пересчитать все ВАТ-файлы, которые находятся на его компьютере и которые он сам написал. Их оказалось 145.

Статья Константина Даниленко "BAT-файлы" выложена на сайте:
http://stacmv.boom.ru/lib_articles_batchfiles.html

СТРАТЕГИЯ И ТАКТИКА .::. Защитное программирование

В прошлом выпуске, Константин Даниленко приводил пример bat-файла для архивирования некоего каталога и уделил особое внимание, так называемой, "защите от дурака".

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

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

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

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

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

Скажем, функция SQR(x), предполагает, что x>=0. CHR$(code) предполагает, что (code>=0 AND code<=255).

Если какое-то условие правильной работы функции не выполнено, то и сама функция не может правильно отработать.

Она может отработать не правильно, но это нам не нужно, т.к. это может быть опасно (например, для пользовательских данных, для работы компьютера и т.п.).

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

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

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

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

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

За примерами далеко не надо ходить. Возьмите оператор INPUT a%. Если на его запрос вместо числа введете строку, то будет выдано сообщение типа "Redo from start" и запрос будет повторен (конкретные действия зависят от используемого компилятора).

В вашей программе нет фразы "Redo from start", откуда же она взялась?

Дело в том, что такая реакция на неправильный ввод реализована в операторе INPUT.

Другой пример, если вы с помощью команды OPEN попытаетесь открыть не существующий файл, программа выдаст сообщение, что "File not found." и прекратит работу.

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

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

Ну и последний пример. Если при работе с массивом вы обратитесь к элементу, которого не существует ( DIM a(10): PRINT a(23) ), то опять возникнет ошибка с соответствующим сообщением и традиционным завершением работы программы.

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

Но есть еще и высокий уровень. В нашем случае это уровень Бейсик-программы, нашей программы.

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

Можно оставить это на совести системы и компилятора, как мы делали раньше. Но у такого решения есть недостаток. Который состоит в том, что...

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

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

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

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

Прошу вспоминать об этом при выполнении домашних заданий. Начиная с задания basic/3, реализация механизмов защиты является необходимым условием сдачи задания.

Также прошу уделить внимание вопросам защиты тем, кто будет принимать участие в наших проектах. Работа над проектами будет вестись несколькими программистами. Значит, функции, написанные вами, будут использовать (вызывать) другие программисты. Как именно они будут это делать заранее сказать сложно, поэтому нужно предусмотреть все возможные, а также невозможные и вовсе невероятные ситуации. Чтобы не получилось потом, что из-за того, что ваша функция не обработала ошибку, рушится вся программа.

Кстати, из-за пренебрежения защитным программированием взорвалась не одна ракета, не говорю уже про неисправность на крейсере YorkTown (помните?).

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

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

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

Правильность всех этих данных надо подвергать сомнению, а сами данные дополнительной проверке.

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

Следует обратить внимание на данные, которые передаются на обработку стандартным Бейсиковским функциям и операторам.

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

Что такое "деление на ноль" я уже рассказывал (см. Выпуск 22), а что такое "переполнение" демонстрирует следующий пример. Но сначала, так сказать, о физике явления.

Известно, что переменная типа INTEGER может принимать значения от -32768 до 32767, т.к. она занимает в памяти 16 бит, а 2^16=65536 (поделите пополам, получите 32768).

Однако, если к 30000 (допустимое значение) прибавить 10000 (допустимое значение) получится 40000 (недопустимое значение). Что должно при этом произойти (под переменную отведено 2 байта, а 40000 в это место не помещается) сказать сложно. Да это и не так важно, потому что компилятор Бейсика такие ситуации отлавливает, выдает сообщение о переполнении и, да-да, принудительно завершает программу.

А вот и пример:

REM Префикс % - признак типа INTEGER
DIM a%,b%,c%

INPUT a% INPUT b% c%=a%+b% PRINT c%

Пробуйте эту программу при разных a% и b% и вы поймете страшную вещь: при определенных значениях программа работает правильно, а при других (но тоже определенных) - не правильно.

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

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

Ну а здесь что можно сделать?

Решение довольно простое: использовать переменную с& вместо c% (тип LONG вместо INTEGER), немного изменить алгоритм, а переменные a% и b% оставить как есть.

Переменная типа LONG занимает 4 байта (32 бит) и может вместить любую возможную сумму a%+b%.

Общее правило такое: тип переменной результата должен быть длиннее (в байтах) чем типы операндов.

Если в программе подразумевается, что сумма должна иметь тип INEGER, то переменную c& все равно нужно объявлять как LONG, затем сравнивать ее значение на предмет попадания в допустимый для INTEGER диапазон и обрабатывать ошибку в случае необходимости, а не доводить дело до прекращения работы программы.

Вот пример, как все выше сказанное можно проделать:

DIM a%, b%, c&, sum%
DO
  INPUT a%
  INPUT b%
  c& = a%
  c& = c& + b%
  IF c& < -32768 OR c& > 32767 THEN PRINT "Переполнение."
LOOP WHILE c& < -32768 OR c& > 32767
sum%=c&
PRINT sum%

Почему вместо c&=a%+b% я написал c&=a% : c&=c&+b% ?

Здесь нужно вспомнить (или узнать), как работает оператор присваивания. Сначала вычисляется правая часть выражения, а потом этот результат присваивается переменной в левой части.

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

В исходном примере таким типом является INTEGER, и сумма может в него не поместиться. Переполнение возникнет еще до присвоения c& значения.

В строке c&=c&+b% самым длинным типом будет LONG.

Другим вариантом решения проблемы будет задание типа LONG для переменных a и b и их анализ еще до сложения.

Еще одна проблема, относящаяся к защите заключается в операторе INPUT. Попробуйте запустить второй (исправленный) вариант примера и введите в качестве a% или b% число не допустимое для типа INTEGER, например, 40000.

Посмотрели? А теперь посмотрите Выпуск 22, где рассказывалось, как решать такие проблемы.

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

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

Для начала отказываемся от использования оператора INPUT для ввода с клавиатуры.

Затем пишем собственную функцию, чья работа будет основываться на использовании функции INKEY$. Наша функция может быть любой и она может всячески затруднить ввод пользователем не правильных данных.

Знания и соответствующие навыки для того, чтобы реализовать это, вы уже имеете, если выполнили задачу basic/1/6 ("печатная машинка").

Для примера рассмотрим функцию для ввода телефонных номеров в формате ###-##-##, где "решетка" соответствует цифре от 0 до 9.

Это кажется сложнее, чем просто оператор INPUT, но никто и не говорил, что будет легко :-)

DECLARE FUNCTION InputPhone$ ()
PRINT "Введите телефонный номер:";
phone$ = InputPhone$

FUNCTION InputPhone$ '# вводит с клавиатуры строку формата '# "###-##-##", отображая ее на экране. '# Позволяет редактировать строку во время '# ввода.

DIM CR AS STRING, BS AS STRING, esc AS STRING CR = CHR$(13): BS = CHR$(8): esc = CHR$(27) 'CR - ENTER, BS - BACKSPACE (забой) DIM srcx AS INTEGER, srcy AS INTEGER, y AS INTEGER, i AS INTEGER, n AS INTEGER DIM ch AS STRING, buf AS STRING ' srcx,srcy - координаты первого символа строки на экране ' y - текущая координата курсора в строке ввода. ' n - число символов, которые нужно ввести (7) ' i - число уже введенных символов srcx = CSRLIN: srcy = POS(0) + 1' запомнили текущую позицию курсора. y = srcy i = 0 n = 7 buf = "" ch = ""

PRINT "#"; DO ch = INKEY$ SELECT CASE ch CASE "0" TO "9" IF i < n THEN LOCATE srcx, y: PRINT ch; buf = buf + ch y = y + 1 i = i + 1 IF i = 3 OR i = 5 THEN PRINT "-"; y = y + 1 END IF ELSE BEEP END IF CASE BS IF i > 0 THEN y = y - 1 LOCATE srcx, y: PRINT " "; IF i = 3 OR i = 5 THEN y = y - 1 PRINT " " END IF i = i - 1 buf = LEFT$(buf, i) ELSE BEEP END IF CASE esc i = 0 y = srcy LOCATE srcx, y: PRINT STRING$(n, " ") buf = "" CASE CR IF i = n THEN InputPhone$ = LEFT$(buf, 3) + "-" + MID$(buf, 4, 2) + "-" + MID$(buf, 6) EXIT FUNCTION ELSE BEEP END IF CASE ""

CASE ELSE BEEP END SELECT LOOP WHILE 1

END FUNCTION

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

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

Часто вместо написания специальных функций для ввода, этот ввод упрощают. Цель та же самая - избежать ошибок ввода.

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

Во втором ДЗ есть задача, где вы должны написать программу-тест. Некоторые из вас уже выполнили эту задачу, организовав ввод ответов тем или иным способом.

Вот пример неудачного решения:

 ...
PRINT "В каком году была война 1812 года?"
PRINT "1) 1147"
PRINT "2) 1812"
PRINT "3) 1941"
PRINT "4) 2000"
INPUT answer$
IF answer$="1812" THEN
 ...

Формально в этом фрагменте ошибок нет. Но, друзья, не советую вам относиться к своей работе формально, тем более к своему хобби.

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

Этот фрагмент стоило писать примерно так:

 ...
PRINT "В каком году была война 1812 года?"
PRINT "1. 1147"
PRINT "2. 1812"
PRINT "3. 1941"
PRINT "4. 2000"
DO
  ch$=INKEY$
LOOP UNTIL ch$>="1" AND ch$<="4"
select case ch$
  case "1"
  ...

Вот так. Теперь я бы хотел, чтобы вы вспомнили программу TVStart, которую мы написали в 24-м выпуске.

Рассмотрим ее с позиции защитного программирования.

Сначала посмотрим, что в ней реализовано с этой позиции верно, а затем взглянем на недостатки.

Итак, найдите у себя выпуск 24 и просмотрите быстренько исходный текст программы TVStart.

Найдите место, где мы анализируем командную строку, а точнее параметры, с которыми была запущена наша программа.

 ...
'Берем первый символ командной строки, являющийся цифрой
  day$ = LTRIM$(STR$(VAL(LEFT$(LTRIM$(COMMAND$), 1))))
 ...

Вы знаете, что для того чтобы взять первый символ стоки достаточно использовать LEFT$(COMMAND$,1). Мы, однако, в программе нагородили невесть чего.

Посмотрим как происходит присвоение значения переменной day$, распишем присвоение на нескольких стоках:

day$=LTRIM$(COMMAND$) 'Параметры командной строки могут иметь ведущие
                      'пробелы, которые мы убираем с помощью LTRIM$().
day$=LEFT$(day$,1)    'Берем 1-й символ, он может быть каким угодно,
                      'только не пробелом, пробелы мы только что
                      'убрали.
dayval=VAL(day$)      'Переводим символ в число. Если символ не был
                      'цифрой, то dayval=0.
day$=STR$(dayval)     'Переводим число в строку (строка нам нужна в
                      'итоге).
day$=LTRIM$(day$)     'Избавляемся от ведущего пробела, который всегда
                      'появляется при переведе положительного числа в
                      'строку с помощью STR$.

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

Это стоит делать, если "расписываемый" фрагмент кода важен для понимания сути работы программы человеком(!).

Наш фрагмент выполняет вспомогательные функции, поэтому записан покороче.

Теперь смотрите, как реализован основной цикл программы:

i = 1   'Читать файл будем начиная с первой строки
htmeof = ReadLine(HTM, i, s$) 'Читаем 1-ую строку файла
                              'Если файл пустой, то htmeof
                              'будет равно -1, и программа
                              'закончит свою работу.
DO UNTIL htmeof               'Пока не достигнут конец файла...
  FOR p = 1 TO 2              'Для 2-х строк поиска pattern()...
     'В прочитанной из файла строке ищем строку поиска, вне зависимости
     'от регистра.
    spos = INSTR(LCASE$(s$), LCASE$(pattern$(p)))
    IF spos > 0 THEN   'Если совпадение найдено...
      'Заменяем цифру после "#d" на номер дня day$
      CALL ReplaceChar(s$, spos + LEN(pattern$(p)), day$)
      'Пишем модифицированную строку в файл.
      CALL WriteLine(HTM, i, s$, -1)
    END IF
  NEXT p
  i = i + 1
  htmeof = ReadLine(HTM, i, s$) 'Читаем следующую строку из файла.
LOOP
END

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

Как сказано в комментарии к соответствующей строке, если исходный файл окажется пустым, то программа завершит свою работу. Штатно!

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

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

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

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

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

Кстати, в языках типа Си совсем нет процедур, только функции... .

Теперь смотрите сюда:

spos = INSTR(LCASE$(s$), LCASE$(pattern$(p)))

Мы ищем позицию в строке s$, с которой начинается подстрока pattern$(p). При этом мы защищаемся от того, что строка s$ и подстрока будут в разных регистрах (прописные - строчные) и переводим их в нижний регистр.

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

Например, если вы задаете пользователю вопрос, требующий ответа Y/N, то нужно рассматривать не только ответы "Y" и "N", но и "y", "n". Помните, пользователь обычно не следит постоянно за состоянием CAPS LOCK.

Следующий момент.

ReplaceChar и WriteLine реализованы как процедуры, а не функции. Казалось бы, это противоречит тому, о чем мы говорили чуть выше о ReadLine().

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

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

Более правильно обнаруживать ошибки на низком уровне, а обрабатывать на высоком. Так работает ReadLine.

Однако и второй подход имеет право на жизнь. Рассмотрим его подробнее на примере процедуры ReplaceChar.

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

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

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

SUB ReplaceChar (s$, chpos, ch$)
'# Замещает символ строки s$ в позиции chpos
'# на символ ch$.
'# Если chpos больше длины строки, то строка
'# удлиняется путем добавления в ее конец
'# пробелов.

IF chpos > LEN(s$) THEN s$ = s$ + STRING$(chpos - LEN(s$), " ")

'Вставляем в строку первый символ из строки ch$. Делаем это, потому 'что в общем случае ch$ может содержать произвольное число символов. MID$(s$, chpos) = LEFT$(ch$, 1) END SUB

Сначала мы проверили значение chpos, и если оно больше длины строки, то строка должна быть удлинена. Теперь s$ и chpos соответствуют друг другу.

Затем мы подвергаем сомнению тот факт, что символ ch$ действительно является символом, а не строкой из нескольких символов.

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

Он может сделать это так:

CALL ReplaceChar("дом",1,LEFT$("ком"))

Но если он видел исходный текст процедуры он может написать так:

CALL ReplaceChar("дом",1,"ком")

Второй вариант работает быстрее, т.к. LEFT("ком",1) будет вызвана один раз, а не два, как в первом варианте.

Но! Возникает другая сложность. Тот факт, что процедура ReplaceChar выполняет LEFT$(ch$,1) не описан в документации (к документации в данном случае относятся комментарии, начинающиеся с "'#"). Таким образом во втором варианте гипотетический программист использует недокументированную возможность ReplaceChar.

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

В один прекрасный момент программист, обслуживающий текст ReplaceChar, может решить ускорить работу своей процедуры и переложить ответственность за значение ch$ на вызывающую программу. А он имеет право сделать такое изменение, ведь оно не противоречит документации.

Программы, которые используют недокументированную особенность ReplaceChar станут работать неправильно.

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

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

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

Заметьте, что компилятор не заметит ошибки, т.к. эта ошибка логическая и не отлавливается никакими компиляторами.

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

Но вернемся к защитному программированию.

В программе TVStartесть еще несколько интересных моментов (найдите их сами :). Давайте лучше посмотрим на ее недостатки.

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

Начнем с того, что мы открываем файл "c:\tvstart.htm". При этом мы не проверяем, есть ли такой файл вообще по указанному пути.

Если его там нет, программ будет принудительно завершена.

А как его может там не быть, если мы сами его создали?

Почти сразу же после сдачи выпуска 24 я обратил внимание на то, что программа TVStart не работает на моем компьютере, завершаясь с сообщением об ошибке 74 ("Переименование через диски").

Повторный запуск программы приводил к ошибке открытия файла "c:\tvstart.htm", потому что его не было на месте.

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

ct$=TIME$
tmpfile$ = LEFT$(ct$, 2) + MID$(ct$, 4, 2) + RIGHT$(ct$, 2) + ".tmp"

Имя временного файла представляет из себя нечто вида "165600.tmp". Заметьте, что не указан ни диск, ни каталог, где будет создан временный файл. В соответствии с принятыми в DOS соглашениями файл создается на текущем диске, в текущем каталоге.

Если вспомнить, при каких обстоятельствах запускается программа TVStart, то можно предположить, что текущим диском будет c:.

Я так и предположил. Однако у меня к моменту запуска TVStart текущим диском оказывался h:, а текущим каталогом \work (в этом "виноваты" всякие разные команды, имеющиеся в моем autoexec.bat). Именно там и создавался временный файл.

Теперь взгляните на последние строки WriteLine:

KILL f$             '"Убиваем" исходный файл
NAME tmpfile$ AS f$ 'Переименовываем временный файл.

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

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

Вот такая, вот, пакость!

А исправляется эта ошибка очень просто - нужно создавать временный файл на диске c:.

ct$=TIME$
tmpfile$ = "c:\" + LEFT$(ct$, 2) + MID$(ct$, 4, 2) + RIGHT$(ct$, 2) + ".tmp"

Вот так.

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

ОКРУЖЕНИЕ

Рассылка для серьезных разработчиков:

Рассылки Subscribe.Ru
Создание драйверов устройств для Windows и Linux

Интересные DOS программы (страница ссылок) http://www.opus.co.tt/dave/index.htm


С уважением,
Вячеслав Stac Мацнев mailto:stac@stacmv.net
02.11.01.



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу
Рейтингуется SpyLog

В избранное