Вы находитесь на страницах старой версии сайта.
Переходите на новую версию Interface.Ru

предыдущая статья серии

Программирование на языке Delphi
Глава 2. Основы языка Delphi. Часть 2

© А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четырько
Статья была опубликована на сайте rsdn.ru

Операторы

Общие положения

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

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

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

Оператор присваивания

Оператор присваивания (:=) вычисляет выражение, заданное в его правой части, и присваивает результат переменной, идентификатор которой расположен в левой части. Например:

X := 4;
Y := 6;
Z := (X + Y) / 2;

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

В общем случае для числовых типов данных действует следующее правило: выражение с более узким диапазоном возможных значений можно присвоить переменной с более широким диапазоном значений. Например, выражение с типом данных Byte можно присвоить переменной с типом данных Integer, а выражение с типом данных Integer можно присвоить переменной с типом данных Real. В таких случаях преобразование данных из одного представления в другое выполняется автоматически:

var
  B: Byte;
  I: Integer;
  R: Real;
begin
  B := 255;
  I := B + 1;    // I = 256
  R := I + 0.1;  // R = 256.1
  I := R;        // Ошибка! Типы данных несовместимы по присваиванию
end.

Исключение составляет случай, когда выражение принадлежит 32-разрядному целочисленному типу данных (например, Integer), а переменная — 64-разрядному целочисленному типу данных Int64. Для того, чтобы на 32-разрядных процессорах семейства x86 вычисление выражения происходило правильно, необходимо выполнить явное преобразование одного из операндов выражения к типу данных Int64. Следующий пример поясняет сказанное:

var
  I: Integer;
  J: Int64;
begin
  I := MaxInt;        // I =  2147483647 (максимальное целое)
  J := I + 1;         // J = -2147483648 (неправильно: ошибка переполнения!)
  J := Int64(I) + 1;  // J =  2147483648 (правильно: вычисления в формате Int64)
end.

Оператор вызова процедуры

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

Writeln('Hello!'); // Вызов стандартной процедуры вывода данных
MyProc;            // Вызов процедуры, определенной программистом

Составной оператор

Составной оператор представляет собой группу из произвольного числа операторов, отделенных друг от друга точкой с запятой и заключенную в так называемые операторные скобки — begin и end:

begin
  <оператор 1>;
  <оператор 2>;
  …
  <оператор N>
end

Частным случаем составного оператора является тело следующей программы:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  X, Y: Integer;

begin
  X := 4;
  Y := 6;
  Writeln(X + Y);
  Writeln('Press Enter to exit...');
  Readln; // Точка с запятой после этого оператора не обязательна
end.

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

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

Оператор ветвления if

Оператор ветвления if — одно из самых популярных средств, изменяющих естественный порядок выполнения операторов программы. Вот его общий вид:

if <условие> then
  <оператор 1>
else
  <оператор 2>;

Условие — это выражение булевского типа, оно может быть простым или сложным. Сложные условия образуются с помощью логических операций и операций отношения. Обратите внимание, что перед словом else точка с запятой не ставится.

Логика работы оператора if очевидна: выполнить оператор 1, если условие истинно, и оператор 2, если условие ложно. Поясним сказанное на примере:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  A, B, C: Integer;

begin
  A := 2; 
  B := 8;
  if A > B then
    C := A
  else
    C := B;
  Writeln('C=', C);
  Writeln('Press Enter to exit...');
  Readln;
end.

В данном случае значение выражения А > В ложно, следовательно на экране появится сообщение C=8.

У оператора if существует и другая форма, в которой else отсутствует:

if <условие> then <оператор>;

Логика работы этого оператора if еще проще: выполнить оператор, если условие истинно, и пропустить оператор, если оно ложно. Поясним сказанное на примере:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  A, B, C: Integer;

begin
  A := 2;
  B := 8; 
  C := 0;
  if A > B then C := A + B;
  Writeln('C=', C);
  Writeln('Press Enter to exit...');
  Readln;
