
[ Миронченко ] Императивное и объектно-ориентированное програмирование на Turbo Pascal и Delphi
.pdf151
) |
;конец |
if |
) |
;конец |
описания функции |
Обратите внимание: для того, чтобы использовать функцию в императивных языках, надо использовать оператор вызова функции (т.е. базовым понятием является все-таки оператор). А в LISP операции и операторы выглядят как функции (т.е. базовым понятием является функция). В этом и различие между функциональными и императивными языками.
Заметьте, что в LISP нет специального оператора возвращения результата функции. Одним из следствий этого является то, что можно избавиться от оператора присваивания: передавать данные только как аргументы функций, а полученные результаты сразу передавать как параметры другим функциям.
8.15. Сопрограммы
Косвенная рекурсия приводит к еще одному очень важному следствию. Ранее мы говорили о локальных и глобальных переменных и подпрограммах, и отметили тот факт, что понятия «локальный» и «глобальный» относительны. Обыкновенная рекурсия привела к тому, что копии одной и той же функции могут быть вложены одна в другую и одна копия функции может быть глобальной по отношению к другой копии; но по отношению ко всем остальным подпрограммам, которые вызывают рекурсивную функцию, сама она остается локальной, и если считать рекурсивную функцию со всеми ее копиями единым целым, то понятия «локальный» и «глобальный» сохраняют свою суть. С косвенной рекурсией дело обстоит еще интереснее: когда одна подпрограмма вызывает другую, а та, в свою очередь, третью, которая может вызвать снова первую,…, - то считать косвенно-рекурсивную подпрограмму чем-то отдельным мы не можем, более того, - если раньше могло показаться, что понятие «локальный» означает также и «подчиненный», то в случае косвенной рекурсии понятия «главный» и «подчиненный» совсем теряют смысл. Конечно, мы должны помнить, что для каждой копии подпрограммы компилятор создает новые переменные под локальные данные, поэтому копии одной и той же функции владеют разными локальными данными.
Можно пойти еще дальше, и ввести понятие сопрограмм.
•2 подпрограммы назовем сопрограммами, если каждая из них может приостанавливать свое выполнение, и возобновлять выполнение другой
подпрограммы (не копии, а той же самой подпрограммы).
На рис. 8.4 показан пример двух сопрограмм: сначала начинает выполняться первая сопрограмма, затем, выполнив 2 оператора, она вызывает 2-ю сопрограмму, которая выполняет 3 оператора и возобновляет работу первой (а не вызывает новую копию 1-й сопрограммы). Потом 1-я сопрограмма начинает выполнять свои операторы и затем возобновляет работу 2-й сопрограммы и т.д.
Несмотря на то, что в каждый момент времени выполняется лишь одна из сопрограмм, тем не менее с их помощью можно описывать параллельные процессы.
В ТР нет встроенных средств, позволяющих описывать сопрограммы. Но когда мы будем изучать Delphi, мы рассмотрим иной механизм, который позволит моделировать параллельные процессы. О сопрограммах и о языке Simula, который поддерживает их, мы еще вспомним в последней главе этой книги.

