Тема 6. Сортировка
-
Основные понятия и определения
Сортировкой называют процесс перегруппировки заданного множества объектов в некотором определенном порядке. Сортировка (sorting) – распределение по сортам, деление на категории. Программисты используют более узкий термин – упорядочение (ordering), однако использование этого слова может привести к путанице из-за перегруженности слова «порядок». Ранжирование (sequencing) не всегда отражает суть дела, особенно если есть равные элементы. Часто используются все три термина с одним и тем же смыслом.
Цель сортировки – облегчение последующего поиска элементов в отсортированном множестве во внутренней или внешней памяти. В последнем случае она особенно важна, если имеется только последовательный доступ к файлам (появляется возможность приемлемой замены прямого доступа). Сортировка и поиск в той или иной мере присутствуют во всех приложениях. При обработке больших объемов данных эффективность именно этих операций определяет эффективность, а иногда и работоспособность всей системы.
Зависимость выбора алгоритма сортировки от структуры данных настолько сильна, что методы сортировки обычно разделяют на две категории: сортировка массивов и сортировка последовательных файлов. Их также называют внутренней и внешней сортировкой.
Постановка задачи. Пусть задана последовательность элементов a1, a2, ..., an. Сортировка означает перестановку элементов в таком порядке ak1, ak2, ..., akn, что при заданной функции упорядочения f справедливы отношения: f(ak1)f(ak2)...f(akn). Функция упорядочения задает отношение порядка. Ее значение f(aki) называется ключом элемента ai и, как правило, содержится в каждом элементе в виде явной компоненты (поля). В простейшем случае ключом является само значение ai.
Для представления элемента сортируемой последовательности хорошо подходит такая структура данных, как запись, одной из компонент которой является ключ элемента, а другими компонентами являются все существенные данные об элементе. Исходную последовательность при этом можно описать как массив из n записей-элементов.
Метод сортировки называется устойчивым, если исходный порядок расположения элементов с равными ключами не изменяется при сортировке.
-
Оценка эффективности алгоритмов сортировки
В первую очередь алгоритмы сортировки оценивают по объему дополнительной памяти ЭВМ, кроме памяти, необходимой для хранения сортируемой последовательности.
Эффективность алгоритмов сортировки целесообразно оценивать по их быстродействию. Удобной мерой эффективности являются число необходимых сравнений ключей (C) и число пересылок или перестановок элементов (P). Эти величины являются функциями от числа n сортируемых элементов. Хорошие алгоритмы сортировки требуют порядка nlog2(n) сравнений, простые – порядка n2 сравнений и эффективны только при малых значениях n.
-
Внутренняя сортировка
Сортировка выполняется или над самими записями, или над указателями вспомогательной таблицы адресов.
Сортировка самих записей.
|
|
|
Первоначальная последовательность |
|
Отсортированная последовательность |
||
|
Запись 1 |
|
4 |
DDD |
|
1 |
AAA |
|
Запись 2 |
|
2 |
BBB |
|
2 |
BBB |
|
Запись 3 |
|
1 |
AAA |
|
3 |
CCC |
|
Запись 4 |
|
5 |
EEE |
|
4 |
DDD |
|
Запись 5 |
|
3 |
CCC |
|
5 |
EEE |
Сортировка таблицы указателей (адресов) используется, если объем данных, помещенных в каждую запись, такой, что перемещение записей связано с большими затратами.
|
Первоначальная таблица указателей |
|
|
Отсортированная таблица указателей |
||||||
|
1 |
|
|
4 |
D |
|
|
1 |
||
|
2 |
|
|
2 |
BBB |
|
|
2 |
||
|
3 |
|
|
1 |
AAA |
|
|
3 |
||
|
4 |
|
|
5 |
E |
|
|
4 |
||
|
5 |
|
|
3 |
CCC |
|
|
5 |
||
Рассмотрим различные методы сортировки последовательностей. При изложении методов сортировки будем для простоты считать, что ключом является само значение элемента и других компонент элемент не содержит. Кроме того, будем предполагать, что сортировка осуществляется по возрастанию значений элементов.
-
Сортировка подсчетом
Простой подсчет. Метод основан на очевидном утверждении: если в отсортированной последовательности элемент находится на i-м месте, то его ключ превышает i-1 ключей предшествующих элементов. Алгоритм сортировки предполагает подсчет числа ключей ki, меньших ключа i-го элемента исходной последовательности, по рекуррентной формуле
ki:=ki+1, если ai>aj, i, j=1, 2, 3, ..., n.
Вычисленные значения ki сохраняются в массиве адресов длиной n. В отсортированной последовательности i-й элемент из исходной последовательности помещается на (ki+1)-е место.
Для расстановки элементов в соответствии с вычисленными адресами необходимо увеличить все элементы массива адресов на 1, чтобы получить адрес каждого элемента в результирующей последовательности, а затем менять местами элементы сортируемого массива с теми элементами, которые стоят на их местах, и параллельно переставлять местами соответствующие элементы массива адресов. В приведенном ниже участке программы Item и Addr – вспомогательные переменные для хранения элемента и его адреса.
for i:=1 to n do Inc(k[i]); // расстановка элементов a[i]
for i:=1 to n-1 do begin // по адресам, хранящимся
while i<>k[i] do begin // в массиве k[i]
Item:=a[i]; a[i]:=a[k[i]]; a[k[i]]:=Item;
Addr:=k[i]; k[i]:=k[Addr]; k[Addr]:=Addr;
end;
end;
Метод простого подсчета применим только для последовательностей с неповторяющимися элементами. Он требует дополнительной памяти для хранения n адресов элементов. В процессе сортировки необходимо выполнить n2 сравнений, 3n пересылок элементов и 3n пересылок адресов при расстановке элементов массива по найденным адресам.
Модифицированный метод подсчета. Если исходная последовательность содержит элементы с одинаковыми ключами, то метод подсчета нужно модифицировать следующим образом. При определении адреса i-го элемента исходной последовательности в результирующей последовательности элемент ai сравнивается со следующими за ним n-i элементами, и значения счетчиков адресов определяются по рекуррентным формулам:
ki:=ki+1, если ai>aj; kj:=kj+1, если ai<aj; i=1, 2, ..., n-1; j=i+1, i+2, ..., n,
т.е. на единицу увеличивается значение счетчика адреса большего из сравниваемых элемента. В случае равенства элементов увеличивается значение счетчика того элемента, для которого сравнения со следующими за ним элементами еще не выполнялись.
Метод требует n элементов дополнительной памяти для хранения адресов элементов, необходимо выполнить С=0,5n(n-1)0,5n2 сравнений, 3n пересылок элементов и 3n пересылок адресов.
-
Сортировка вставками
Простые вставки. Все элементы условно разделяют на упорядоченную последовательность a1, a2, ..., ai-1 и исходную (неупорядоченную) последовательность ai, ai+1, ..., an. На начальном этапе упорядоченная последовательность состоит из одного элемента a1. На каждом шаге сортировки начиная с i=2 из исходной последовательности берут элемент ai и передают его в упорядоченную последовательность, вставляя элемент на подходящее по ключу место:
{$B-}
for i:=2 to n do begin
Item:=a[i];
j:=i;
while (j>=2) and (Item<a[j-1]) do begin
a[j]:=a[j-1];
Dec(j);
end;
a[j]:=Item;
end;
Проиллюстрируем метод простых вставок числовым примером:
|
i=1 |
44 |
55 |
12 |
42 |
94 |
18 |
06 |
67 |
|
│ |
||||||||
|
i=2 |
44 |
55 |
12 |
42 |
94 |
18 |
06 |
67 |
|
┌───────┘ |
||||||||
|
i=3 |
12 |
44 |
55 |
42 |
94 |
18 |
06 |
67 |
|
┌────────┘ |
||||||||
|
i=4 |
12 |
42 |
44 |
55 |
94 |
18 |
06 |
67 |
|
│ |
||||||||
|
i=5 |
12 |
42 |
44 |
55 |
94 |
18 |
06 |
67 |
|
┌─────────────────┘ |
||||||||
|
i=6 |
12 |
18 |
42 |
44 |
55 |
94 |
06 |
67 |
|
┌──────────────────────────┘ |
||||||||
|
i=7 |
06 |
12 |
18 |
42 |
44 |
55 |
94 |
67 |
|
┌────┘ |
||||||||
|
i=8 |
06 |
12 |
18 |
42 |
44 |
55 |
67 |
94 |
Элементы упорядоченной и исходной последовательности разделены двойной вертикальной чертой. Линиями показаны направления вставок элементов. Число шагов алгоритма равно n–1. Определение позиции вставляемого элемента называется просеиванием или погружением, т.к. сравнения ai с элементами упорядоченной последовательности удобно совмещать с пересылками больших элементов вправо.
Метод устойчив, требует в среднем С0,25n2 сравнений и P0,25n2 перестановок.
Поведение сортировки естественное – наибольшие затраты на сортировку необходимы в случае обратной упорядоченности исходной последовательности.
Бинарные вставки. Алгоритм сортировки прямыми вставками можно улучшить, если обратить внимание на то, что последовательность, в которую вставляется новый элемент, уже упорядочена. Позицию вставляемого элемента можно определить путем бинарного поиска: значение вставляемого элемента сравнивается со значением элемента, стоящего в середине упорядоченной последовательности; в зависимости от их отношения для дальнейшего поиска места включения выбирается левая или правая половина упорядоченной последовательности. Процесс деления частей этой последовательности пополам продолжается до тех пор, пока левая и правая границы интервала поиска не поменяются местами. После того как определена позиция j элемента ai исходной последовательности, все элементы с aj до ai сдвигаются на одну позицию вправо, а элемент ai помещается на место элемента aj.
Метод устойчив, требует в среднем Cnlog2n и P0,25n2 перестановок.
Поведение сортировки неестественное – наибольшие затраты на сортировку необходимы в случае полной упорядоченности исходной последовательности.
В приведенном ниже участке программы Left, Right – левая и правая границы поиска; Middle – середина области поиска.
for i:=2 to n do begin
Item:=a[i];
Left:=1;
Right:=i;
while Left<Right do begin
Middle:=(Left+Right) div 2;
if a[Middle]<=Item
then Left:=Middle+1
else Right:=Middle;
end;
for j:=i-1 downto Right do a[j+1]:=a[j];
a[Right]:=Item;
end;
Сортировка двухпутевыми вставками. Этот метод является модификацией сортировки простыми вставками. Резервируется некоторый выходной массив размером n, и первый элемент исходной последовательности помещается в середину резервного массива. Когда в середине резервного массива находится непрерывная группа элементов, место для нового элемента отводится при помощи сдвига всех элементов с меньшими значениями на один шаг влево или элементов с большими значениями на один шаг вправо. Выбор, в какую сторону выполнять сдвиг, зависит от того, какой из них приведет к меньшему количеству сдвигов.
Метод требует n элементов резервной памяти; Cnlog2n; P0,25n2.
Вставки в список. Метод использует связанное представление сортируемых элементов в памяти ЭВМ. Чтобы вставить очередной элемент в готовую последовательность, необходимо, отыскав путем последовательного просмотра подходящее по ключу место, изменить лишь значения указателей вставляемого элемента и элемента, предшествующего ему. При использовании метода вставок в список необходимо C0,25n2 сравнений, P=0 перестановок и V2n изменений связей у элементов списка. Представление сортируемой последовательности в виде связанного списка требует дополнительной памяти для хранения n указателей.
Вставки в дерево. Метод использует представление последовательности сортируемых элементов в виде дерева поиска. С переходом от списковой к древовидной структуре число сравнений сокращается до Cnlog2n (как при бинарных вставках), перемещения элементов исключаются (P=0), число изменений значений указателей элементов уменьшается до Vn. Представление последовательности в виде дерева требует дополнительной памяти для хранения 2n указателей на левые и правые поддеревья.
Сортировка с вычислением адреса. Этот метод является обобщением метода вставок в список. Основная идея метода – распределить элементы сортируемой последовательности по нескольким вспомогательным спискам, каждый из которых содержит ключи из определенного диапазона области изменения ключей. Например, можно распределить все элементы по первому символу ключа-строки или по старшей цифре числового ключа. При этом необходимо сделать предположение о том, что ключи распределены довольно равномерно и не «скапливаются» в каких-то диапазонах. Множество всех возможных значений ключей разбивается на m отрезков, и предполагается, что данный ключ попадает в данный отрезок с вероятностью, близкой к 1/m. Каждый элемент помещается в список на соответствующее его ключу место с помощью метода вставок в список. После распределения всех элементов исходной последовательности из вспомогательных списков формируется отсортированная последовательность.
При использовании этого метода для сортировки числовых ключей необходимо, чтобы все они имели одинаковое количество цифр, что легко достигается добавлением незначащих нулей.
Ниже приведен пример сортировки последовательности 44, 55, 12, 42, 94, 06, 18, 67 по 10 спискам L0 – L9 по значению старшей цифры числа.