end.

В результате на экране появится сообщение С=0, поскольку выражение А > В ложно и присваивание С := А + В пропускается.

Один оператор if может входить в состав другого оператора if. В таком случае говорят о вложенности операторов. При вложенности операторов каждое else соответствует тому then, которое непосредственно ему предшествует. Например

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  A: Integer;

begin
  Readln(A);
  if A >= 0 then
    if A <= 100 then 
      Writeln('A попадает в диапазон 0 - 100.')
    else 
      Writeln('A больше 100.')
  else
    Writeln('A меньше 0.');
  Writeln('Press Enter to exit...');
  Readln;
end.

Конструкций со степенью вложенности более 2–3 лучше избегать из-за сложности их анализа при отладке программ.

Оператор ветвления case

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

case <переключатель> of
  <список №1 значений переключателя>: <оператор 1>;
  <список №2 значений переключателя>: <оператор 2>;
      ...
  <список №N значений переключателя>: <оператор N>;
  else <оператор N+1>
end;

Оператор case вычисляет значение переключателя (который может быть задан выражением), затем последовательно просматривает списки его допустимых значений в поисках вычисленного значения и, если это значение найдено, выполняет соответствующий ему оператор. Если переключатель не попадает ни в один из списков, выполняется оператор, стоящий за словом else. Если часть else отсутствует, управление передается следующему за словом end оператору.

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

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

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  Day: 1..31;

begin
  Readln(Day);
  case Day of
    20..31: Writeln('День попадает в диапазон 20 - 31.');
    1, 5..10: Writeln('День попадает в диапазон 1, 5 - 10.');
    else Writeln('День не попадает в заданные диапазоны.');
  end;
  Writeln('Press Enter to exit...');
  Readln;
end.

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

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  Day: 1..31;

begin
  Readln(Day);
  case Day of
    1, 5..10: Writeln('День попадает в диапазон 1, 5 - 10.');
    20..31: Writeln('День попадает в диапазон 20 - 31.');
    else Writeln('День не попадает в заданные диапазоны.');
  end;
  Writeln('Press Enter to exit...');
  Readln;
end.

Операторы повтора — циклы

Алгоритм решения многих задач требует многократного повторения одних и тех же действий. При этом суть действий остается прежней, но меняются данные. С помощью рассмотренных выше операторов трудно представить в компактном виде подобные действия в программе. Для многократного (циклического) выполнения одних и тех же действий предназначены операторы повтора (циклы). К ним относятся операторы for, while и repeat. Все они используются для организации циклов разного вида.

Любой оператор повтора состоит из условия повтора и повторяемого оператора (тела цикла). Тело цикла представляет собой простой или структурный оператор. Оно выполняется столько раз, сколько предписывает условие повтора. Различие среди операторов повтора связано с различными способами записи условия повтора.

Оператор повтора for

Оператор повтора for используется в том случае, если заранее известно количество повторений цикла. Приведем наиболее распространенную его форму:

for <параметр цикла> := <значение 1> to <значение 2> do
  <оператор>;

где <параметр цикла> — это переменная любого порядкового типа данных (переменные вещественных типов данных недопустимы); <значение 1> и <значение 2> — выражения, определяющие соответственно начальное и конечное значения параметра цикла (они вычисляются только один раз перед началом работы цикла); <оператор> — тело цикла.

Оператор for обеспечивает выполнение тела цикла до тех пор, пока не будут перебраны все значения параметра цикла от начального до конечного. После каждого повтора значение параметра цикла увеличивается на единицу. Например, в результате выполнения следующей программы на экран будут выведены все значения параметра цикла (от 1 до 10), причем каждое значение — в отдельной строке:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  I: Integer;

begin
  for I := 1 to 10 do Writeln(I);
  Writeln('Press Enter to exit...');
  Readln;
end.

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