152
Сопрограмма1 Сопрограмма2
Выход Выход
Рис 8.4. Работа сопрограмм
Задачи
1.Вычислите периметр и площадь острова Коха.
2.Напишите рекурсивную функцию вычисления n!, основываюсь на следующем определении: 0!=1, n! = n (n −1)!, n > 0 .
3.Вычислите с помощью рекурсии cos(cos(...(cos(x))) , где глубина вложенности
4. |
косинуса = n . |
= Cn |
= 1, Сn |
= Cn−1 |
+ Cn−1 |
Докажите формулы Сn |
|||||
|
0 |
n |
m |
m |
m−1 |
5. |
Придумайте алгоритм вычисления an , где n - натуральное, работающий за время |
c log2 n .
6.Придумайте итерационный алгоритм решения задачи о Ханойских башнях.
7.Подсчитайте для задачи о ханойских башнях количество перемещений дисков, которое необходимо для переноса пирамиды из n дисков.
8.В задаче о Ханойских башнях диски можно перемещать лишь одним способом (можно, конечно, каждый диск перекладывать с места на место по несколько раз, но этот случай мы не рассматриваем). Задача значительно усложняется, если количество стержней больше 3-х. Попытайтесь найти оптимальный алгоритм и количество перемещений дисков, если количество столбцов = 4, а затем обобщите задачу на случай переноса n дисков с помощью m стержней.
9.Можно ли написать рекурсивную подпрограмму, которая в процессе выполнения может вызвать саму себя с теми же входными параметрами, но при этом бесконечная рекурсия все равно может не возникнуть? Обоснуйте ответ.
10.Можно ли написать рекурсивную подпрограмму, которая во время работы обязательно вызовет себя хотя бы один раз с теми же параметрами, но при этом бесконечной последовательности рекурсивных вызовов не будет?
11.Напишите рекурсивную подпрограмму, которая ищет в массиве элемент методом бинарного поиска.
12.Заданное натуральное число необходимо разбить на всевозможные суммы натуральных чисел без повторений, с точностью до перестановки слагаемых. Результаты вывести на экран.
153
13.Написать программу определения количества шестизначных счастливых билетов (билетов, у которых сумма первых трех цифр равна сумме трех последних).
14.Модернизируйте программу о n ферзях так, чтобы она выдавала количество расстановок.
15.Найдите минимальное количество ферзей, которые надо поставить на шахматную доску размера n n , чтобы при этом они держали под боем все поля доски
16.Написать программу обхода шахматной доски конем так, чтобы он на каждом поле побывал по одному разу.
17.Вам попалась карта острова в виде матрицы n m , заполненной нулями и единицами (1-суша, 0 – море). Остров – это совокупность элементов суши, граничащих между собой по горизонтали или вертикали. Надо вывести на экран размер острова.
18.Пусть у вас есть карта целого архипелага, и количество островов в нем. Надо найти размер максимального острова.
19.Пусть у вас есть процедура, которая упорядочивает массив методом быстрой сортировки (в которой выбирается центральный элемент для разбиения массива на 2 части). Напишите программу, которая по числу n строит массив, который быстрая сортировка может упорядочить только за время cn2 . Сравните скорость сортировок пузырьком и быстрой сортировки для таких массивов.
154
Глава 9: Модули и записи
Когда количество строк кода в наших программах превысило 100, нам пришлось разбивать нашу программу на более мелкие части - подпрограммы. Но обычно само количество подпрограмм в проекте больше 100, поэтому мы приходим к той же проблеме, лишь на новом уровне. Но что выясняется: ТР позволяет разбивать программу не только на отдельные подпрограммы, но и на отдельные файлы (которые называются модулями), каждый из которых является целой коллекцией подпрограмм.
9.1.Модули и их структура
•Модуль – это отдельно компилируемая программная единица, содержащая набор типов, подпрограмм, переменных, констант, …, которые могут экспортироваться в основную программу.
Модули подключаются к программе с помощью директивы Uses (кроме модуля
System, который подключается автоматически).
Структура модулей:
Unit имя;
Interface
интерфейсная часть
Implementation
исполняемая часть
BEGIN
инициализирующая часть
END.
Ключевое слово unit означает, что данный файл – модуль. Название модуля должно совпадать с именем файла с исходным кодом. Например, если:
unit Cmplx;
то название файла, в котором написан модуль, должно быть cmplx.pas.
Интерфейсная часть
Начинается со слова interface; содержит объявления всех глобальных типов, констант и переменных, которые должны быть доступны в той программе (или другом модуле), которая подключает данный модуль. При объявлении глобальных подпрограмм в интерфейсной части указывается только их заголовок.
Исполняемая часть
Начинается словом implementation и содержит реализации подпрограмм, объявленных в интерфейсной части. В ней могут объявляться локальные для модуля объекты: вспомогательные типы, константы, переменные.
Когда вы пишете подпрограмму, объявленную в интерфейсной части модуля, вы можете писать только название процедуры или функции, опуская список формальных параметров и тип результата для функции. Но так делать не очень удобно, так как
155
обычно сначала пишется реализация функции, а затем ее заголовок копируется в интерфейсную часть, а не наоборот.
Кроме того, в части implementation можно писать подпрограммы, не объявленные в части interface. В таком случае эти подпрограммы могут быть использованы лишь в других подпрограммах этого модуля, а вне его они не видны. Так рекомендуется делать с теми подпрограммами, которые крайне узкоспециализированы, и вне модуля они не пригодятся.
Инициализирующая часть
Начинается словом Begin. В этой части располагаются исполняемые операторы, содержащие какую-то часть программы. Обычно это некоторые вспомогательные действия: инициализация графического режима, открытие или создание нужных файлов. Все действия инициализирующей части выполняются до передачи управления основной программе.
Инициализирующая часть может отсутствовать. В таком случае слово begin не пишется, и после части implementation следует сразу END.
Компиляция модулей
Модуль не может запускаться на выполнение самостоятельно. Он может участвовать только при построении программы или другого модуля. Для того, чтобы вы могли использовать модуль, надо предварительно откомпилировать его. В результате компиляции создается tpu-файл (Turbo Pascal Unit). Файлы с расширением tpu содержат исходный код модуля в специальном формате, поэтому прочитать их нельзя (точнее: прочитать можно, но понять нельзя). При компиляции основной программы эти файлы преобразуются в машинный язык. Конечно, можно было бы обойтись без промежуточных файлов. Но тогда было бы больше проблем с защитой авторских прав, т.к. если вы пишете не программу, а модуль, то для того, чтобы он мог быть использован в другой программе, вы должны предоставить его исходный код. Если же есть промежуточные .tpu-файлы, то исходный код никто, кроме вас знать не будет. Не менее важно то, что если вы пишете большой проект, то его компиляция занимает много времени, а использование заранее скомпилированных модулей значительно снижает затраты времени.
В ТР определены 3 режима компиляции:
COMPILE – при компиляции модуля или основной программы для всех модулей, которые указываются в директиве uses, должны существовать соответствующие tpu-файлы.
MAKE – компилятор проверяет для каждого объявленного в разделе uses модуля наличие .tpu файлов. Если tpu-файл есть, то для компиляции программы используется он. В противном случае компилятор проверяет, есть ли файл с исходным кодом. Если он есть, то этот файл компилируется в tpu-файл, а затем tpu-файл используется для компиляции программы. Причем если в исходный код модулей вносились изменения, то независимо от того, существует ли уже .tpu файл, или нет, программа откомпилирует модуль заново.
BUILD – существующие tpu-файлы не принимаются во внимание, и программа пытается найти и откомпилировать соответствующие pas-файлы для каждого объявленного в uses модуля.
156
Файл с расширением .tpu при сохранении модуля автоматически не создается: надо, чтобы этот модуль был откомпилирован какой-то программой в любом из приведенных режимов. Программа видит модуль, только если файл с исходным кодом (или соответствующий .tpu файл) находится в директории, которая указана в пункте меню Options -> Directories ->Unit Directories. Если там записана пустая строка, то это означает, что модули должны находиться в том же каталоге, что и turbo.exe.
Итак, чтобы создать модуль вы должны:
1.Создать pas-файл с исходным кодом
2.Подключить его к какой-то программе, откомпилировать его в любом режиме и получить tpu-файл.
Пример 1: Модуль для работы с массивами.
1 : unit Massiv;
2 :
3 : Interface
4 :
5 : procedure LeseMas(var A:array of integer);
6 : procedure SchrMas(const A:array of integer);
7 : function BinSuche(const A:array of integer;left,right:integer; numb:integer):longint;
8 : procedure AuswahlSort(var A:array of integer); {Auswahl -
выбор}
9 : procedure RandFulle(var A:array of integer;k:integer); 10:
11: Implementation 12:
13:procedure LeseMas(var A:array of integer);
14:var
15:i:longint;
16:begin
17:for i:=0 to High(A) do
18:read(A[i]);
19:end;
20:
21:procedure SchrMas(const A:array of integer);
22:var
23:i:longint;
24:begin
25:for i:=0 to High(A) do
26:write(A[i],' ');
27:end;
28:
29:function BinSuche(const A:array of integer;left,right:integer; numb:integer) :longint;
30:var
31:mid:longint;
32:begin
33:mid:=round((left+right)/2);
34:if A[mid]=numb then
35:begin
157
36:BinSuche:=mid;
37:exit;
38:end;
39:if left>=right then
40:begin
41:BinSuche:=0;
42:exit;
43:end;
44:if A[mid]>numb then
45:BinSuche:=BinSuche(A,left,mid-1,numb)
46:else
47:BinSuche:=BinSuche(A,mid+1,right,numb);
48:end;
49:
50:procedure AuswahlSort(var A:array of integer);
51:var
52:i,j,s:longint;
53:min:integer;
54:begin
55:for i:=0 to High(A)-1 do
56:begin
57:min:=A[i];
58: |
s:=i; |
{Индекс минимального элемента} |
59:for j:=i+1 to High(A) do
60:if A[j]<min then {Если нашелся элемент, меньший данного}
61: |
begin |
{присваиваем его переменной min} |
62:min:=A[j];
63: |
s:=j; |
{В s запишем индекс нового мин. элемента} |
64:end;
65: |
A[s]:=A[i]; |
{Меняем A[i] и min местами} |
66:A[i]:=min;
67:end;
68:end;
69:
70:procedure RandFulle(var A:array of integer;k:integer);
71:var
72:i:longint;
73:begin
74:for i:=0 to High(A) do
75:A[i]:=random(k);
76:end;
77:
78:
79: END.
Использование модуля:
1 : uses
2 : Massiv;
3 : const
4 : n=100;
5 : type
6 : Mas = array[1..n] of integer;
158 |
|
|
7 |
: var |
|
8 |
: |
A:Mas; |
9 |
: |
|
10:begin
11:randomize;
12:RandFulle(A,1000);
13:writeln('Исходный массив');
14:SchrMas(A);
15:writeln;
16:AuswahlSort(A);
17:writeln('Отсортированный массив');
18:SchrMas(A);
19:writeln;
20:end.
9.2. Взаимодействие модулей
При разработке программных комплексов с большим количеством модулей могут появляться проблемы согласования их работы. Основной проблемой является зацикливание, т.е. случай, когда модуль А подключается в модуле В, а модуль В - в модуле А. Эта проблема возникает настолько часто, что знать методы ее решения надо обязательно!
Следующий программный код неверен:
Unit A; Interface Uses B;
…
Implementation
…
End.
Unit B; Interface Uses A;
…
Implementation
…
End.
Компилятор ТР подключает модули последовательно, поэтому в этом фрагменте программы прежде, чем подключить модуль А надо подключить модуль В, и наоборот: перед подключением модуля В надо подключить А. Поэтому компилятор так написать не позволит.
Если хотя бы один из двух модулей зависит от другого только в части implementation, то тогда программу можно исправить так:
Unit A;
Interface
159
…
Implementation Uses B;
…
End.
Unit B; Interface Uses A;
…
Implementation
…
End.
Теперь ТР не выдаст ошибки, и программа будет откомпилирована нормально. Если же взаимозависимы части interface, то решить проблему сложнее.
Очень часто отделаться удается малой кровью: просто создать 3-й модуль С, в который поместить все типы данных, подпрограммы и переменные, которые ссылаются друг на друга в модулях А и В. После этого убрать их из обоих модулей, а модуль С подключить и в А, и в В.
К сожалению, это спасает не всегда. Могут существовать проблемы, когда даже внутри одного модуля нельзя описать 2 типа из-за того, что они ссылаются друг на друга. Однако такие случаи появятся, когда мы начнем использовать классы, поэтому их рассмотрение отложим до главы «ООП», где сразу и решим эту проблему.
9.3.Тип Запись
•Запись – объединение нескольких переменных в единое целое.
•Элементы записи называются полями.
Поля записи могут быть любого типа, доступного в ТР (в том числе и другие записи). Одним записям можно присваивать другие записи того же типа.
Синтаксис описания записи: имя типа = record
список полей;
end;
Простейший пример использования записи – работа с рациональными числами. Рациональное число представляется в виде двух целых чисел – числителя и знаменателя. Поэтому запись дробь будет выглядеть так:
type
Bruch= record {Объявление типа Bruch (дробь)} Zahler:longint; { Zähler = числитель} Nenner:longint;{знаменатель}
end;
Переменные созданного нами типа объявляются как обычно, например:
Var
A:Bruch;
160
Доступ к полям записи можно получить с помощью операции ‘.’. Вначале нужно ставить название переменной, затем точку, далее – название поля, к которому надо получить доступ.
В следующих строках числителю дроби А присваивается значение 14, а знаменателю – 15.
A.Zahler:=14;
A.Nenner:=15;
Передавать переменные типа запись в подпрограммы будем так же, как мы это делали с массивами: как параметр-переменную, либо как параметр-константу. По значению передавать записи не желательно, т.к. при этом неэффективно расходуется память.
Вкачестве примера мы напишем модуль, содержащий подпрограммы для работы
сдробями.
Для записей не подходят процедуры write и read, поэтому надо написать собственные подпрограммы ввода и вывода дробей на экран. Кроме того, надо реализовать арифметические и логические (> < = <>) операции над дробями. Все эти подпрограммы должны лежать в части interface, чтобы они были доступны для пользователя модуля.
Кроме того, модуль обязательно должен содержать подпрограммы сокращения дробей, проверки на правильность ввода (не равен ли знаменатель нулю), а также процедуру переноса знака «-» со знаменателя на числитель (см. упражнение №2). Эти подпрограммы не должны быть доступны для пользователя модуля (все эти операции должны проводиться автоматически, например, сокращать дроби надо после всех арифметических действий с дробями).
Пример 2: Модуль для работы с рациональными числами (в модуле Math.pas есть процедура вычисления НОД).
1 |
: unit BruchU; |
|
2 |
: |
|
3 |
: interface |
|
4 |
: |
|
5 |
: uses |
|
6 |
: |
Math; {нам нужна процедура GGT(a,b) для нахождения НОД} |
7 |
: type |
|
8 |
: |
Bruch= record {Объявление типа Bruch (дробь)} |
9 |
: |
Zahler:longint; {числитель} |
10:Nenner:longint;{знаменатель}
11:end;
12:
13:procedure AddBruch(const Rat,Rat2:Bruch;var Rat3:Bruch);
14:{складывает числа Rat и Rat2 и записывает результат в Rat3}
16:procedure MultBruch(const Rat,Rat2:Bruch;var Rat3:Bruch);
17: {умножает числа Rat и Rat2 и записывает результат в Rat3} 18:
19:procedure LeseBruch(var Rat:Bruch);{Ввод дроби}
20:procedure SchrBruch(var Rat:Bruch);{Печать Rat}
22:function Grosser(const Rat1,Rat2:Bruch):boolean;
23:{Возвращает true, если Rat1>Rat2}