Версия для печати

Урок 17. Циклы - цикл с предусловием и цикл с постусловием

Оцените материал
(20 голосов)

Циклы - цикл с предусловием и цикл с постусловием

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

Задача

Определить количество натуральных чисел, рассматривая их в порядке возрастания, сумма кубов которых не превышает 50000. Т.е. мы должны последовательно суммировать кубы чисел 1, 2, 3, ..., и делать это до тех пор, пока сумма не достигнет 50000, а в результате должны узнать, сколько чисел было пройдено.

Понятно, что решить задачу лучше использованием цикла. Но будет ли решение с циклом FOR оптимальным? Конечно, можно задать диапазон пробегаемых значений от 1 до 50000 - количество чисел точно будет найдено (очевидно, это кол-во будет даже менее 50, ведь 50^3 >> 50000). Но в этом случае придётся ставить дополнительное условие на значение суммы и выполнение команды Break.

Есть способ проще!

Цикл WHILE - цикл с предусловием

Цикл WHILE (англ. "пока") - цикл, в котором условие находится перед телом цикла, а сам цикл выполняется до тех пор, пока условие не станет ложным.

Общий вид:

WHILE {условие} DO
  {действия}

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

Особенностью цикла с предусловием является то, что он может не выполниться ни разу - это произойдёт, если указанное условие изначально будет ложным. При этом, цикл может и стать "вечным" - если условие никогда не примет значения False. Именно поэтому следует следить за тем, чтобы всегда присутствовали условия для завершения работы цикла.

Решение задачи с помощью цикла WHILE

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

procedure TForm1.Button1Click(Sender: TObject);
var s,i: integer;
begin
  i:=0;
  s:=0;
  while s < 50000 do
  begin
    Inc(i);
    Inc(s,i*i*i);
  end;
  Label1.Caption:=IntToStr(i-1)
end;

В данном случае процесс "повешен" на кнопку (Button1), а результат выводится в текстовую метку (Label1). Вы спросите, почему выводится i-1, а не само число i ? Всё просто. Ведь переменная-счётчик увеличивается после того, как проверяется условие цикла, а значит и результат получится на единицу больше. Удостовериться в этом можно, добавив в тело цикла вывод промежуточных результатов (в примере - в текстовое поле Memo1):

Memo1.Lines.Add(IntToStr(s)+' : '+IntToStr(i));

Цикл REPEAT - цикл с постусловием

Завершает тройку циклов цикл с постусловием - REPEAT (англ. "повтор"). Примечательно, что этого цикла во многих языках программирования нет - есть только FOR и WHILE. Между тем, цикл с постусловием очень удобен.

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

Общий вид:

REPEAT
  {действия}
UNTIL {условие выхода из цикла};

Есть несколько моментов, на которые стоит обратить внимание. Во-первых, в качестве условия задаётся уже условие выхода из цикла, в то время как в цикле WHILE задаётся условие продолжения цикла. Во-вторых, при наличии нескольких команд, которые помещаются в тело цикла, заключать их в блок BEGIN .. END не нужно - зарезервированные слова REPEAT .. UNTIL сами составляют аналогичный блок.

Цикл с постусловием, в отличие от цикла с предусловием, всегда выполняется хотя бы один раз! Но, как и цикл WHILE, при неверно написанном условии цикл станет "вечным".

Решение задачи с помощью цикла REPEAT

Решение нашей задачи практически не изменится - всё останется, только условие будет стоять в конце, а само условие изменится на противоположное:

procedure TForm1.Button2Click(Sender: TObject);
var s,i: integer;
begin
  i:=0;
  s:=0;
  repeat
    Inc(i);
    Inc(s,i*i*i);
  until s >= 50000;
  Label1.Caption:=IntToStr(i-1);
end;

Команды Break и Continue

Команда Break, выполняющая досрочный выход из цикла, работает не только в цикле FOR, но и в циклах WHILE и REPEAT. Аналогично, команда Continue, немедленно запускающая следующую итерацию цикла, может использоваться в циклах WHILE и REPEAT.

Как остановить цикл?

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

Задание: при нажатии кнопки "Старт" программа начинает генерировать случайные комбинации из латинских букв (верхнего регистра) длиной 10 символов и все эти комбинации отображаются в текстовом поле TMemo. При нажатии кнопки "Стоп" генерация должна быть прекращена.

Каким образом решить поставленную задачу? Сначала сделаем акцент на механизме остановки. Понятно, что в данном случае нужно задать вечный цикл, да-да, самый настоящий, но каким образом предусмотреть его остановку? Очевидно, что выбор падает либо на WHILE, либо на REPEAT. Остановить цикл можно либо изменив значение логического выражения, заданного для цикла, либо вызвав команду Break. Вопрос стоит теперь в том, как при нажатии кнопки "Стоп" изменить условие цикла, описанного в обработчике кнопки "Старт". Ответ прост - использовать ту память, которая доступна обработчикам обеих кнопок. Итак, заведём глобальную переменную, которая будет видна во всей программе. Глобальные переменные описываются над словом implementation модуля, там, где идёт описание формы:

var
  Form1: TForm1;
  Stop: Boolean = False;

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

На форме разместим Memo1 (TMemo) и 2 кнопки: Button1 ("Старт"), Button2 ("Стоп"). У кнопки "Стоп" поставим Enabled = False, т.е. выключим её.

Сразу приведу обработчики обеих кнопок, после чего подробно разберём их работу:

procedure TForm1.Button1Click(Sender: TObject);

var s: string; c: byte;
begin
  Button2.Enabled:=True;
  Button1.Enabled:=False;
  Stop:=False;
  while not(Stop) do
  begin
    s:='';
    for c := 1 to 10 do
      s:=s+Chr(Random(Ord('Z')-Ord('A')+1)+Ord('A'));
    Memo1.Lines.Add(s);
    Application.ProcessMessages
  end
end;
 
procedure TForm1.Button2Click(Sender: TObject);
begin
  Stop:=True;
  Button2.Enabled:=False;
  Button1.Enabled:=True;
end;

Начнём с кнопки "Стоп" (Button2). При её нажатии:

1) Значение переменной Stop устанавливается в True, т.е. мы подаём сигнал, что нужно остановиться;

2) Кнопку "Стоп" мы снова выключаем;

3) Кнопку "Старт" - наоборот, включаем.

Теперь кнопка "Старт" (Button1):

1) Кнопка "Стоп" включается, кнопка "Старт" выключается;

2) Переменной Stop присваивается значение False (если этого не сделать, то запустить процесс генерации второй раз будет невозможно);

3) Цикл с генерацией строки с условием на переменную Stop - цикл будет работать до тех пор, пока переменная Stop имеет значение False. Как только значение станет True, цикл сам завершит свою работу.

А теперь более подробно о том, что происходит в теле цикла. Начнём с генерации строки символов. Напомню, что строки можно складывать. Если сложить строку 'A' со строкой 'B', то получится строка 'AB'. Именно этот приём здесь и использован. Сначала мы делаем строку пустой, а затем последовательно добавляем в неё 10 произвольных символов. Как происходит добавление... Ну естественно с помощью цикла на 10 итераций. А вот выбор случайной из латинских букв не совсем прост и не для всех очевиден. Конечно, можно было заранее записать все буквы куда-либо (например, в массив, или в другую строковую переменную), а затем брать их оттуда. Но это не совсем хорошо, ведь можно сделать гораздо проще. В данном случае решающим фактором является то, что латинские буквы в кодовой таблице символов идут по порядку. Т.е. 'A' имеет некоторый код n, 'B' имеет код n+1 и т.д. - весь алфавит идёт последовательно, без разрывов. Убедиться в этом можно с помощью программы "Таблица символов", которая есть в Windows.

Вернёмся к нашей задаче. Мы должны выбрать случайную из букв 'A'..'Z'. Так как коды этих символов последовательны, то мы должны выбрать произвольное число от код_символа_A до код_символа_Z. Напомню, что для выбора случайного числа используется функция Random(n), возвращающая случайное число от 0 до n-1. Недостаток функции в том, что она берёт числа всё время от нуля, а код символа 'A' уж явно не 0. Но и здесь ничего сложного нет: сначала узнаём "ширину" диапазона кодов - из кода символа 'Z' вычитаем код символа 'A'. Не забываем прибавить 1, иначе буква 'Z' никогда не попадёт в строку, т.к. число берётся от 0 до n-1. Ну и дальше мы делаем сдвиг числа на код символа 'A'.

На буквах: от 'A' до 'Z' p позиций. Сама 'A' стоит в позиции n. Очевидно, 'Z' стоит в позиции p+n. Берём случайное число от 0 до p, а прибавив n получаем число из интервала от n до n+p. Простая арифметика, которая не для всех кажется простой.
Итак, код символа мы получили - осталось только добавить соответствующий символ в нашу строку. Функция Chr() возвращает символ с указанным кодом.

Для справки: очень часто, глядя на такой код, говорят, что он не оптимален - мол, коды символов будут постоянно рассчитываться (речь о функции Ord). Однако знающие люди никогда этого не скажут, ведь в Delphi есть маленькая хитрость: компилятор вычислит эти значения ещё на этапе компиляции и в программе они будут просто константами. Т.е. Ord('Z') и Ord('A') в программе не будут считаться никогда - там будут стоять вполне реальные числа, а значит никакого избытка вычислений идти не будет. Более того, даже вычитание будет произведено на этапе компиляции, ведь оба слагаемых являются константами. Мы пишем Ord('A') и Ord('Z') только из-за того, чтобы не лезть в кодовую таблицу, и не смотреть вручную коды этих символов. Кроме того, если вместо этого записать реальные коды, другой человек может испытать затруднение при чтении кода - он ведь не знает, откуда вы взяли эти числа и не помнит всю кодовую таблицу, чтобы определить, каким символам эти числа соответствуют.

Далее полученная строка s добавляется в Memo1 уже известным способом. А вот последняя строка в теле цикла - это маленькая хитрость. В одном из уроков об этой команде уже упоминалось. Дело в том, что при отсутствии этой команды программа просто зависнет с точки зрения пользовательского интерфейса - никаких изменений в Memo видно не будет, да и само окно перестанет реагировать на щелчки. На самом деле, конечно, программа будет работать, в памяти генерация строк будет идти полным ходом... Команда Application.ProcessMessages заставляет приложение обработать всю очередь задач, в том числе по отрисовке окна программы и элементов формы. Если при генерации каждой строки это будет происходить, программа будет выглядеть вполне живой - можно будет легко переместить окно по экрану и, что самое главное, нажать на заветную кнопку "Стоп". Ради эксперимента попробуйте убрать эту строку, и посмотреть, что получится.

0017_01

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

Подводим итог

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