В качестве начального и конечного значений параметра цикла могут использоваться выражения. Они вычисляются только один раз перед началом выполнения оператора for. В этом состоит важная особенность цикла for в языке Delphi, которую следует учитывать тем, кто имеет опыт программирования на языках C/C++.

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

Вторая форма записи оператора for обеспечивает перебор значений параметра цикла не по возрастанию, а по убыванию:

for <параметр цикла> := <значение 1> downto <значение 2> do
  <оператор>;

Например, в результате выполнения следующей программы на экран будут выведены значения параметра цикла в порядке убывания (от 10 до 1):

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  I: Integer;

begin
  for I := 10 downto 1 do Writeln(I);
  Writeln('Press Enter to exit...');
  Readln;
end.

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

Оператор повтора repeat

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

repeat
  <оператор 1>;
      ...
  <оператор N>;
until <условие завершения цикла>;

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

В следующем примере показано, как оператор repeat применяется для суммирования вводимых с клавиатуры чисел. Суммирование прекращается, когда пользователь вводит число 0:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  S, X: Integer;

begin
  S := 0;
  repeat
    Readln(X);
    S := S + X;
  until X = 0;
  Writeln('S=', S);
  Writeln('Press Enter to exit...');
  Readln;
end.

Часто бывает, что условие выполнения цикла нужно проверять перед каждым повторением тела цикла. В этом случае применяется оператор while, который, в отличие от оператора repeat, содержит условие выполнения цикла, а не условие завершения.

Оператор повтора while

Оператор повтора while имеет следующий формат:

while <условие> do
  <оператор>;

Перед каждым выполнением тела цикла происходит проверка условия. Если оно истинно, цикл выполняется и условие вычисляется заново; если оно ложно, происходит выход из цикла, т.е. переход к следующему за циклом оператору. Если первоначально условие ложно, то тело цикла не выполняется ни разу. Следующий пример показывает использование оператора while для вычисления суммы S = 1 + 2 + .. + N, где число N задается пользователем с клавиатуры:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  S, N: Integer;

begin
  Readln(N);
  S := 0;
  while N > 0 do
  begin
    S := S + N;
    N := N - 1;
  end;
  Writeln('S=', S);
  Writeln('Press Enter to exit...');
  Readln;
end.

Прямая передача управления в операторах повтора

Для управления работой операторов повтора используются специальные процедуры-операторы Continue и Break, которые можно вызывать только в теле цикла.

Процедура-оператор Continue немедленно передает управление оператору проверки условия, пропуская оставшуюся часть цикла (рисунок 4):

Рисунок 4. Схема работы процедуры-оператора Continue

Рисунок 4. Схема работы процедуры-оператора Continue

Процедура-оператор Break прерывает выполнение цикла и передает управление первому оператору, расположенному за блоком цикла (рисунок 5):

Рисунок 5. Схема работы процедуры-оператора Break

Рисунок 5. Схема работы процедуры-оператора Break

Оператор безусловного перехода

Среди операторов языка Delphi существует один редкий оператор, о котором авторы сперва хотели умолчать, но так и не решились. Это оператор безусловного перехода goto ("перейти к"). Он задумывался для того случая, когда после выполнения некоторого оператора надо выполнить не следующий по порядку, а какой-либо другой, отмеченный меткой, оператор.

Метка — это именованная точка в программе, в которую можно передать управление. Перед употреблением метка должна быть описана. Раздел описания меток начинается зарезервированным словом label, за которым следуют имена меток, разделенные запятыми. За последним именем ставится точка с запятой. Типичный пример описания меток:

label
  Label1, Label2;

В разделе операторов метка записывается с двоеточием. Переход на метку выполняется с помощью зарезервированного слова goto, за которым следует имя метки:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

label
  M1, M2;

begin
  M1:
  Write('Желаем успеха ');
  goto M2;
  Write('А этого сообщения вы никогда не увидите!');
  M2:
  goto M1;
  Writeln('в освоении среды Delphi!');
  Writeln('Press Enter to exit...');
  Readln;
end.

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