Очевидно, что для хранения вспомогательных списков требуется дополнительная память объемом 2n+m элементов (n – для хранения полей значений сортируемых элементов, n – для полей указателей и m – для заголовков списков). Если сортируемая последовательность первоначально хранится в связанной форме, то дополнительная память потребуется только для хранения заголовков вспомогательных списков.
Временные затраты этой сортировки зависят от равномерности распределения элементов исходной последовательности по m группам ключей и величины m, они колеблются от Сn при равномерном распределении ключей по группам и mn до Cn2, если распределение неравномерное или n>>m.
-
Обменная сортировка
Простой обмен. Алгоритм основан на сравнении и перемене мест соседних элементов, если их ключи не упорядочены. Процесс продолжается до тех пор, пока не будут упорядочены все элементы исходной последовательности.
Проиллюстрируем алгоритм числовым примером, расположив элементы вертикально. Проходы массива осуществляются с его конца (снизу).
|
i=1 |
i=2 |
i=3 |
i=4 |
i=5 |
i=6 |
i=7 |
i=8 |
|
44 |
06 |
06 |
06 |
06 |
06 |
06 |
06 |
|
55 |
44 |
12 |
12 |
12 |
12 |
12 |
12 |
|
12 |
55 |
44 |
18 |
18 |
18 |
18 |
18 |
|
42 |
12 |
55 |
44 |
42 |
42 |
42 |
42 |
|
94 |
42 |
18 |
55 |
44 |
44 |
44 |
44 |
|
18 |
94 |
42 |
42 |
55 |
55 |
55 |
55 |
|
06 |
18 |
94 |
67 |
67 |
67 |
67 |
67 |
|
67 |
67 |
67 |
94 |
94 |
94 |
94 |
94 |
На каждом проходе наименьший элемент оставшейся последовательности продвигается к началу массива и исключается из последующих сравнений. В примере каждый такой элемент подчеркнут. Результат i-го прохода показан в (i+1)-м столбце таблицы. Если значение ключа элемента уподобить его весу, то можно сказать, что элемент «всплывает» до уровня, соответствующего его весу. Поэтому метод простого обмена называют также «пузырьковой сортировкой». Реализуется он просто:
for i:=2 to n do
for j:=n downto i do
if a[j-1]>a[j]
then begin
Item:=a[j-1];
a[j-1]:=a[j];
a[j]:=Item;
end;
В среднем число проходов массива равно n-1, на каждом проходе сравнивается n-i пар элементов, где i – номер прохода (i=1, 2, ..., n-1). Характеристики метода: C0,5n2; P0,25n2.
Улучшения алгоритма простого обмена. Из приведенного выше примера видно, что три последние прохода не влияют на порядок элементов из-за того, что они уже отсортированы. Отсюда очевидны следующие улучшения алгоритма простого обмена:
1) запоминание наличия обменов в процессе очередного прохода; если в последнем проходе перестановок не было, то сортировку можно завершать;
2) запоминание не только фактов обмена, но и места (индекса) последнего обмена. Все пары соседних элементов, расположенных выше этого места, уже находятся в нужном порядке, поэтому индекс последнего обмена может служить границей просмотра еще неотсортированной последовательности.
Шейкер-сортировка. Обратимся еще раз к приведенному выше примеру сортировки методом простого обмена. Можно заметить, что «легкий» элемент за один проход «всплывает» на соответствующий его весу уровень, зато «тяжелый» элемент «опускается» только на одну позицию. Если массив просматривать сверху вниз, то скорость передвижения «тяжелых» и «легких» элементов изменится на противоположную. Из этого факта следует еще одно возможное улучшение обменной сортировки: чередование направлений следующих друг за другом просмотров, что приводит к уменьшению требуемого числа просмотров.
Проиллюстрируем алгоритм на том же числовом примере, расположив исходную последовательность по-прежнему вертикально.
|
Left= |
2 |
3 |
3 |
4 |
4 |
|
Right= |
8 |
8 |
7 |
7 |
4 |
|
|
|
|
|
|
|
|
|
44 |
06 |
06 |
06 |
06 |
|
|
55 |
44 |
44 |
12 |
12 |
|
|
12 |
55 |
12 |
44 |
18 |
|
|
42 |
12 |
42 |
18 |
42 |
|
|
94 |
42 |
55 |
42 |
44 |
|
|
18 |
94 |
18 |
55 |
55 |
|
|
06 |
18 |
67 |
67 |
67 |
|
|
67 |
67 |
94 |
94 |
94 |
Результат каждого прохода показан в соответствующем столбце справа, направления просмотров указаны стрелками. Для каждого просмотра устанавливаются левая Left и правая Right границы. Их начальные значения: Left=2, Right=8. На просмотре снизу вверх поочередно сравниваются и обмениваются местами элементы aj-1 и aj для j = Right, Right–1, ..., Left. После просмотра устанавливается новое значение левой границы, равное k+1, где k – индекс последнего обмена. На следующем просмотре – сверху вниз – сравниваются и обмениваются местами элементы aj-1 и aj для j = Left, Left+1, ..., Right, после чего устанавливается новое значение правой границы, равное k–1, где k – индекс последнего обмена. Чередование просмотров осуществляется до тех пор, пока левая и правая границы не встретятся.
В программе Шейкер-сортировки Last – место последнего обмена:
Left:=2; Right:=n; Last:=n;
repeat
for j:=Right downto Left do // просмотр снизу вверх
if a[j-1]>a[j]
then begin
Item:=a[j-1]; a[j-1]:=a[j]; a[j]:=Item;
Last:=j; // запоминание места последнего обмена
end;
Left:=Last+1;
for j:=Left to Right do // просмотр сверху вниз
if a[j-1]>a[j] then begin // обмен
Item:=a[j-1]; a[j-1]:=a[j]; a[j]:=Item;
Last:=j; // запоминание места последнего обмена
end;
Right:=Last-1;
until Left>Right;
Анализ улучшенных алгоритмов обменной сортировки показывает, что введенные улучшения не дают существенного выигрыша по сравнению с алгоритмом простого обмена: C<0,5n2; P0,25n2. Число перемещений не изменяется, число сравнений немного сокращается, но по-прежнему остается пропорциональным n2. Шейкер-сортировку целесообразно использовать в случаях, когда известно, что исходная последовательность почти упорядочена.
Метод четных и нечетных транспозиций. Сортировка выполняется следующим образом. На очередном шаге сравниваются и, если необходимо, обмениваются местами ai и ai+1 для всех нечетных i. На следующем шаге эти действия выполняются для всех четных i. Эти два шага повторяются до тех пор, пока массив не будет отсортирован.
Обменная сортировка с разделением (быстрая сортировка). Разработана Хоаром в 1962 г. Результаты оказались настолько хорошими, что на настоящее время это наиболее быстрая сортировка. Метод заключается в следующем. Случайным образом выбирается какой-либо элемент x массива, массив просматривается слева направо, пока не будет найден a[i]>x, затем массив просматривается справа налево, пока не будет найден a[j]<x. Эти элементы меняются местами, и процесс просмотра с обменом продолжается, пока два просмотра не встретятся где-то в середине массива. В результате массив разделится на две части: левую (с ключами, меньшими x) и правую (с ключами, большими x). Сортировка заключается в проведении процедуры разделения сначала для всего массива, затем каждой части, затем частей каждой части и т.д., пока каждая часть не будет содержать один элемент.
Процедура сортировки может быть реализована рекурсивно. В приведенной ниже программе Left, Right – левая и правая границы просмотра, TIndex, TItem – типы индекса и элемента массива.
procedure QuickSort(Left, Right: TIndex);
var
i, j : TIndex;
x, w : TItem;
begin
i:=Left; j:=Right;
x:=a[(Left+Right) div 2];
repeat
while a[i]<x do Inc(i);
while a[j]>x do Dec(j);
if i<=j
then begin
w:=a[i]; a[i]:=a[j]; a[j]:=w;
Inc(i); Dec(j);
end;
until i>j;
if Left<j then QuickSort(Left, j);
if Right>i then QuickSort(i, Right);
end;
В процессе одного разделения проводится n сравнений и n/6 обменов (зависит от выбора x). Наилучшие результаты будут получены, если в качестве x будет выбрана медиана последовательности (элемент, значение которого n/2 элементов и n/2 элементов). В этом случае каждое разделение разбивает массив на две равные части, и число проходов сортировки равно log2(n), Cnlog2(n), Pnlog2(n)/6. Если граница выбирается случайным образом, то эффективность ухудшается примерно в 2ln2 раза. Метод неустойчив.
К недостаткам алгоритма быстрой сортировки можно отнести то, что он неэффективен при малых n (как и все усовершенствованные методы), и в наихудшем случае (неудачный выбор x, например, наибольшее значение в массиве и подмассивах) сортировка становится медленной: Cn2.
-
Сортировка выбором
Сортировка с помощью простого выбора. Метод основан на следующем. Сначала методом последовательного поиска отыскивается элемент с наименьшим ключом, затем он меняется местами с первым элементом и исключается из дальнейшего рассмотрения. Из оставшихся n–1 (от 2-го до n-го) элементов вновь находится наименьший, меняется местами со вторым элементом и также исключается из исходной последовательности. Этот процесс повторяется с оставшимися n–2, n–3, ... элементами до тех пор, пока не останется единственный элемент.
В программе сортировки выбором k – адрес минимального элемента:
for i:=1 to n-1 do begin
k:=i; Item:=a[i];
for j:=i+1 to n do
if a[j]<Item
then begin
k:=j;
Item:=a[j];
end;
a[k]:=a[i]; a[i]:=Item;
end; // for
Число сравнений не зависит от начального порядка ключей и равно C0,5n2, число перестановок Pnln(n), откуда следует, что алгоритм с простым выбором предпочтительнее простого обмена и простых вставок.
Выбор из дерева или метод турнира с выбыванием. Для улучшения сортировки выбором желательно от каждого прохода получать больше информации, нежели просто указание на наименьший элемент.
Например, с помощью n/2 сравнений можно определить наименьший ключ из каждой пары, при помощи следующих n/4 сравнений – выбрать наименьший из каждой пары наименьших и т.д. Таким образом, на первом шаге с помощью n–1 сравнений можно построить дерево выбора, и определить корень как наименьший ключ:
┌──────────06──────────┐
┌────12────┐ ┌────06────┐
┌─44─┐ ┌─12─┐ ┌─18─┐ ┌─06─┐
44 55 12 42 94 18 06 67
На втором шаге спускаемся по пути с наименьшим ключом и исключаем его, заменяя элементом противоположного листа либо значением , если исключаемый элемент – лист.
┌──────────12──────────┐
┌────12────┐ ┌────18────┐
┌─44─┐ ┌─12─┐ ┌─18─┐ ┌─67─┐
44 55 12 42 94 18 67
Элемент, находящийся в корне дерева, имеет наименьший ключ среди оставшихся элементов и может быть исключен. После n шагов дерево состоит из символов , и процесс сортировки закончен.
На каждый шаг требуется log2n сравнений, т.е. C=nlog2n, дополнительно нужно n шагов на построение дерева. Ускорение достигнуто за счет усложнения структуры хранения информации – дерево требует 2n–1 единиц памяти при его реализации на базе массива. Дыры () постепенно заполняют все дерево и приводят к большому числу ненужных сравнений.
Экономии памяти можно добиться, если в узлах дерева хранить не сами элементы с ключами, а указатели этих ключей. Тогда объем необходимой памяти составит n исходных элементов и n–1 указателей.
Пирамидальная сортировка. Разработана Дж. Уильямсом и позволяет хранить дерево из n элементов исходной последовательности в n единицах памяти в виде пирамиды, которая определяется как последовательность ключей hl, hl+1, ..., hr такая, что hih2i, hih2i+1 для каждого i=l, ..., r/2.
Например, массив h из 15 элементов, расположенный в виде дерева, имеет вид:
┌───────h1───────┐
┌───h2───┐ ┌───h3───┐
┌─h4─┐ ┌─h5─┐ ┌─h6─┐ ┌─h7─┐
h8 h9 h10 h11 h12 h13 h14 h15
Элемент h1 является ее наименьшим элементом: h1=min(h1, ..., hn).
Предположим, что дана пирамида с элементами hl+1, ..., hr и нужно добавить новый элемент x для того, чтобы сформировать расширенную пирамиду hl, hl+1, ..., hr.
Пример. Возьмем исходную пирамиду (см. ниже) и расширим ее, добавив элемент h1=44. Новый элемент сначала помещается в вершину дерева, а затем «просеивается» по пути, на котором находятся меньшие, чем он, элементы, а они, в свою очередь, поднимаются вверх:
|
Исходная пирамида |
┌───h1───┐ ┌─42─┐ ┌─06─┐ 55 94 18 12 |
Результат просеивания ключа 44 |
┌───06───┐ ┌─42─┐ ┌─12─┐ 55 94 18 44 |
На каждом шаге элемент h1 обменивается с элементом, меньшим из смежной с ним пары элементов.
Процедура просеивания (Left, Right – границы исходной пирамиды):
procedure Sift(Left, Right: TIndex);
var
i, j: TIndex;
x: TItem;
begin
i:=Left; j:=2*Left;
x:=a[Left];
while j<=Right do begin
// выбор наименьшего из смежных с x:
if j<Right
then
if a[j]>a[j+1] then Inc(j);
if x<=a[j] then Break;
a[i]:=a[j];
i:=j; j:=2*i;
end;
a[i]:=x;
end; // procedure Sift
Для построения пирамиды Р. Флойд предложил следующий способ. Дан массив h1, ..., hn, в котором элементы hn/2+1, ..., hn уже образуют пирамиду, так как нет индексов j=2i (или j=2i+1). Эти элементы составляют листья двоичного дерева. Далее необходимо расширить пирамиду влево: добавить на каждом шаге один элемент и с помощью просеивания поместить его на соответствующее место:
Left:=(n div 2) +1;
while Left>1 do begin
Dec(left);
Sift(Left, n);
end;
Ниже проиллюстрирован процесс построения пирамиды (│ – граница пирамиды):
|
44 |
55 |
12 |
42 |
94 |
18 |
06 |
67 |
|
44 |
55 |
12 |
42 |
94 |
18 |
06 |
67 |
|
44 |
55 |
06 |
42 |
94 |
18 |
12 |
67 |
|
44 |
42 |
06 |
55 |
94 |
18 |
12 |
67 |
|
06 |
42 |
12 |
55 |
94 |
18 |
44 |
67 |
Построена пирамида:
┌─────06─────┐
┌──42──┐ ┌──12──┐
┌─55 94 18 44
67
Для каждого элемента выполняются условия: hih2i, hih2i+1.
Для сортировки всех элементов с помощью пирамиды нужно выполнить n шагов просеивания. На каждом шаге выбирается последний элемент пирамиды (назовем его x); верхний элемент пирамиды помещается на его место и не принимает участия в дальнейших действиях; x просеивается через пирамиду, в результате чего на вершину дерева выталкивается очередной наименьший элемент, а просеиваемый элемент занимает свое место. Процесс повторяется до тех пор, пока в пирамиде не останется один элемент.
Процесс сортировки описывается следующим образом:
Right:=n;
while Right>1 do begin
x:=a[1]; a[1]:=a[Right]; a[Right]:=x;
Dec(Right); Sift(1, Right);
end;
Иллюстрация процесса сортировки (│ – граница пирамиды):
|
06 |
42 |
12 |
55 |
94 |
18 |
44 |
67 |
|
12 |
42 |
18 |
55 |
94 |
67 |
44 |
06 |
|
18 |
42 |
44 |
55 |
94 |
67 |
12 |
06 |
|
42 |
55 |
44 |
67 |
94 |
18 |
12 |
06 |
|
44 |
55 |
94 |
67 |
42 |
18 |
12 |
06 |
|
55 |
67 |
94 |
44 |
42 |
18 |
12 |
06 |
|
67 |
94 |
55 |
44 |
42 |
18 |
12 |
06 |
|
94 |
76 |
55 |
44 |
42 |
18 |
12 |
06 |
Сортировка выполняется по убыванию, для достижения противоположного порядка необходимо изменить упорядочивающие отношения в процедуре Sift: if a[j]<a[j+1] then Inc(j); if x>=a[j] then Break;
Эффективность пирамидальной сортировки тем больше, чем больше n. Число сравнений Cnlog2(n), число перемещений Pnlog2(n)/2. Алгоритм неустойчивый и имеет несколько неестественное поведение.
-
Сортировка слиянием
Слияние означает объединение двух или более отсортированных последовательностей в единую упорядоченную последовательность путем поочередного сравнения элементов исходных последовательностей и выдачи младшего из них в результирующую.
Сортировка простым слиянием:
1) последовательность a разбивается на две половины: b и c;
2) части b и c сливаются, при этом одиночные элементы образуют упорядоченные пары;
3) полученная последовательность под именем a вновь обрабатывается, как указано в пунктах 1 и 2, при этом упорядоченные пары образуют четверки;
4) повторяя предыдущие шаги, сливаем четверки в восьмерки и т.д., каждый раз «удваивая» длину слитых последовательностей до тех пор, пока не будет упорядочена вся последовательность.
Если одна из частей b или c будет исчерпана, то оставшаяся часть другой копируется (переписывается) в выходную последовательность.
Возьмем в качестве примера последовательность: 44 55 12 42 94 18 06 67. После разбиения (шаг 1) получаем две последовательности
44 55 12 42 и 94 18 06 67.
Слияние одиночных компонент (т.е. упорядоченных последовательностей длины 1) в упорядоченные пары дает
44 94 18 55 06 12 42 67
Делим массив пополам и сливаем упорядоченные пары:
06 12 44 94 18 42 55 67
Третье разделение и слияние приводят к желаемому результату
06 12 18 42 44 55 67 94
Разбиение последовательности a на части b и c можно осуществлять, сливая на первом проходе соседние элементы, на следующем – соседние пары, затем четверки и т.д. Можно при слиянии брать элементы с двух концов массива и слитые пары поочередно записывать на разные концы нового массива. При этом сразу будет получаться разделенная последовательность с упорядоченными парами элементов.
Сортировка методом простого слияния требует Pnlog2n пересылок элементов и несколько меньшего числа сравнений, поскольку при копировании остатков сравнения ключей не выполняются. При реализации алгоритма требуется n элементов резервной памяти.
Л
абораторная
работа 6.
Внутренняя
сортировка
Задание
Составить программу сортировки заданной последовательности. Программу необходимо выполнить с двумя вариантами исходных данных:
– отладочный (10 элементов), демонстрирующий результаты всех проходов алгоритма сортировки;
– рабочий (50 элементов), в результате обработки которого выводятся отсортированная последовательность, а также значения параметров эффективности: число сравнений ключей С и число перестановок элементов P (или число переключений связей V для списка).
Исходные данные и результаты работы программы (демонстрационные и рабочие) разместить в текстовых файлах.
В вариантах с нечетными номерами сортируется последовательность записей, состоящих из двух полей: строки и числового ключа.
В вариантах с четными номерами сортируется по алфавиту последовательность строковых переменных.
В вариантах, отмеченных звездочкой, исходная последовательность организована в виде связанной структуры – списка. В остальных вариантах сортируемая последовательность хранится в массиве.
|
Вариант |
Метод сортировки |
|
1 2 3, 4* 5, 6 7, 8 9, 10* 11 12 13, 14 15, 16 17, 18* 19 ,20 21, 22* 23, 24* 25, 26 27, 28 29, 30 |
Модифицированный метод подсчета Метод выбора из дерева Метод простых вставок Метод бинарных вставок Метод двухпутевых вставок Сортировка с вычислением адреса по старшей цифре Пирамидальная сортировка Метод обмена с запоминанием количества обменов Шейкер-сортировка Метод четных и нечетных транспозиций Метод простого выбора Быстрая сортировка Сортировка простым слиянием Сортировка с вычислением адреса по младшей цифре Вставки в список Метод подсчета Метод обмена с запоминанием адреса последнего обменов |
При выполнении работы необходимо руководствоваться рекомендациями, приведенными в задании к лабораторной работе 5.
П
риложение
1.
Разработка и выполнение программ
в консольном режиме Delphi 7
-
Работа с консольным приложением
Для создания консольного приложения в меню File выберите команду New, а затем в открывшемся подменю выберите Other…. На экране появится окно New Items с открытой вкладкой New. Выберите Console Application (консольное приложение).
На экране появится окно Редактора кода с заголовком Project1.dpr (номер проекта может изменяться в зависимости от количества ранее созданных проектов в текущем сеансе работы Delphi). В окне Редактора кода располагается программа. Имя проекта (программы) совпадает с именем файла проекта.
К окну проекта слева пристыковано окно Проводника кода. В нем в виде дерева отображаются все объекты программы: процедуры, типы, переменные и константы, модули.
Для каждого нового проекта целесообразно создавать отдельную папку. Это необходимо потому, что даже достаточно простой проект состоит из нескольких файлов.
-
Установка параметров среды Delphi
Режим автосохранения. Для установки режима автосохранения в меню Tools выберите подменю Environment Options. Появится диалоговое окно настройки параметров среды. На вкладке Preferences в группе Autosave options (опции автосохранения) установите флажок Editor files (редактируемые файлы).
Создание резервных копий файлов. Если вы хотите, чтобы в процессе редактирования создавались резервные копии файлов, в меню Tools выберите подменю Editor Options. Появится диалоговое окно настройки параметров редактора Editor Properties. На вкладке Display в группе Display and file options (опции дисплея и файлов) установите флажок Create backup files (создавать резервные копии файлов).
Использование русских букв. Чтобы иметь возможность использовать в комментариях и строковых константах русские буквы, выберите для отображения программы один из шрифтов с кириллицей. Для этого в меню Tools выберите подменю Editor Options. В диалоговом окне настройки параметров редактора Editor Properties на вкладке Display в поле Editor font выберите один из шрифтов с кириллицей, например, Courier New Cyr. В поле Size можно выбрать размер шрифта.
Получение информации о данных программы. Возможность получения отладочной информации устанавливается в том же окне настройки параметров редактора Editor Properties на вкладке Code Insight в группе Automatic features:
-
Code parameters – разрешает появление подсказки с перечислением формальных параметров подпрограмм при вводе открывающей круглой скобки после имени подпрограммы при записи оператора обращения к ней;
-
Tooltip expression evaluation – разрешает показывать текущее значение переменной или выражения при указании на них мышью в процессе отладки программы.
-
Компиляция и выполнение программы
Для компиляции проекта в меню Project выберите команду Compile Project или нажмите комбинацию клавиш Ctrl+F9.
Для выполнения
программы в меню Run
выберите команду Run,
или нажмите кнопку
на панели инструментов Отладка,
или нажмите клавишу F9.
При выполнении программы, использующей вывод данных на экран дисплея, результаты выводятся в специальном окне, соответствующем экрану пользователя DOS. Сразу же после выполнения программы это окно закрывается. Чтобы задержать окно на экране для просмотра результатов выполнения программы, необходимо после каждой порции выводимых данных размещать операторы
Writeln('Press Enter to continue.'); // вывод подсказки для продолжения работы
Readln; // ожидание нажатия клавиши Enter
Выполнение процедуры Readln переведет экран пользователя в состояние ожидания и потребует нажатия на клавишу Enter для продолжения выполнения программы.
-
Состав проекта
Консольный проект Delphi состоит из следующих файлов (в скобках указаны расширения имен файлов):
-
файл проекта (dpr);
-
файл параметров проекта (dof);
-
файл параметров среды (cfg);
-
файлы модулей (pas), если при создании программы создаются и используются модули.
Кроме этих файлов могут автоматически создаваться резервные копии файлов: ~dpr – для файла проекта и ~pas – для файлов модулей.
После компиляции создается исполняемый файл (exe) с именем проекта.
-
Использование русских букв
Поскольку код программы представлен в кодировке Windows (ANSI), а вывод в консольном режиме осуществляется в кодировке DOS (ASCII), то возможны проблемы с выводом русских букв. В этом случае следует использовать функцию перекодировки символов из кодировки Windows в кодировку DOS. Таблица кодов русских букв в кодировках Windows и DOS, а также текст функции перекодировки с именем WinDOS приведёны ниже.
Кодировка русских букв в стандартах ASCII и ANSI
|
Буквы |
Коды |
Разность кодов ANSI и ASCII |
|
|
ANSI (Windows) |
ASCII (DOS) |
||
|
А..Я |
192..223 |
128..159 |
64 |
|
а..п |
224..239 |
160..175 |
64 |
|
р..я |
240..255 |
224..239 |
16 |
|
Ё |
168 |
240 |
–72 |
|
ё |
184 |
241 |
–57 |
function WinDOS(const s:string):string;
// Перекодировка русских символов строки s из ANSI (Windows) в ASCII (DOS)
var
i: Word; // номер символа в строке
begin
Result:=s; // копирование исходной Windows-строки в строку-результат
for i:=1 to Length(s) do begin
case Result[i] of
'А'..'п' : Dec(Result[i],64); // уменьшение кода ANSI на 64
'р'..'я' : Dec(Result[i],16); // уменьшение кода ANSI на 16
'Ё' : Inc(Result[i],72); // увеличение кода ANSI на 72
'ё' : Inc(Result[i],57); // увеличение кода ANSI на 57
end; // case
end; // for
end; // WinDOS
Для вывода в консольном режиме строки, содержащей русские буквы, функцию WinDOS можно использовать следующим образом:
Writeln(WinDOS(‘Введите значение удаляемого элемента’));
П
риложение
2.
Особенности языка программирования
Delphi
В среде Delphi для разработки приложений используется язык программирования Delphi, основу которого составляет язык Object Pascal (объектно-ориентированное расширение стандартного языка Pascal).
Все основные конструкции Borland Pascal 7.0 сохранены в языке Delphi. Коренному преобразованию подверглась модель объектов. Delphi поддерживает сразу две модели: «старую», появившуюся в Turbo Pascal 5.5, и «новую», ориентированную на среду визуального программирования. Сочетание старой и новой моделей в одной программе фирмой Borland декларируется как «возможное», однако без необходимости делать это не рекомендуется.
-
Комментарии
Текст многострочного комментария ограничивается символами (* и *) или их эквивалентами { и }.
Однострочный комментарий содержит двойной слэш (//) в начале комментария.
-
Отличия в описании простых типов данных
Некоторые простые типы данных (целочисленные, вещественные и символьные) делятся на базовые типы Delphi и машинно-ориентированные типы.
Базовые типы заложены в язык при его разработке, они не зависят от особенностей конкретного компьютера и используются для надлежащего представления данных в памяти ЭВМ.
Машинно-ориентированные типы используются для выполнения арифметических операций в процессоре компьютера, их состав определяется операционной системой и процессором ЭВМ, они представляют собой некоторое подмножество базовых типов. Их использование считается более предпочтительным, т.к. при этом компилятор создает более эффективный код. При использовании базовых типов, не совпадающих с машинно-ориентированными, во время выполнения операций осуществляется предварительное приведение к «ближайшему» машинно-ориентированному типу.
Базовые целочисленные типы
|
Имя типа |
Диапазон |
Представление в памяти |
|
Shortint |
–128 .. 127 |
1 байт, со знаком |
|
Smallint |
–32 768 .. 32 767 |
2 байта, со знаком |
|
Longint |
–2 147 483 648 .. 2 147 483 647 |
4 байта, со знаком |
|
Int64 |
–263 .. 263–1 |
8 байтов, со знаком |
|
Byte |
0 .. 255 |
1 байт, без знака |
|
Word |
0 .. 65 535 |
2 байта, без знака |
|
LongWord |
0 .. 4 294 967 295 |
4 байта, без знака |
Машинно-ориентированные целые типы в Delphi представлены типами Integer и Cardinal. Тип Integer эквивалентен базовому типу Longint, а тип Cardinal – типу LongWord. Фактически они представляют собой знаковое и беззнаковое четырехбайтные числа.
Базовые вещественные типы
|
Имя типа |
Минимальное значение |
Максимальное значение |
Точность (число цифр мантиссы) |
Память, байт |
|
Real48 |
2.910–39 |
1.71038 |
11–12 |
6 |
|
Single |
1.710–45 |
3.41038 |
7–8 |
4 |
|
Double |
5.010–324 |
1.710308 |
15–16 |
8 |
|
Extended |
3.610–4951 |
1.1104932 |
19–20 |
10 |
|
Comp |
–21063+1 |
21063–1 |
19–20 |
8 |
|
Currency |
–922 337 203 685 477.5808 |
922 337 203 685 477.5808 |
19–20 |
8 |
Машинно-ориентированный вещественный тип представлен типом Real, который эквивалентен базовому типу Double.
Значениями символьного типа являются отдельные символы. Базовые символьные типы представлены типами AnsiChar и WideChar.
Символ типа AnsiChar занимает один байт, а для кодирования символов используется код ANSI Американского национального института стандартов (American National Standards Institute). Символ типа WideChar занимает два байта, а для кодирования символов используется международный набор символов Unicode. Набор символов Unicode включает в свой состав более 60 тысяч элементов. Первые 256 символов Unicode совпадают с кодом ANSI.
Кроме базовых символьных типов в языке Delphi определен машинно-ориентированный тип Char, который эквивалентен типу AnsiChar.
-
Особенности описания и использования функций
Подпрограмма-функция предназначена для вычисления какого-либо значения. В отличие от процедуры, в заголовке функции указывается тип возвращаемого значения. В разделе операторов функции хотя бы раз имени функции должно быть присвоено значение.
Имя функции нельзя использовать как простую переменную. Так, если имя функции внутри ее описания используется не в левой части оператора присваивания, то это означает, что функция вызывает себя рекурсивно.
Внутри каждой функции по умолчанию определена локальная переменная Result, имеющая тот же тип, что и возвращаемое функцией значение. В отличие от имени функции, которое доступно для записи и используется только для возврата значения, переменная Result доступна для чтения и может быть использована в промежуточных вычислениях. Значение переменной Result при входе в функцию не инициализируется и до первого присваивания не определено. При выходе из функции значение переменной Result автоматически присваивается имени функции.
Пример. Функция вычисления факториала числа n.
function Factorial(n: Byte): Longint;
var
i: Byte;
begin
Result := 1; // 0! = 1!= 1 по определению.
for i := 2 to n do Result:= Result * i;
end;
Стилистической нормой является следующее требование: описание подпрограммы должно включать в себя спецификацию подпрограммы в виде комментария, содержащего: назначение подпрограммы, описание формальных параметров и значения функции (если подпрограмма – функция), требования, накладываемые на входные параметры: диапазон изменения, допустимые значения.
-
Обработка исключительных ситуаций
Возникновение исключительных ситуаций
В ходе выполнения программы могут возникать динамические ошибки, которые являются следствием неправильной работы инструкций, процедур, функций или методов программы, а также операционной системы. Динамические ошибки называют также ошибками времени выполнения (RunTime Errors). Например, в операторе присваивания:
Itog:=Сount / Number,
во время выполнения программы возможно появление ошибки, если переменная Number будет принимать нулевое значение.
Программист должен предвидеть возможность возникновения ошибок и предусматривать их обработку. Для обработки динамических ошибок введено понятие исключения, которое представляет собой нарушение условий выполнения программы, вызывающее прерывание или полное прекращение ее работы. Обработка исключения состоит в нейтрализации вызвавшей его динамической ошибки.
Исключения могут возникать по различным причинам, например, в случае нехватки памяти, из-за ошибки преобразования, в результате выполнения вычислений или попытки обращения к несуществующему элементу массива, выход за допустимые пределы значения и т.д. В любом случае, независимо от источника ошибки приложение информируется о его возникновении.
Возникающие при выполнении программы динамические ошибки автоматически преобразовываются средствами Delphi в соответствующие объекты-исключения. Объект-исключение содержит информацию о типе ошибки и при возникновении исключения заставляет программу временно приостановиться. После обработки исключения объект-исключение автоматически удаляется.
Внимание! При работе в среде Delphi при каждой исключительной ситуации среда перехватывает управление программой. Для отмены реакции среды на исключительную ситуацию необходимо вызвать опцию Tools|Debugger Options и на странице Language Exceptions отменить переключатель Stop on Delphi Exceptions.
Классы исключений
В Delphi базовым классом для всех исключений служит класс Exception. Свойство Message класса Exception содержит описание исключений. При возникновении исключения этот текст появляется в диалоговом окне сообщения об ошибке. Наиболее часто объект-исключение использует конструктор: Create(const Msg: string), который создает объект-исключение; строка Msg указывает текст для свойства Message создаваемого объекта.
От класса Exception порождены многочисленные дочерние типы, соответствующие часто встречающимся случаям ошибок ввода/вывода, распределения памяти и т.п.
Наиболее часто используются следующие классы исключений:
|
Класс |
Обрабатываемое исключение |
|
EAbort |
«Тихая» (без сообщения) обработка любого исключения. |
|
EConvertError |
Ошибки преобразования в функциях StrToInt или StrToFloat. |
|
EInOutError |
Ошибка в файловых операциях. Поле ErrorCode объекта этого класса содержит код ошибки: 2 – Файл не найден; 3 – Неправильное имя файла; 4 – Слишком много открытых файлов; 5 – Файл не доступен; 100 – Достигнут конец файла (EOF); 101 – Диск переполнен; 106 – Ошибка ввода. |
|
EIntError |
Любая ошибка в целочисленных вычислениях. |
|
EDivByZero |
Попытка целочисленного деления на ноль. |
|
ERangeError |
Присвоение целочисленной переменной значения, выходящего за пределы допустимого диапазона. |
|
EIntOverFlow |
Переполнение в операции с целочисленными переменными. |
|
EMathError |
Любая ошибка в вычислениях с плавающей точкой. |
|
EInvalidOp |
Ошибка в операциях (недопустимая инструкция, переполнение стека сопроцессора и т.п.). |
|
EZeroDivide |
Попытка вещественного деления на ноль. |
|
EOverFlow |
Переполнение в операции с плавающей точкой. |
|
EUnderFlow |
Результат операции с плавающей точкой слишком мал, чтобы уместиться в регистрах сопроцессора (исчезновение порядка). Результат получает нулевое значение. |
|
EInvalidCast |
Недопустимое преобразование типов с помощью оператора as. |
|
EInvalidGraphic |
Программа пытается загрузить в контейнер изображение из файла, который имеет недопустимый формат (допустимыми форматами являются растр, метафайл, курсор, пиктограмма). |
|
EInvalidGraphic Operation |
Программа пытается выполнить недопустимую графическую операцию. |
|
EInvalidPointer |
Попытка использовать недопустимый указатель (обычно nil). |
|
EListError, EStringListError |
Неверные действия программы по отношению к разного рода спискам (например, индекс меньше нуля). |
|
EMenuError |
Ошибка при работе с меню, например, при добавлении элемента с идентификатором, ранее определенным в меню. |
|
EOutOfMemory |
Программа запрашивает слишком большой объем памяти. |
Обработка исключений
Для обработки исключений в приложении в состав языка введены две конструкции: try .. finally и try .. except.
Конструкция try .. finally состоит из двух блоков (try и finally):
try
// операторы, выполнение которых может вызвать ошибку
finally
// операторы, выполняемые всегда, даже в случае ошибки
end;
Если в любом из операторов блока try возникает исключение, то управление передается первому оператору блока finally. Если исключение не возникло, то выполняются все операторы обоих блоков.
Конструкция try .. finally не обрабатывает исключение, а выполняет действия, которые должны быть произведены даже в случае возникновения ошибки. Такими действиями могут являться, к примеру, освобождение оперативной памяти или закрытие файла.
Пример использования конструкции try .. finally:
var f: File;
begin
AssignFile(f, 'Somefile.ext');
try
// операторы, в которых возможна ошибка работы с файлом
Reset(f);
finally
CloseFile(f);
end;
end;
Так как конструкция try .. finally не ликвидирует исключительную ситуацию, в приведенном примере при возникновении исключения глобальный обработчик приложения выдаст сообщение о характере ошибки.
Конструкция try .. except также состоит из двух блоков (try и except):
try
// операторы, выполнение которых может вызвать ошибку
except
// операторы, которые должны быть выполнены только в случае ошибки
end;
В отличие от предыдущей данная конструкция применяется для перехвата исключения и предоставляет возможность его обработки.
Конструкция try .. except работает так: если в любой из инструкций блока try возникает исключение, то управление передается первой инструкции блока except. Если же исключение не возникло, то инструкции блока except не выполняются. При появлении исключения инструкции блока except могут ликвидировать исключительную ситуацию и восстановить работоспособность программы.
Пример использования конструкции try .. except:
var f: File;
begin
AssignFile(f, 'Somefile.ext');
try
// операторы, в которых возможна ошибка работы с файлом
Reset(f);
except
ShowMessage(‘Ошибка работы с файлом Somefile.ext’);
CloseFile(f);
end;
end;
При возникновении исключения его обрабатывают инструкции блока except. В данном примере обработка заключается в выдаче предупреждающего сообщения и закрытии файла. Если работа с файлом выполнена корректно, то инструкции блока except не выполняются.
Блок except можно разбить на несколько частей с помощью конструкций on .. do, позволяющих анализировать класс исключения для его более удобной и полной обработки:
on Идентификатор: класс исключения do Оператор обработки ИС этого класса
else Операторы обработки исключения по умолчанию;
В инструкции on класс возникшего исключения сравнивается с указанным классом исключения. В случае совпадения классов выполняется оператор после слова do, реализующий обработку этого исключения.
Идентификатор (произвольное имя, заданное программистом) является необязательным элементом и может отсутствовать, при этом не ставится и разделительный знак двоеточия (:). Идентификатор – это локальная переменная класса исключения, которую можно использовать для доступа к объекту возникшего исключения внутри конструкции on .. do.
Если класс возникшего исключения не совпадает с проверяемым классом, то выполняется оператор после слова else. Блок else является необязательным и может отсутствовать.
Стандартная обработка подразумевает вывод на экран сообщения с указанием типа ошибки, имени модуля и адреса, где она имела место. Однако ее легко переопределить:
try
U:= 220.0; R:= 0; I:=U/R;
except
on EZeroDivide do ShowMessage('Короткое замыкание! ');
end;
При возникновении ИС объект класса-обработчика создается и уничтожается автоматически. Если во время обработки программисту требуется доступ к свойствам и методам этого объекта, он должен поименовать автоматически создаваемый объект внутри on ... do:
on EObject: EClassName do EObject.Message := ...
Для стандартных классов такой прием фактически позволяет использовать единственное строковое свойство Message со стандартным сообщением об ошибке. Исключение составляет класс EInOutError, в котором для программиста может представлять интерес целочисленное свойство ErrorCode с кодом ошибки ввода/вывода. Например:
try
Reset (f);
while not EOF(f) do begin … end; // возможна ошибка работы с файлом
Close(f);
except
on E: EInOutError do ShowMessage(′При выполнении файловой операции’+
#13#10‘возникла ошибка №′+IntToStr(E.ErrorCode));
end;
Если в блоке except расположено несколько конструкций on .. do, то else располагается в конце блока и относится ко всей совокупности конструкций on .. do. Следующий после слова else оператор выполняется в том случае, если обработка исключений не была осуществлена ни в одном из операторов, расположенных в любой из конструкций do блока.
Если обработка ошибки (после do) требует записи нескольких операторов, то их необходимо заключить в блок begin … end. После else можно записывать любое количество операторов.
Пример. В поля Edit1 и Edit2 вводятся два целых числа. При нажатии кнопки btDiv выполняется преобразование этих чисел из текстового формата в числовой, после чего первое число делится на второе, а результат помещается в поле Edit3. Эти действия могут вызвать ошибки, поэтому они помещены в блок try. Если возникает исключение, оно анализируется в блоке except. Проверяются варианты: EDivByZero и EConvertError. При возникновении одного из прогнозируемых исключений выдается сообщение об ошибке (дополнительно устанавливается фокус ввода на одно из полей редактирования и в поле результата выводится строка «Ошибка!»). Любое другое исключение не будет распознано. В этом случае выполняются операторы блока else и выдается сообщение о неопознанной ошибке.
procedure TForm1.btDivClick(Sender: TObject);
var x, y, res: Integer;
begin
try
x:=StrToInt(Edit1.Text); // возможна ошибка преобразования
y:=StrToInt(Edit2.Text); // возможна ошибка преобразования
res:=x div y; // возможна ошибка деления на ноль
Edit3.Text:=IntToStr(res);
except
on EDivByZero do begin // проверка ошибки целочисленного деления на ноль
ShowMessage('Попытка деления на ноль!');
Edit2.SetFocus; // установка фокуса ввода на поле делителя
Edit3.Text:=’Ошибка!’; // вывод текста «Ошибка» в поле результата
end;
on EConvertError do begin // проверка ошибки преобразования
ShowMessage('Одно из чисел содержит недопустимые символы!');
Edit1.SetFocus; // установка фокуса ввода на поле первого числа
Edit3.Text:=’Ошибка!’; // вывод текста «Ошибка» в поле результата
end;
else
ShowMessage('Ошибка не идентифицирована');
Edit1.SetFocus; // установка фокуса ввода на поле первого числа
Edit3.Text:=’Ошибка!’; // вывод текста «Ошибка» в поле результата
end;
end.
Конструкции try могут быть вложенными и размещаться одна в другой. При этом, внешняя и внутренняя конструкции могут иметь любой из двух рассмотренных видов. Обязательным условием является то, что внутренний блок должен полностью размещаться во внешнем блоке.
Вызов исключений
При необходимости исключение можно генерировать программно. Для этого используется оператор raise, который создает объект-исключение – экземпляр класса Exception или его потомка. Синтаксис оператора raise:
raise ClassException.Construct
Здесь ClassException является классом исключения, на основе которого создаётся объект-исключение, а конструктор Construct выполняет создание объекта-исключения. Через этот метод объекту-исключению передаётся информация о виде исключения. Для создания объектов-исключений чаще всего используется конструктор Create классов исключений.
Строковый параметр обращения к конструктору запоминается в поле FMessage и становится доступен с помощью свойства Message объекта.
Примеры генерации исключений в секции try .. except:
raise Exception.Create(‘Новая ошибка!’) // исключение класса Exception
raise EInOutError.Create('Неверная файловая операция!'); // ошибка ввода-вывода
Обработчик исключения EInOutError в секции except .. end:
on E: EInOutError do ShowMessage(E.Message);
именует объект идентификатором E и с помощью стандартной процедуры ShowMessage показывает значение его свойства Message в окне.
Пример генерации и обработки исключения:
procedure TfmPassWord.btOKClick(Sender: TObject);
begin
try
if edPassWord.Text <> Key then raise Exception.Create('Доступ запрещен!');
fmWork.Show;
except
on E: Exception do begin ShowMessage (E.Message); Close; end;
end;
end;
В секции try обработчика нажатия кнопки btOK формы ввода пароля fmPassWord введённый в поле edPassWord текст сравнивается с переменной Key, содержащей строку пароля. Если они не совпадают, то генерируется исключение Exception и управление передаётся в блок except. Расположенный после инструкции raise оператор вывода на экран рабочей формы fmWork не выполняется.
Обработка исключения заключается в выводе соответствующего сообщения об ошибке и закрытия формы ввода пароля, что приведёт к завершению работы приложения, если эта форма – основная.
Если в какой-либо подпрограмме исключение только генерируется, но не обрабатывается, то при возникновении исключения управление передаётся в вызывающую подпрограмму, и может быть обработано там. В этом случае инструкция raise действует как процедура выхода exit, но в отличие от неё инструкция raise позволяет удобно передавать в вызывающую подпрограмму информацию о характере ошибки.
Инструкцию raise, кроме генерации исключения, можно использовать и для более удобной обработки уже возбуждённых исключений (например, добавить к стандартному обработчику ошибок собственные действия). В этом случае её синтаксис совсем прост:
raise;
Если зарезервированное слово raise встретилось в секции finally .. end или except .. end, то считается, что данный защищенный блок на текущем уровне вложенности завершил свою работу и управление передается вышестоящему уровню вложенности защищенного блока. Если других уровней обработки нет, то исключение будет перехвачено обработчиком по умолчанию, который выведет собственное сообщение об ошибке.
Пример возбуждения исключения для добавления собственного сообщения об ошибке:
try
// Возможна ошибка EMathError
except
on EMathError do begin
ShowMessage('Возникла ошибка в вычислениях!');
raise; // вызов вышестоящего или стандартного обработчика EMathError
end;
end;
Такой прием – единственная возможность возбудить нестандартное исключение, обрабатываемое пользовательским классом.
Создание классов исключений
Программист может создать собственный класс обработки исключений, объявив его потомком Exception или любого другого стандартного класса. Тогда новое исключение будет наследовать свойства и методы, требуемые для поддержки рассмотренного выше механизма обработки исключений. Объявление нестандартного класса имеет смысл только тогда, когда необходимо научить программу распознавать некорректные наборы данных и соответствующим образом на них реагировать. Обычно новый класс исключения предоставляет более подробную информацию об ошибке.
Чтобы создать класс исключения, в разделе interface модуля нужно описать новый класс, введя в него новые или переопределив старые поля, свойства и методы. Например:
type
EWrongPassword = class (Exception) // тип исключения – неверный пароль
end;
То, что в таком типе ничего не переопределено, не столь важно – главное, что в обработчике ИС можно отследить именно его.
Затем новые или измененные элементы класса, если они описаны, необходимо реализовать в разделе implementation и в нужный момент инициировать новое исключение инструкцией raise.
try
if edPassword.Text <>Key // сравнение введенного текста с паролем
// Возбуждение исключения и запись сообщения для него в его свойство Message
then raise EWrongPassword.Create('Доступ к данным невозможен!');
…
except
// При возникновении исключения E типа EWrongPassword
// вывод сообщения из его свойства Message
on E: EWrongPassword do ShowMessage(E.Message);
end;
-
Модули в Delphi
Модуль в Delphi состоит из следующих частей:
1) заголовок модуля;
2) секция интерфейса;
3) секция реализации;
4) секция инициализации;
5) секция деинициализации.
Первые три части являются обязательными. Обязательна также указанная последовательность разделов.
Заголовок модуля состоит из зарезервированного слова unit и идентификатора. Идентификатор модуля должен быть уникальным.
Модуль должен быть помещен в файл, имя которого совпадает с именем модуля, а его расширение должно быть .pas. После компиляции файл с модулем получает расширение .dcu.
Секции интерфейса (interface) и реализации (implementation) имеют то же назначение и ту же структуру, что и в Турбо Паскале.
Секция инициализации – факультативная часть модуля. В ней размещаются операторы, которые выполняются в начале работы той программы, которая использует данный модуль. Если программа использует несколько модулей, то содержимое их секций инициализации выполняется в порядке перечисления имён модулей в предложении uses. Эта секция начинается зарезервированным словом initialization, например:
initialization
Assign(f1, 'File1.dat');
Секция деинициализации начинается словом finalization и является необязательной. В этом разделе размещаются операторы, выполняемые при завершении работы программы, использующей данный модуль. Если программа использует несколько модулей, то содержимое их секций деинициализации выполняется в порядке, обратном порядку их перечисления в предложении uses.
В конце модуля размещается слово end. (с точкой).
Использование модуля в Delphi ничем не отличается от его использования в Турбо Паскале.
Добавление модуля к проекту. Для добавления модуля к проекту необходимо в меню File выполнить команду New и в появившемся списке вновь создаваемых элементов проекта выбрать Unit (модуль). В окне кода проекта появится новая страница с заготовкой модуля, а в файл проекта автоматически будут вставлена ссылка на этот модуль. Новый модуль необходимо сохранить в нужной папке под нужным именем. Обычно модуль хранится в той же папке, что и проект.
Программу, использующую модули, можно компилировать либо с помощью команды меню Project|Compile <имя проекта> (клавиши Ctrl+F9) либо Project|Build <имя проекта>.
При использовании команды Project|Compile наряду с компиляцией главной программы компилируются и те используемые программой модули, у которых к моменту компиляции был изменен текст.
При использовании команды Project|Build все используемые программой модули перекомпилируются безусловно.
Приложение 3. Классы в Dephi
-
Классы и объекты
Классом в Delphi называется особая структура, состоящая из полей, методов и свойств.
type
tMyClass = class (tObject) // класс tMyClass
fMyField: Integer; // поле fMyField
function MyMethod(Data:tData): Integer; // метод MyMethod
end;
Все классы в Delphi являются наследниками базового класс tObject, который реализует наиболее общие для всех классов элементы, например, действия по созданию и удалению объекта.
Поля класса – это данные, уникальные для каждого созданного в программе экземпляра класса. Они могут иметь любой тип, в том числе – тип класс. В Delphi перед именами полей принято ставить символ f (от field – поле): fLength, fWidth, fMyFileld и т. п.
Методы – это процедуры и функции, описанные своими заголовками внутри класса и предназначенные для операций над его полями. В отличие от полей, методы у всех объектов одного класса общие. От обычных процедур и функций методы отличаются тем, что им при вызове передается (неявно) указатель на тот объект, который их вызвал. Поэтому обрабатываться будут поля именно того объекта, который вызвал метод. Внутри метода указатель на вызвавший его объект доступен под зарезервированным именем Self.
Свойство можно определить как поле, доступное для чтения и записи не напрямую, а через соответствующие методы.
Классы
могут быть описаны либо в разделе
описания типов в самой внешней части
программы, либо в секции интерфейса
модуля, либо на верхнем уровне вложенности
секции реализации. Не допускается
описание классов внутри процедур и
других блоков кода.
Методы класса реализуются в разделе описания процедур и функций программы или разделе implementation модуля. При реализации метода указывается его полное имя, состоящее из имени класса, точки и имени метода класса:
function tMyClass.MyMethod(Data:tData): Integer;
// заголовок метода tMyClass.MyMethod
begin
… // тело метода tMyClass.MyMethod
end;
Разрешено опережающее объявление классов единственным словом class с последующим полным описанием:
type
tFirstClass = class;
tSecondClass = class(tObject)
f1 : tFirstClass;
end;
tFirstClass = class(tObject)
f2 : tSecondClass;
end;
-
Области видимости
Области видимости – это возможности доступа к составным частям объекта. В Delphi поля и методы могут относиться к четырем группам: «общие» (public), «личные» (private), «защищенные» (protected) и «опубликованные» (published).
1. Поля, свойства и методы, находящиеся в секции public, не имеют ограничений на видимость. Они доступны из других функций и методов объектов, как в данном модуле, так и во всех прочих, ссылающихся на него.
2. Поля, свойства и методы, находящиеся в секции private, доступны только в методах класса и в функциях, содержащихся в том же модуле, что и описываемый класс. Такая директива позволяет скрыть детали внутренней реализации класса от всех. Элементы из секции private можно изменять, и это не будет сказываться на программах, работающих с объектами этого класса. Обратиться к ним можно, только переписав содержащий их модуль.
3. Поля, свойства и методы, находящиеся в секции protected, доступны только внутри классов, являющихся потомками данного, в том числе и в других модулях. Такие элементы особенно необходимы дня разработчиков новых компонентов – потомков уже существующих.
4. Область видимости, определяемая четвертой директивой – published, имеет особое значение для интерфейса визуального проектирования Delphi. В этой секции должны быть собраны те свойства объекта, которые будут видны не только во время исполнения приложения, но и из среды разработки. Все свойства компонентов, доступные через Инспектор объектов, являются их опубликованными свойствами. Во время выполнения такие свойства общедоступны, как и public.
-
Создание и уничтожение объектов
В Delphi объекты могут быть только динамическими! Любая переменная объектного типа есть указатель, причем для доступа к данным, на которые ссылается указатель объекта не нужно применять символ ^.
Для выделения памяти экземпляру любой динамической переменной используется процедура New, для уничтожения – процедура Dispose. С объектами эти процедуры не используются: объект создается специальным методом – конструктором, а уничтожается специальным методом – деструктором:
AMyObject := tMyClass.Create; // создание объекта AMyObject класса tMyClass
… // действия с созданным объектом
AMyObject.Destroy; // уничтожение объекта AMyObject
Конструктор – это специальный метод, заголовок которого начинается зарезервированным словом constructor. Функция конструктора заключается в выделении памяти под экземпляр класса (объект) и установлении связи между созданным объектом и специальной информацией о классе. В теле конструктора можно расположить любые операторы, которые необходимо выполнить при создании объекта, например, присвоить полям начальные значения. В Delphi конструкторов у класса может быть несколько. Общепринято называть конструктор Create.
До передачи управления телу конструктора происходит собственно создание объекта – под него отводится память, значения всех полей обнуляются. Далее выполняется код конструктора, написанный программистом. Фактически конструктор – это функция, возвращающая созданный и проинициализированный объект.
Конструктор
создает новый экземпляр объекта только
в том случае, если перед его именем
указано имя класса. Если указать имя
уже созданного объекта, он поведет себя
по-другому: не создаст новый экземпляр,
а только выполнит код, содержащийся в
теле конструктора.
Конструктор класса-потомка практически всегда должен перекрывать конструктор класса-предка. Чтобы правильно проинициализировать в создаваемом объекте поля, относящиеся к классу-предку, нужно в начале кода конструктора вызвать конструктор предка с помощью зарезервированного слова inherited:
constructor tMyClass.Create;
begin
inherited Create;
... // собственный код конструктора
end;
Деструктор – это специальный виртуальный (см. ниже) метод, заголовок которого начинается зарезервированным словом destructor. Деструктор предназначен для удаления объектов. Типичное название деструктора – Destroy.
Для уничтожения объекта рекомендуется использовать метод Free (он есть у базового класса tObject), который первоначально проверяет указатель на объект (не равен ли он nil) и только затем вызывает деструктор Destroy:
AMyObject.Free;
Деструктор очень часто не переопределяется в классе-потомке. Но если его необходимо переопределить, то необходимо учитывать следующее. Деструктор Destroy любого класса перекрывает виртуальный деструктор базового класса tObject, поэтому деструктор потомка необходимо объявить с директивой override:
destructor Destroy; override;
При реализации перекрывающего деструктора потомка нужно в конце кода деструктора вызвать деструктор предка с помощью зарезервированного слова inherited:
destructor tMyClass.Destroy;
begin
… // собственный код деструктора
inherited Destroy;
end;
-
Инкапсуляция. Свойства
Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляцией. В Delphi пользователь объекта может быть полностью отгорожен от полей при помощи свойств.
Обычно свойство определяется тремя своими элементами: полем и двумя методами, которые осуществляют его чтение/запись:
type
tMyClass = class (tObject)
function GetAProperty: tSomeType;
procedure SetAProperty(ANewValue: tSomeType);
property AProperty: tSomeType read GetAProperty write SetAProperty;
end;
В контексте свойства слова read и write являются зарезервированными. Для доступа к значению свойства AProperty явно достаточно написать
AMyObject.AProperty := AValue;
AVariable := AMyObject.AProperty;
и компилятор оттранслирует эти операторы в вызовы методов. То есть внешне свойство выглядит в точности как обычное поле.
В методах, входящих в состав свойств, может осуществляться проверка устанавливаемой величины на попадание в допустимый диапазон значении, и вызов других процедур, зависящих от вносимых изменений. Если же потребности в специальных процедурах чтения и/или записи нет, возможно вместо имен методов применять имена полей.
Если свойство должно только читаться или только записываться, в его описании может присутствовать только соответствующий метод:
tMyClass = class (tObject)
property AProperty: tSomeType read GetValue;
end;
Значение свойства AProperty можно лишь прочитать; попытки присвоить AProperty значение вызовет ошибку компиляции.
Для присвоения свойству значения по умолчанию используется ключевое слово default:
property Visible: Boolean read fVisible write SetVisible default True
Свойство может быть и векторным; в этом случае оно внешне выглядит как массив:
property APoints[Index: Integer]:tPoint read GetPoint write SetPoint;
На самом деле, среди полей класса может и не быть поля-массива. При помощи свойств вся обработка обращений к внутренним структурам класса может быть замаскирована.
Для векторного свойства необходимо описать не только тип элементов массива, но также имя и тип индекса. После ключевых слов read и write в этом случае должны стоять имена методов – использование здесь имени поля типа массив недопустимо. Метод чтения значения векторного свойства должен быть описан как функция, возвращающая значение того же типа, что и элементы свойства, и имеющая единственный параметр: того же типа и с тем же именем, что и индекс свойства:
function GetPoint(Index: Integer): TPoint;
Метод записи значения в такое свойство должен первым параметром иметь индекс, а вторым – переменную нужного типа (которая может быть передана как по ссылке, так и по значению):
procedure SetPoint(Index: Integer; NewPoint: tPoint);
У векторных свойств есть еще одна важная особенность. Некоторые классы в Delphi (списки, наборы строк) «построены» вокруг векторного свойства. Основной метод такого класса дает доступ к некоторому массиву, а все остальные методы являются вспомогательными. Специально для облегчения работы в этом случае векторное свойство может быть описано как default:
tМуClass = class
property Strings[Index: Integer]: string read Get write Put; default;
end;
Когда у объекта есть такое свойство (его называют векторным свойством по умолчанию), то можно его не упоминать, а ставить индекс в квадратных скобках прямо у имени объекта:
var AMyObject: tMyClass;
begin
AMyObject.Strings[1] := 'First'; // первый способ
AMyObject[2] := 'Second'; // второй способ
...
end;
Будьте внимательны, применяя зарезервированное слово default, – для обычных и векторных свойств оно употребляется в разных случаях и с различным синтаксисом.
О роли свойств в Delphi красноречиво говорит следующий факт: у всех имеющихся в вашем распоряжении стандартных классов 100% полей недоступны (помещены в секцию private) и заменены базирующимися на них свойствами. Рекомендуем при разработке своих классов придерживаться этого же правила.
-
Наследование. Методы
Принцип наследования позволяет объявить класс
tNewObject = class(tOldObject);
являющийся потомком или дочерним классом старого класса, называемого предком или родительским классом, и добавить к нему новые поля, методы и свойства.
В Delphi все классы являются потомками класса tObject. При построении дочернего класс прямо от tObject в определении его можно не упоминать. Следующие объявления одинаково верны:
tMyObject = class(tObject);
tMyObject = class;
Первый вариант предпочтительнее, хотя он и более длинный, – для устранения возможных неоднозначностей. Класс tObject несет очень серьезную нагрузку и будет рассмотрен отдельно.
Унаследованные от предка поля и методы доступны в дочернем классе; если имеет место совпадение имен методов, то говорят, что они перекрываются.
По тому, какие действия происходят при вызове, методы делятся на три группы: 1) статические, 2) виртуальные (virtual) и динамические (dynamic), 3) перегружаемые (overload) методы.
Статические методы, а также любые поля в объектах-потомках ведут себя одинаково: можно без ограничений перекрывать старые имена, и при этом изменять тип методов. Перекрытое поле предка недоступно в потомке. Перекрытый метод доступен при указании зарезервированного слова inherited. Методы объектов по умолчанию являются статическими – их адрес определяется еще на стадии компиляции проекта. Они вызываются быстрее всего.
Принципиально отличаются от статических виртуальные и динамические методы (директива virtual или dynamic). Их адрес определяется во время выполнения программы по специальной таблице. С точки зрения наследования методы этих двух видов одинаковы: они могут быть перекрыты в дочернем классе только одноименными методами, имеющими тот же тип.
В Delphi понятие множественного наследования отсутствует. Если вы хотите, чтобы новый класс объединял свойства нескольких, породите классы-предки один от другого или включите в класс несколько полей, соответствующих этим желаемым классам.
-
Полиморфизм. Виртуальные и динамические методы
Рассмотрим пример. Пусть у нас имеются некое обобщенное поле для хранения данных – абстрактный класс tField и три его потомка – для хранения строк, целых и вещественных чисел:
tField = class
function GetData: string; virtual; abstract;
end;
tStringField = class(tField)
fData : string;
function GetData: string; override;
end;
tIntegerField = class(tField)
fData : Integer;
function GetData: string; override;
end;
tExtendedField = class(tField)
fData : Extended;
function GetData: string; override;
end;
function tStringField.GetData;
begin
Result := fData;
end;
function tIntegerField.GetData;
begin
Result := IntToStr(fData);
end;
function tExtendedField.GetData;
begin
Result:=FloatToStr(fData);
end;
procedure ShowData(AField:tField);
begin
Writeln(AField.GetData);
end;
В этом примере классы содержат разнотипные данные и «умеют» сообщать о значении этих данных текстовой строкой (при помощи метода GetData). Внешняя по отношению к ним процедура ShowData получает объект в виде параметра и показывает эту строку.
Правила контроля соответствия типов (typecasting) языка Delphi гласят, что объекту как указателю на экземпляр объектного типа может быть присвоен адрес любого экземпляра любого из дочерних типов. В процедуре ShowData параметр описан как tField – это значит, что в нее можно передавать объекты классов и tStringField, и tIntegerField, и tExtendedField, и любого другого потомка tField.
Но чей метод GetData при этом будет вызван? Тот, который соответствует классу фактически переданного объекта. Этот принцип называется полиморфизмом и он, пожалуй, представляет собой наиболее важный принцип ООП. Например, чтобы смоделировать некоторую совокупность явлений или процессов средствами ООП, нужно выделить их самые общие, типовые черты. Те из них, которые не изменяют своего содержания, должны быть реализованы в виде статических методов. Те же, которые варьируются при переходе от общего к частному, лучше облечь в форму виртуальных методов. Основные, «родовые» черты (методы) нужно описать в классе-предке и затем перекрывать их в классах-потомках.
При вызове виртуальных и динамических методов адрес определяется не во время компиляции, а во время выполнения – это называется поздним связыванием (late binding). Позднее связывание реализуется с помощью таблицы виртуальных методов (Virtual Method Table, VMT) и таблицы динамических методов (Dynamic Method Table, DMT).
Разница между виртуальными и динамическими методами заключается в особенности поиска адреса. Когда компилятор встречает обращение к виртуальному методу, он подставляет вместо обращения к конкретному адресу код, который обращается к VMT этого объекта и извлекает оттуда нужный адрес. Такая таблица одна для каждого класса (объектного типа). В ней хранятся адреса всех виртуальных методов класса, независимо от того, унаследованы ли они от предка или перекрыты. Отсюда и достоинства, и недостатки виртуальных методов: они вызываются сравнительно быстро (но медленнее статических), однако для хранения указателей на них требуется большое количество памяти.
Динамические методы вызываются медленнее, но позволяют более экономно расходовать память. Каждому динамическому методу системой присваивается уникальный индекс. В таблице динамических методов класса хранятся индексы и адреса только тех динамических методов, которые описаны в данном классе. При вызове динамического метода происходит поиск в этой таблице; в случае неудачи просматриваются таблицы DMT всех классов-предков в порядке иерархии и, наконец, DMT класса tObject, где имеется стандартный обработчик вызова динамических методов. Экономия памяти налицо.
Для перекрытия и виртуальных, и динамических методов служит директива override, с помощью которой (и только с ней!) можно переопределять оба этих типа методов. Приведем пример:
tClass1 = class
fField1: Integer;
fField2: Longint;
procedure stMet;
procedure vrMet1; virtual;
procedure vrMet2; virtual;
procedure dnMet1; dynamic;
procedure dnMet2; dynamic;
end;
tClass2 = class(tClass1)
procedure stMet;
procedure vrMet1; override;
procedure dnMet1; override;
end;
var
Obj1: tClass1;
Obj2: tClass2;
Первый метод класса tClass2 создается заново, остальные два – перекрываются. Попытка применить директиву override к статическому методу вызовет ошибку компиляции.
-
Пример реализации полиморфизма для иерархии графических объектов
Рассмотрим описание и реализацию классов «Точка» и «Окружность» для «рисования» на экране точки и окружности.
type
tCoord = Word; // тип – координата точки на экране
// Описание класса tPoint
tPoint = class(tObject) // класс – точка на экране дисплея
protected
fX, fY: tCoord; // поля – координаты точки на экране
fColor: Byte; // поле – цвет рисования точки
public
property X: tCoord read fX write fX; // свойство – координата X
property Y: tCoord read fY write fY; // свойство – координата Y
property Color: Byte read fColor write fColor; // свойство – цвет
procedure Show; // метод высвечивания точки
procedure Hide; // метод гашения точки
procedure MoveTo(NewX, NewY: tCoord); // метод перемещения точки
end;
// Описание класса tCircle
tCircle = class(tPoint) // класс – окружность на экране
protected
fRadius: tCoord; // поле – радиус окружности
public
property Radius: tCoord read fRadius write fRadius; // свойство – радиус
procedure Show; // метод высвечивания окружности
procedure Hide; // метод гашения окружности
procedure MoveTo (NewX,NewY:tCoord) // метод перемещения окружности
end;
// Реализация методов класса tPoint
procedure tPoint.Show;
begin
Writeln('Рисую точку (', fx, ', ', fy, ') цветом ', fColor); // «рисование» точки
end;
procedure tPoint.Hide;
begin
Writeln('Гашу точку (', fx, ', ',fy, ') цвета ', fColor); // «гашение» точки
end;
procedure tPoint.MoveTo(NewX, NewY: tCoord);
begin
Hide;
fX := NewX; fY := NewY;
Show;
end;
// Реализация методов класса tCircle
procedure tCircle.Show;
begin
// «рисование» окружности
Writeln('Рисую окружность с центром (', fx, ', ', fy, ') радиуса ',
fRadius, ' цветом ', fColor);
end;
procedure tCircle.Hide;
begin
// «гашение» окружности
Writeln('Гашу окружность с центром (', fx, ', ', fy, ') радиуса ',
fRadius, ' цвета ', fColor);
end;
procedure tCircle.MoveTo (NewX,NewY: tCoord);
begin
Hide;
fX := NewX; fY := NewY;
Show;
end;
Обратите внимание, что методы MoveTo классов tPoint и tCircle реализуются одинаково за исключением того, что в tPoint.MoveTo используются tPoint.Hide и tPoint.Show, а в tCircle.MoveTo используются tCircle.Hide и tCircle.Show. Взаимодействие методов классов tPoint и tCircle можно проиллюстрировать следующей схемой:

Поскольку методы реализуются одинаково, можно попытаться унаследовать метод MoveTo от класса tPoint. При этом возникает следующая ситуация. При компиляции метода tPoint.MoveTo в него будут включены ссылки на коды методов tPoint.Show и tPoint.Hide. Так как метод с именем MoveTo не определен в классе tCircle, то компилятор обращается к родительскому типу и ищет в нем метод с этим именем. Если метод найден, то адрес родительского метода заменяет имя в исходном коде потомка. Следовательно, при вызове tCircle.MoveTo в программу будут включены коды tPoint.MoveTo (то есть класс tCircle будет использовать метод так, как он откомпилирован). В этом случае процедура tCircle.MoveTo будет работать неверно.
Структура вызовов методов при этом будет следующей:

В нашем примере процедуры Show и Hide должны быть объявлены виртуальными, так как только в этом случае класс tCircle может наследовать метод MoveTo у типа tPoint. Это становится возможным потому, что подключение виртуальных методов Show и Hide к процедуре MoveTo будет осуществляться во время выполнения программы, и будут подключаться методы, определенные для типа tCircle (tCircle.Show и tCircle.Hide). Напомним, что подключение виртуальных методов осуществляется с помощью специальной таблицы виртуальных методов (VMT).
При использовании виртуальных методов Show и Hide взаимодействие методов классов tPoint и tCircle можно проиллюстрировать следующей схемой:

Описание классов «Точка» и «Окружность» с использованием виртуальных методов:
// Описание класса tPoint
tPoint = class(TObject)
protected
fX, fY: tCoord;
fColor: Byte;
public
property X: tCoord read fX write fX;
property Y: tCoord read fY write fY;
property Color: Byte read fColor write fColor;
procedure Show; virtual;
procedure Hide; virtual;
procedure MoveTo(NewX, NewY: tCoord);
end;
// Описание класса tCircle
tCircle = class (tPoint)
protected
fRadius: tCoord;
public
property Radius: tCoord read fRadius write fRadius;
procedure Show; override;
procedure Hide; override;
end;
-
Перегрузка методов
В последних версиях Delphi появилась новая разновидность методов – перегружаемые методы. Эту категорию методов нельзя назвать антагонистом двух предыдущих: и статические и динамические методы могут быть перегружаемыми. Перегрузка нужна, чтобы выполнить одинаковые или похожие действия с разнотипными данными. Рассмотрим пример:
type
tClass1 = class
i : Extended;
procedure SetData(AValue : Extended);
end;
tClass2 = class(tClass1)
j : Integer;
procedure SetData(AValue : Integer);
end;
var
Obj1: tClass1;
Obj2: tClass2;
Попытка вызова методов
Obj2.SetData(1.0);
Obj2.SetData(1);
вызовет ошибку компиляции на первой из двух строк. Для компилят5ра внутри Obj2 статический метод с параметром типа Extended перекрыт, и он его не «признает». Объявить методы виртуальными нельзя, так как тип и количество параметров в одноименных виртуальных методах должны совпадать. Чтобы указанные вызовы были верными, необходимо объявить методы перегружаемыми, для чего используется директива overload:
type
tClass1 = class
i : Extended;
procedure SetData(AValue : Extended); overload;
end;
tClass2 = class(tClass1)
j : Integer;
procedure SetData(AValue : Integer); overload;
end;
Объявив метод SetData перегружаемым, в программе можно использовать обе его реализации одновременно. Это возможно потому, что компилятор определяет тип передаваемого параметра (целый или с плавающей точкой) и в зависимости от этого подставляет вызов соответствующего метода.
Можно перегружать и виртуальные методы. В этом случае необходимо добавить директиву reintroduce:
type
tClass1 = class
i : Extended;
procedure SetData(AValue : Extended); overload; virtual;
end;
tClass2 = class(tClass1)
j : Integer;
procedure SetData(AValue : Integer); reintroduce; overload;
end;
На перегрузку методов накладывается ограничение – нельзя перегружать методы, находящиеся в области видимости published. Области видимости обсуждаются далее.
-
Абстрактные методы
Абстрактными называются методы, которые определены в классе, но не содержат никаких действий, никогда не вызываются и обязательно должны быть переопределены в потомках класса. Абстрактными могут быть только виртуальные и динамические методы. В Delphi есть одноименная директива (abstract), указываемая при описании метода:
procedure NeverCallMe; virtual; abstract;
Никакого кода для этого метода писать не нужно. Вызов абстрактного метода приведет к созданию исключительной ситуации EAbstractError.
Пример с классом tField из параграфа 7 «Полиморфизм» поясняет, для чего нужны абстрактные методы. В данном случае класс tField не используется сам по себе; его основное предназначение – быть родоначальником иерархии конкретных классов-«полей» и дать возможность абстрагироваться от частностей. Хотя параметр процедуры ShowData и описан как tField, но если передать в нее объект этого класса, произойдет исключительная ситуация вызова абстрактного метода.
-
Объект изнутри
Рассмотрим пример из параграфа 7 «Полиморфизм»:
tClass1 = class
fField1: Integer;
fField2: Longint;
procedure stMet;
procedure vrMet1; virtual;
procedure vrMet2; virtual;
procedure dnMet1; dynamic;
procedure dnMet2; dynamic;
end;
tClass2 = class(tClass1)
procedure stMet;
procedure vrMet1; override;
procedure dnMet1; override;
end;
var
Obj1: tClass1;
Obj2: tClass2;
Внутренняя структура объектов Obj1 и Obj2 имеет вид:
|
Указатель на объект Obj1 |
|
VMT класса tClass1 |
|
DMT класса tClass1 |
|
|
|
|
|
|
|
У |
|
R |
|
Число динамических методов: 2 |
|
Поле fField1 |
@tClass1.vrMet1 |
Индекс tClass1.dnMet1 (-1) |
||
|
Поле fField2 |
|
@tClass1.vrMet2 |
|
Индекс tClass1.dnMet2 (-2) |
|
|
|
@tObject.Destroy |
|
@tClass1.dnMet1 |
|
|
|
|
|
@tClass1.dnMet2 |
|
|
|
|
|
|
|
У |
|
VMT класса tClass2 |
|
DMT класса tClass2 |
|
|
|
|
|
|
|
Указатель на класс tClass2 |
|
RTTI класса tClass2 |
|
Число динамических методов: 1 |
|
Поле fField1 |
@tClass2.vrMet1 |
Индекс tClass2.dnMet1 (-1) |
||
|
Поле fField2 |
|
@tClass1.vrMet2 |
|
@tClass2.dnMet1 |
|
|
|
@tObject.Destroy |
|
|
Первое поле каждого экземпляра объекта содержит указатель на его класс. Класс как структура состоит из двух частей. Начиная с адреса, на который ссылается указатель на класс, располагается таблица виртуальных методов, содержащая адреса всех виртуальных методов класса, включая унаследованные от предков. Перед таблицей виртуальных методов расположена специальная структура, которая называется информацией о типе времени выполнения (runtime type information, RTTI). В ней содержатся данные, полностью характеризующие класс: его имя, размер экземпляра, указатели на класс-предок, на имя класса и т. д. На рисунке она показана одним блоком.
Одно из полей структуры содержит адрес таблицы динамических методов класса (DMT). Таблица имеет следующий формат – в начале слово, содержащее количество элементов таблицы; затем – слова, соответствующие индексам методов. Нумерация индексов начинается с –1 и идет по убывающей. После индексов идут собственно адреса динамических методов. Обратите внимание, что DMT объекта Obj1 состоит из двух элементов, Obj2 – из одного элемента, соответствующего перекрытому методу dnMet1. В случае вызова метода Obj2.dnMet2 индекс не будет найден в таблице DMT Obj2, поиск продолжится в DMT объекта Obj1. Именно так экономится память при использовании динамических методов.
Напомним, что указатель на класс указывает на адрес первого виртуального метода. Служебные данные размещаются перед таблицей VMT, то есть с отрицательным смещением. Эти смещения описаны в модуле SYSTEM.PAS.
В языке определены два оператора: is и as, неявно обращающиеся к информации о классе.
Оператор is предназначен для проверки совместимости по присваиванию экземпляра объекта с заданным классом. Выражение вида:
AnObject is tObjectType
принимает значение True только в том случае, если объект AnObject совместим по присваиванию с tObjectType, то есть является объектом этого класса или одного из классов, порожденных от него. Кстати, определенная проверка происходит еще при компиляции: если фактические объект и класс несовместимы, компилятор выдаст ошибку в этом операторе.
Оператор as введен в язык для приведения объектных типов. С его помощью можно рассматривать экземпляр объекта как принадлежащий к другому совместимому типу:
with ASomeObject as tAnotherType do ...
Использование оператора as отличается от стандартного способа приведения типов с помощью конструкции tAnotherType(ASomeObject) наличием проверки на совместимость типов во время выполнения (как в операторе is): попытка приведения к несовместимому типу приводит к возникновению исключительной ситуации EInvalidCast. После применения оператора as сам объект остается неизменным, но вызываются только те его методы, которые есть у присваиваемого класса. Присваиваемый фактически тип должен быть известен на стадии компиляции, поэтому на месте tObjectType (после is) и tAnotherType (после as) не может стоять переменная-указатель на класс.
Информация, описывающая класс, создается и размещается в памяти на этапе компиляции. Возникает вопрос: можно ли получить доступ к ней, не создавая экземпляр класса (объект)? Да, можно. Доступ к информации класса вне методов этого класса можно получить, описав соответствующий указатель, который называется указателем на класс, или указателем на объектный тип (class reference). Он описывается при помощи зарезервированных слов class of. Например, указатель на класс tObject описан в модуле SYSTEM.PAS и называется tClass:
type
tObject = class;
tClass = class of tObject;
Указатели на классы подчиняются правилам приведения объектных типов. Указатель на класс-предок может ссылаться и на любые дочерние классы; обратное невозможно.
С указателем на класс тесно связано понятие методов класса. Такие методы можно вызывать без создания экземпляра объекта – с указанием имени класса, в котором они описаны. Перед описанием метода класса нужно поставить зарезервированное слово class:
type
tMyClass = class (tObject)
class function GetSize: string;
end;
var
MyObject: tMyClass;
AString: string;
begin
AString:= tMyClass.GetSize; // обращение к методу класса до создания объекта этого класса
MyObject:= tMyObject.Create;
AString:= MyObject.GetSize;
end.
Разумеется, методы класса не могут использовать значения, содержащиеся в полях: ведь экземпляра-то не существует! Возникает вопрос: для чего тогда нужны такие методы? Важнейшие методы класса определены в самом классе tObject: они позволяют извлечь из внутренней структуры класса практически всю необходимую информацию.
Некоторые важные методы класса tObject
|
Метод |
Описание |
|
class function ClassName: ShortString; |
Возвращает имя класса. |
|
class function ClassParent: tClass; |
Возвращает указатель на родительский класс (для tObject возвращает nil). |
|
class function InstanceSize: Longint; |
Возвращает размер экземпляра класса. |
|
function ClassType: tClass; |
Возвращает указатель на класс данного объекта. |
|
constructor Create; |
Конструктор. Создает новый экземпляр класса. |
|
destructor Destroy; virtual; |
Деструктор. Уничтожает экземпляр класса. |
|
procedure Free; |
Используется вместо деструктора. Проверяет передаваемый деструктору указатель на экземпляр. |
В следующем примере переменная ObjectRef является указателем на класс; он по очереди указывает на tObject и tMyObject. Посредством этой переменной-указателя вызывается функция класса ClassName:
type
tMyObject = class;
tMyObjClass = class of tObject;
var
ObjectRef : tMyObjClass;
s: String;
begin
ObjectRef := tObject;
s := ObjectRef .ClassName; // s :='TObject'
ObjectRef := tMyObject;
s:= ObjectRef .ClassName; // s : ='TMyObject'
end.
Удобно использовать метод Free для уничтожения экземпляра класса, при этом необходимо переопределить метод Destroy, в котором описываются все действия по уничтожению экземпляра, например освобождение выделенной памяти или закрытие файлов. При вызове метода Free проверяется передаваемый указатель на экземпляр, после чего вызывается метод Destroy и происходит уничтожение объекта.

DD
EE
казатель
на класс tClass1
TTI
класса
tClass1
казатель
на объект Obj2