При программировании на языке Turbo Pascal практически необходимо вставлять директивы для компилятора в текст программы. В противном случае, при компиляции программы из командной строки (в этом случае используются те значения для ключей компилятора, которые установлены по умолчанию) или из среды программирования, в которой через меню были установлены не те значения для ключей, при которых отлаживалась программа, работа программы может не соответствовать ожиданиям. Напомним, что разместить директивы компилятора (значения ключей компилятора и характеристики оперативной памяти, выделяемой программе при ее выполнении) в тексте программы можно с помощью команды Ctrl+O. Наличие директив компилятора в тексте позволяет легко их редактировать, а компиляция такой программы будет производиться согласно установленным параметрам тех или иных ключей. Приведем значения ключей компилятора, наиболее оптимальные для отладки программы и прокомментируем наиболее важные из рекомендованных установок параметров:
{$A+,B-,D+,E+,F+,G-,I+,L+,N+,O-,R+,S+,T+,Q+,P-,V+,X+}
{$M 65520,0,655360}
Собственно для отладки программы наиболее значимыми являются ключи D+ и L+. Именно благодаря им в исполнимый код программы вставляется отладочная информация и из среды программирования программу можно выполнять “по шагам”, просматривая значения тех или иных переменных на различных стадиях выполнения программы.
Еще одна пара ключей — E+ и N+ необходима, если программа использует вещественные числовые типы данных, арифметические и логические операции над которыми производятся с помощью сопроцессора, а именно: comp, single, double и extended. Без упомянутых ключей программа, использующая эти типы, просто не будет компилироваться. Строго говоря, ключ E+ был необходим лишь для компьютеров, сопроцессор в которых отсутствовал и операции над вещественными числами осуществлялись с помощью так называемого эмулятора — программы, реализующей эти операции только через команды процессора. Однако все процессоры класса Pentium, а именно ими и оснащено большинство современных компьютеров, содержат встроенный сопроцессор, который будет использоваться при выполнении программы, написанной на языке Turbo Pascal, в случае установки ключа компилятора N+. Заметим, что одновременное “включение” и эмулятора и сопроцессора приводит к использованию последнего при наличии сопроцессора и эмулятора при его отсутствии. То есть такая комбинация ключей делает программу “переносимой”, не зависящей от компьютера, на котором она исполняется.
Установка ключа I+ приводит к тому, что при работе программы строго контролируется соответствие поступающих на вход программы констант типам переменных, которым значения этих констант будут присвоены. В случае несоответствия типов, например, при вводе символа вместо числа или вещественного числа вместо целого, выполнение программы будет прервано.
Совершенно незаменима при отладке программы следующая установка ключей компилятора: R+ и Q+ (последний из двух ключей появился лишь в версии Turbo Pascal 7.0). Они позволяют контролировать во время выполнения программы “выход за границу массивов” и “выход за границу допустимого диапазона значений” при операциях над целочисленными переменными. То есть при попытке обращения к несуществующему элементу массива или если во время выполнения операции (арифметической или присваивания) над целыми числами результат, в том числе и промежуточный, не является допустимым для соответствующего типа, то выполнение программы прерывается. При этом ключ R+ отвечает за корректную работу с массивами и присваивание только допустимых значений переменным типа byte и shortint, а Q+ — за корректное выполнение арифметических операций над целыми числами в рамках соответствующих типов1. При отсутствии такого контроля поиск ошибки может быть затруднен тем, что промежуточные вычисления чаще всего производятся в целом типе наибольшего размера (обычно 32-разрядном) и лишь при присваивании полученного значения переменной меньшего размера лишние старшие разряды оказываются отброшенными. Как следствие, отладочная информация о значении арифметического выражения и его результат могут не совпадать.
Рассмотрим это на примере следующей простой программы:
{$Q-}
var a:integer;
begin
a:=1*2*3*4*5*6*7;
writeln('7!=',a);
a:=a*8;
writeln('8!=',a)
end.
Если после получения переменной a своего первого значения, равного 7!, мы посмотрим в отладчике значение выражения a*8, то оно будет равно 40320, а в результате второго присваивания значение a окажется равным –25216.
Наконец, при установленном ключе компилятора S+ в программу вставляется код проверки стека на выполнение. Максимальный размер стека устанавливается директивой компилятора $M, речь о параметрах которой пойдет ниже. Заметим, что прерывание работы программы с диагностикой Stack overflow (переполнение стека) чаще всего означает, что в программе есть подпрограмма, использующая рекурсивные вызовы, работа которой в следствие ошибки завершиться не может.
После того как программа отлажена, то, как уже говорилось в п.9 порядка решения олимпиадных задач (см. лекцию 2), ряд ключей компилятора следует заменить на противоположные, а именно: сдавать программу на тестирование следует с ключами D-,I-,L-,R-,Q-. Объясняется это двумя причинами. Во-первых, при отмене ряда проверок и отсутствии отладочной информации программа будет выполняться быстрее. Во-вторых, если часть ошибок при отладке не устранена, но не является для работы программы фатальной (например, обращение к несуществующему элементу массива может не влиять на правильное формирование реальных его элементов), то программа может вполне успешно пройти процедуру тестирования. Если же проверка корректного обращения с данными в исполняемом коде остается, то скорее всего на большинстве тестов выполнение программы будет прервано досрочно и результат ее работы просто не будет получен.
Рассмотрим теперь на что влияет директива компилятора $M. В обычном режиме конфигурация памяти, отводимой для работы программы, характеризуется тремя числами. Первое число определяет максимальный размер в байтах для стека, который будет использоваться программой. Максимально возможный размер стека равен 65520 байтов, размер стека по умолчанию — 16384 байта, а минимально возможный — 1024 байта. Если в программе используется рекурсия, то скорее всего ей понадобится достаточно большой стек, вплоть до максимально возможного. Но и однократный вызов процедуры или функции требует наличия стека достаточного размера, особенно если в качестве параметра-значения в процедуру или функцию передается массив (по этой причине массивы и сопоставимые с ними по объему занимаемой памяти переменные рекомендуется передавать только по ссылке, в Паскале — с использованием ключевого слова var). Уменьшать размер стека с помощью директивы компилятора имеет смысл только в случае использования динамических переменных, применять которые при решении задач школьных олимпиад по информатике требуется достаточно редко. На размер памяти, отводимой под глобальные статические переменные повлиять практически невозможно, все вместе они не могут занимать более 64 килобайт памяти (например, один массив из 10000 чисел типа real занимает 60000 байт, то есть почти всю допустимую память). Данное ограничение является не естественным для современных компьютеров, следовательно системы программирования, его содержащие, будут вытеснены, как это уже произошло на международной олимпиаде по информатике 2001 года (см. №37/2001). Оставшуюся после размещения глобальных переменных и фиксации размера стека оперативную память можно использовать лишь для создаваемых во время работы программы динамических переменных. Показанные в нашем примере значения второго и третьего параметров в директиве $M как раз и позволяют использовать всю оставшуюся в распоряжении программы память. Ее размер в обычном случае работы DOS-приложения ограничен 640 килобайтами, часть из которых используют другие программы (командный процессор, драйвер русской клавиатуры и т.д.). В условиях олимпиады участникам обычно гарантируется наличие 350-400 килобайт свободной оперативной памяти для работы программы участника (конкретное значение оговаривается заранее) и именно на этот объем и следует ориентироваться при создании динамических переменных. К сожалению, каждая из создаваемых во время работы программы динамических переменных в отдельности не может занимать более все тех же 64 килобайт памяти. Примеры создания и использования динамических переменных будут приведены ниже.
В заключение рассмотрим директивы так называемой условной компиляции, которые иногда удобно применять при отладке олимпиадных задач. В зависимости от того была или нет определена с помощью директивы $define некоторая последовательность символов часть кода программы, ограниченная директивами $ifdef и $endif, может быть как включена, так и исключена из процесса компиляции. Если же два фрагмента программы являются альтернативными, то есть включен в программу должен быть строго один из них, то в дополнение к уже перечисленным можно использовать директиву $else. Рассмотрим это на примере организации ввода данных в программу или из файла или с клавиатуры (например, по условию задачи данные должны вводиться из файла, а при отладке входные параметры удобнее вводить с клавиатуры).
var n:integer; begin {$define debug} {$ifdef debug} assign(input,'con'); {$else} assign(input,'input.txt'); {$endif} reset(input); read(n)
… end.
Так как в приведенном фрагменте программы последовательность debug определена, то ввод данных будет осуществляться с клавиатуры, если же эту команду отменить (закомментировать или слово debug в ней заменить на, например, nodebug), то ввод данных будет производиться из файла input.txt.
Сноски:
1 Подробнее об организации целочисленной компьютерной арифметики и возникающих при этом ошибках можно прочитать в гл. 6 книги Е.Андреева, И.Фалина. Системы счисления и компьютерная арифметика. М: Лаборатория базовых знаний, 2000.
|