Внимание! В соответствии с правилами структурного программирования следует избегать применения оператора goto, поскольку он усложняет понимание логики программы. Оператор goto использовался на заре программирования, когда выразительные возможности языков были скудными. В языке Delphi без него можно успешно обойтись, применяя условные операторы, операторы повтора, процедуры Break и Continue, операторы обработки исключений (последние описаны в главе 4).

Подпрограммы

Общие положения

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

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

Процедура — это подпрограмма, имя которой не может использоваться в выражениях в качестве операнда. Процедура состоит из заголовка и тела. По структуре ее можно рассматривать как программу в миниатюре. Когда процедура описана, ее можно вызвать по имени из любой точки программы (в том числе из нее самой!). Когда процедура выполнит свою задачу, программа продолжится с оператора, следующего непосредственно за оператором вызова процедуры. Использование имени процедуры в программе называется оператором вызова процедуры.

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

Все процедуры и функции языка Delphi подразделяются на две группы: встроенные и определенные программистом.

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

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

Стандартные подпрограммы

Арифметические функции

Abs(X)Возвращает абсолютное значение аргумента X.
Exp(X)Возвращает значение ex.
Ln(X)Возвращает натуральный логарифм аргумента X.
PiВозвращает значение числа ?.
Sqr(X)Возвращает квадрат аргумента X.
Sqrt(X)Возвращает квадратный корень аргумента X.

Примеры:

Выражение

Результат

Abs(–4)4
Exp(1)2.17828182845905
Ln(Exp(1))1
Pi3.14159265358979
Sqr(5)25
Sqrt(25)5

Тригонометрические функции

ArcTan(X)Возвращает угол, тангенс которого равен X.
Cos(X)Возвращает косинус аргумента X (X задается в радианах).
Sin(X)Возвращает синус аргумента X (X задается в радианах).

Примеры:

Выражение

Результат

ArcTan(Sqrt(3))1.04719755119660
Cos(Pi/3)0.5
Sin(Pi/6)0.5

Заметим, что в состав среды Delphi входит стандартный модуль Math, который содержит высокопроизводительные подпрограммы для тригонометрических, логорифмических, статистических и финансовых вычислений.

Функции выделения целой или дробной части

Frac(X)Возвращает дробную часть аргумента X.
Int(X)Возвращает целую часть вещественного числа X. Результат принадлежит вещественному типу.
Round(X)Округляет вещественное число X до целого.
Trunc(X)Возвращает целую часть вещественного числа X. Результат принадлежит целому типу.

Примеры:

Выражение

Результат

Frac(2.5)0.5
Int(2.5)2.0
Round(2.5)3
Trunc(2.5)2

Функции генерации случайных чисел

RandomВозвращает случайное вещественное число в диапазоне 0 ? X < 1.
Random(I)Возвращает случайное целое число в диапазоне 0 ? X < I.
RandomizeЗаново инициализирует встроенный генератор случайных чисел новым значением, полученным от системного таймера.

Подпрограммы для работы с порядковыми величинами

Chr(X)Возвращает символ, порядковый номер которого равен X.
Dec(X, [N])Уменьшает целую переменную X на 1 или на заданное число N.
Inc(X, [N])Увеличивает целую переменную X на 1 или на заданное число N.
Odd(X)Возвращает True, если аргумент X является нечетным числом.
Ord(X)Возвращает порядковый номер аргумента X в своем диапазоне значений.
Pred(X)Возвращает значение, предшествующее значению аргумента X в своем диапазоне.
Succ(X)Возвращает значение, следующее за значением аргумента X в своем диапазоне.

Примеры:

Выражение

Результат

Chr(65)'A'
Odd(3)True
Ord('A')65
Pred('B')'A'
Succ('A')'B'

Подпрограммы для работы с датой и временем

DateВозвращает текущую дату в формате TDateTime.
TimeВозвращает текущее время в формате TDateTime.
NowВозвращает текущие дату и время в формате TDateTime.
DayOfWeek(D)Возвращает день недели по дате в формате TDateTime.
DecodeDate(...)Разбивает значение даты на год, месяц и день.
DecodeTime(...)Разбивает значение времени на час, минуты, секунды и милисекунды.
EncodeDate(...)Формирует значение даты по году, месяцу и дню.
EncodeTime(...)Формирует значение времени по часу, минутам, секундам и милисекундам.

Процедуры передачи управления

BreakПрерывает выполнение цикла.
ContinueНачинает новое повторение цикла.
ExitПрерывает выполнение текущего блока.
HaltОстанавливает выполнение программы и возвращает управление операционной системе.
RunErrorОстанавливает выполнение программы, генерируя ошибку времени выполнения.

Разные процедуры и функции

FillChar(...)Заполняет непрерывную область символьным или байтовым значением.
Hi(X)Возвращает старший байт аргумента X.
High(X)Возвращает самое старшее значение в диапазоне аргумента X.
Lo(X)Возвращает младший байт аргумента X.
Low(X)Возвращает самое младшее значение в диапазоне аргумента X.
Move(...)Копирует заданное количество байт из одной переменной в другую.
ParamCountВозвращает количество параметров, переданных программе в командной строке.
ParamStr(X)Возвращает параметр командной строки по его номеру.
SizeOf(X)Возвращает количество байт, занимаемое аргументом X в памяти. Функция SizeOf особенно нужна для определения размеров переменных обощенных типов данных, поскольку представление обощенных типов данных в памяти может изменяться от одной версии среды Delphi к другой. Рекомендуем всегда использовать эту функцию для определения размера переменных любых типов данных; это считается хорошим стилем программирования.
Swap(X)Меняет местами значения старшего и младшего байтов аргумента.
UpCase(C)Возвращает символ C, преобразованный к верхнему регистру.

Примеры:

Выражение

Результат

Hi($F00F)$F0
Lo($F00F)$0F
High(Integer)32767
Low(Integer)–32768
SizeOf(Integer)2
Swap($F00F)$0FF0
UpCase('a')'A'

Процедуры программиста

Очевидно, что встроенных процедур и функций для решения большинства прикладных задач недостаточно, поэтому приходиться придумывать собственные процедуры и функции. По своей структуре они очень напоминают программу и состоят из заголовка и блока. Заголовок процедуры состоит из зарезервированного слова procedure, имени процедуры и необязательного заключенного в круглые скобки списка формальных параметров. Имя процедуры — это идентификатор, уникальный в пределах программы. Формальные параметры — это данные, которые вы передаете в процедуру для обработки, и данные, которые процедура возвращает (подробно параметры описаны ниже). Если процедура не получает данных извне и ничего не возвращает, формальные параметры (в том числе круглые скобки) не записываются. Тело процедуры представляет собой локальный блок, по структуре аналогичный программе:

procedure <имя процедуры> ( <список формальных параметров> ) ;
const ...;
type  ...;
var   ...;
begin
  <операторы>
end;

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

Вызов процедуры для выполнения осуществляется по ее имени, за которым в круглых скобках следует список фактических параметров, т.е. передаваемых в процедуру данных:

<имя процедуры> ( <список фактических параметров> );

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

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

Приведем пример небольшой программы, использующей процедуру Power для вычисления числа X в степени Y. Результат вычисления процедура Power заносит в глобальную переменную Z.

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  Z: Double;

procedure Power(X, Y: Double); // X и Y - формальные параметры
begin
  Z := Exp(Y * Ln(X));
end;

begin
  Power(2, 3);                 // 2 и 3 - фактические параметры
  Writeln('2 в степени 3 = ', Z);
  Writeln('Press Enter to exit...');
  Readln;
end.

Функции программиста

Функции программиста применяются в тех случаях, когда надо создать подпрограмму, участвующую в выражении как операнд. Как и процедура, функция состоит из заголовка и блока. Заголовок функции состоит из зарезервированного слова function, имени функции, необязательного заключенного в круглые скобки списка формальных параметров и типа возвращаемого функцией значения. Функции возвращают значения любых типов данных кроме Text и file of (см. файлы). Тело функции представляет собой локальный блок, по структуре аналогичный программе.

function <имя функции> ( <список формальных параметров> ): <тип результата>;
const ...;
type  ...;
var   ...;
begin
  <операторы>
end;

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

В качестве примера заменим явно неуклюжую процедуру Power (см. выше) на функцию с таким же именем:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

function Power(X, Y: Double): Double;       // X и Y - формальные параметры
begin
  Result := Exp(Y * Ln(X));
end;

begin
  Writeln('2 в степени 3 = ', Power(2, 3)); // 2 и 3 - фактические параметры
  Writeln('Press Enter to exit...');
  Readln;
end.

Параметры процедур и функций

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

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

Входные параметры объявляются с помощью ключевого слова const; их значения не могут быть изменены внутри подпрограммы:

function Min(const A, B: Integer): Integer;
begin
  if A < B then Result := A 
  else Result := B;
end;

Для объявления выходных параметров служит ключевое слово out:

procedure GetScreenResolution(out Width, Height: Integer);
begin
  Width := GetScreenWidth;
  Height := GetScreenHeight;
end;

Установка значений выходных параметров внутри подпрограммы приводит к установке значений переменных, переданных в качестве аргументов:

var
  W, H: Integer;
begin
  GetScreenResolution(W, H);
  ...
end;

После вызова процедуры GetScreenResolution переменные W и H будут содержать значения, которые были присвоены формальным параметрам Width и Height соответственно.

Если параметр является одновременно и входным, и выходным, то он описывается с ключевым словом var:

procedure Exchange(var A, B: Integer);
var
  C: Integer;
begin
  C := A;
  A := B;
  B := C;
end;

Изменение значений var-параметров внутри подпрограммы приводит к изменению значений переменных, переданных в качестве аргументов:

var
  X, Y: Integer;
begin
  X := 5;
  Y := 10;
  ...
  Exchange(X, Y);
  // Теперь X = 10, Y = 5
  ...
end;

При вызове подпрограмм на место out- и var-параметров можно подставлять только переменные, но не константы и не выражения.

Если при описании параметра не указано ни одно из ключевых слов const, out, или var, то параметр считается входным, его можно изменять, но все изменения не влияют на фактический аргумент, поскольку они выполняются с копией аргумента, создаваемой на время работы подпрограммы. При вызове подпрограммы на месте такого параметра можно использовать константы и выражения. Пример подпрограммы:

function NumberOfSetBits(A: Cardinal): Byte;
begin
  Result := 0;
  while A <> 0 do
  begin
    Result := Result + (A mod 2); 
    A := A div 2;
  end;
end;

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

Разные способы передачи параметров (const, out, var и без них) можно совмещать в одной подпрограмме. В следующем законченном примере процедура Average принимает четыре параметра. Первые два (X и Y) являются входными и служат для передачи исходных данных. Вторые два параметра являются выходными и служат для приема в вызывающей программе результатов вычисления среднего арифметического (M) и среднего геометрического (P) от значений X и Y:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

procedure Average(const X, Y: Double; out M, P: Double);
begin
  M := (X + Y) / 2;
  P := Sqrt(X * Y);
end;

var
  M, P: Double;

begin
  Average(10, 20, M, P);
  Writeln('Среднее арифметическое = ', M);
  Writeln('Среднее геометрическое = ', P);
  Writeln('Press Enter to exit...');
  Readln;
end.

Существует разновидность параметров без типа. Они называются нетипизированными и предназначены для передачи и для приема данных любого типа. Нетипизированные параметры описываются с помощью ключевых слов const и var, при этом тип данных опускается:

procedure JustProc(const X; var Y; out Z);

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

Передача фактических аргументов в подпрограмму осуществляется через специальную область памяти — стек. В стек помещается либо значение передаваемого аргумента (передача значения), либо адрес аргумента (передача ссылки на значение). Конкретный способ передачи выбирается компилятором в зависимости от того, как объявлен параметр в заголовке подпрограммы. Связь между объявлением параметра и способом его передачи поясняет таблица 10:

Ключевое слово

Назначение

Способ передачи

<отсутствует>ВходнойПередается копия значения
constВходнойПередается копия значения либо ссылка на значение в зависимости от типа данных
outВыходнойПередается ссылка на значение
varВходной и выходнойПередается ссылка на значение

Таблица 10. Способы передачи параметров

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

Опущенные параметры процедур и функций

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

procedure Initialize(var X; MemSize: Integer; InitValue: Byte = 0);

Для параметра InitValue задано стандартное значение, поэтому его можно опустить при вызове процедуры Initialize:

Initialize(MyVar, 10); // Эквивалентно Initialize(MyVar, 10, 0);

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

procedure Initialize(var X; InitValue: Byte = 0; MemSize: Integer); // Ошибка!

Перегрузка процедур и функций

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

procedure IncrementInteger(var Value: Integer);
procedure IncrementReal(var Value: Real);

В языке Delphi существует возможность дать двум и более процедурам (функциям) одинаковые идентификаторы при условии, что все такие процедуры (функции) отличаются списком параметров. Такая возможность называется перегрузкой. Для указания того, что процедура (функция) перегружена, служит стандартная директива overload. С ее помощью вышеприведенный пример можно переписать следующим образом:

procedure Increment(var Value: Integer); overload; // процедура 1
procedure Increment(var Value: Real); overload;    // процедура 2

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

var
  X: Integer;
  Y: Real;
begin
  X:=1;
  Y:=2.0;
  Increment(X); // Вызывается процедура 1
  Increment(Y); // Вызывается процедура 2
end.

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

procedure Print(X: Shortint); overload; // процедура 1
procedure Print(X: Longint); overload;  // процедура 2

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

Print(5);    // Вызывается процедура 1
Print(150);  // Вызывается процедура 2
Print(-500); // Вызывается процедура 2
Print(-1);   // Вызывается процедура 1

Очевидно, что одно и то же число может интерпретироваться и как Longint, и как Shortint (например, числа 5 и –1). Логика компилятора в таких случаях такова: если значение фактического параметра попадает в диапазон значений нескольких типов, по которым происходит перегрузка, то компилятор выбирает процеудуру (функцию), у которой тип параметра имеет меньший диапазон значений. Например, вызов Print(5) будет означать вызов того варианта процедуры, который имеет тип параметра Shortint. А вот вызов Print(150) будет означать вызов того варианта процедуры, который имеет тип параметра Longint, т.к. число 150 не вмещается в диапазон значений типа данных Shortint.

Поскольку в нынешней версии среды Delphi обощенный тип данных Integer совпадает с фундаментальным типом данных Longint, следующий вариант перегрузки является ошибочным:

procedure Print(X: Integer); overload;
procedure Print(X: Longint); overload; // Ошибка!

Такая же ошибка возникает при использовании пользовательских типов данных, определенных через общий базовый тип.

type
  TMyInteger = Integer;

procedure Print(X: Integer); overload;
procedure Print(X: TMyInteger); overload; // Ошибка!

Что делать в тех случаях, когда такая перегрузка просто необходима? Для этого пользовательский тип данных необходимо создавать с использованием ключевого слова type:

type
  TMyInteger = type Integer;

procedure Print(X: Integer); overload;
procedure Print(X: TMyInteger); overload; // Правильно

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

procedure Increment(var Value: Real; Delta: Real = 1.0); overload; // процедура 1
procedure Increment(var Value: Real); overload;                    // процедура 2

Вызов процедуры Increment с одним параметром вызовет неоднозначность:

var
  X: Real;
begin
  Increment(X, 10); // Вызывается процедура 1
  Increment(X);     // Ошибка! Неоднозначность
end.

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

function SquareRoot(X: Integer): Single; overload;
function SquareRoot(X: Integer): Double; overload; // Ошибка!

Соглашения о вызове подпрограмм

В различных языках программирования используются различные правила вызова подпрограмм. Для того чтобы из программ, написанных на языке Delphi, возможно было вызывать подпрограммы, написанные на других языках (и наоборот), в языке Delphi существуют директивы, соответствующие четырем известным соглашениям о вызове подпрограмм: register, stdcall, pascal, cdecl.

Директива, определяющая правила вызова, помещается в заголовок подпрограммы, например:

procedure Proc; register;
function Func(X: Integer): Boolean; stdcall;

Директива register задействует регистры процессора для передачи параметров и поэтому обеспечивает наиболее эффективный способ вызова подпрограмм. Эта директива применяется по умолчанию. Директива stdcall используется для вызова стандартных подпрограмм операционной системы. Директивы pascal и cdecl используются для вызова подпрограмм, написанных на языках Delphi и C/C++ соответственно.

Рекурсивные подпрограммы

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

Приведенная ниже программа содержит функцию Factorial для вычисления факториала. Напомним, что факториал числа определяется через произведение всех натуральных чисел, меньших либо равных данному (факториал числа 0 принимается равным 1):

X! = 1 * 2 * ... * (X – 2) * (X – 1) * X

Из определения следует, что факториал числа X равен факториалу числа (X – 1), умноженному на X. Математическая запись этого утверждения выглядит так:

X! = (X – 1)! * X

, где 0! = 1

Последняя формула используется в функции Factorial для вычисления факториала:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

function Factorial(X: Integer): Longint;
begin
  if X = 0  then // Условие завершения рекурсии
    Factorial := 1
  else
    Factorial := Factorial(X - 1) * X;
end;

begin
  Writeln('4! = ', Factorial(4)); // 4! = 1 * 2 * 3 * 4 = 24
  Writeln('Press Enter to exit...');
  Readln;
end.

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

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

Упреждающее объявление процедур и функций

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

procedure Proc; forward;
function Func(X: Integer): Boolean; forward;

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

procedure Proc2(<формальные параметры>); forward;

procedure Proc1;
begin
  ...
  Proc2(<фактические параметры>);
  ...
end;

procedure Proc2; // Список формальных параметров опущен
begin
  ...
  Proc1;
  ...
end;

begin
  ...
  Proc1;
  ...
end.

Процедурные типы данных

Наряду с уже известными типами данных в языке Delphi введен так называемый процедурный тип, с помощью которого обычные процедуры и функции можно интерпретировать как некоторую разновидность переменных. Определение процедурного типа состоит из зарезервированного слова procedure или function, за которым следует полное описание параметров. Для функции дополнительно указывается тип результата. Символические имена параметров никакой роли не играют, поскольку нигде не используются.

type
  TProc = procedure (X, Y: Integer);
  TFunc = function (X, Y: Integer): Boolean;

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

var
  P: TProc;
  F: TFunc;

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

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

function Power(X, Y: Double): Double;
begin
  Result := Exp(Y * Ln(X));
end;

type
  TFunc = function (X, Y: Double): Double;

var
  F: TFunc;

begin
  F := Power; // В переменную F заносится адрес функции Power
  Writeln('2 power 4 = ', F(2, 4)); // Вызов Power посредством F
  Writeln('Press Enter to exit...');
  Readln;
end.

следующая статья серии

Дополнительная информация

За дополнительной информацией обращайтесь в компанию Interface Ltd.

Обсудить на форуме Borland

Рекомендовать страницу

INTERFACE Ltd.
Телефон/Факс: +7 (495) 925-0049
Отправить E-Mail
http://www.interface.ru
Rambler's Top100
Ваши замечания и предложения отправляйте редактору
По техническим вопросам обращайтесь к вебмастеру
Дата публикации: 10.02.06