Remove ads
повторювана частина комп'ютерної програми З Вікіпедії, вільної енциклопедії
Цикл — різновид керівної конструкції у високорівневих мовах програмування, призначений для організації багаторазового виконання набору інструкцій (команд). Також циклом може називатися будь-яка багатократно виконувана послідовність команд, організована будь-яким чином (наприклад, із допомогою умовного переходу).
Послідовність інструкцій, призначена для багаторазового виконання, називається тілом циклу. Одноразове виконання тіла циклу називається ітерацією. Вираз, що визначає чи буде вчергове виконуватися ітерація, чи цикл завершиться, називається умовою виходу або умовою завершення циклу (або умовою продовження) залежно від того, як інтерпретується його істинність — як ознака необхідності завершення чи продовження циклу. Змінна, в якій зберігається номер поточної ітерації, називається лічильником ітерацій циклу або просто лічильником циклу. Цикл не обов’язково містить лічильник, також лічильник не забов’язаний бути одним — умова виходу із циклу може залежати від декількох змінюваних в циклі змінних, а може визначатися зовнішніми умовами (наприклад, настанням певного часу), в останньому випадку лічильник взагалі не знадобиться.
Частинами виконання будь-якого циклу є початкова ініціалізація змінних циклу, перевірка умови виходу, виконання тіла циклу та оновлення змінної циклу на кожній ітерації. Крім того, більшість мов програмування надають засоби для дострокового керування циклом, наприклад, оператори завершення циклу, тобто виходу з циклу незалежно від істинності умови виходу (в мові С — break
) і оператори пропущення ітерації (в мові С — continue
).
Іноді в програмах використовуються цикли, вихід з яких не передбачено логікою програми. Такі цикли називаються безумовними або нескінченними. Особливих синтаксичних засобів для створення таких циклів, через їхню нетиповість, мови програмування не передбачають, тому такі цикли створюються за допомогою конструкцій призначених для створення звичайних (або умовних) циклів. Для забезпечення нескінченного повторення перевірка умови в такому циклі відсутня (якщо дозволяє синтаксис, як, наприклад, у циклі LOOP … END LOOP
мови Ада), або замінюється константним значенням (while true do …
в Паскаль).
Цикл з передумовою — цикл, що виконується доки істинна деяка умова, вказана перед його початком. Ця умова перевіряється до початку виконання тіла циклу, тому тіло може бути не виконане жодного разу (якщо умова з початку хибна). У більшості процедурних мов програмування здійснюється за допомогою інструкції while
, звідси його друга назва — while-цикл.
На мові Паскаль цикл з передумовою має такий вигляд:
while <умова> do
begin
<тіло циклу>
end;
На мові Сі:
while(<умова>)
{
<тіло циклу>
}
Цикл з післяумовою — цикл, в якому умова перевіряється після виконання тіла циклу. Звідси випливає, що тіло циклу завжди виконується хоча б один раз. У мові Паскаль такий цикл здійснює інструкція repeat … until;
у Сі — do … while
.
На мові Паскаль цикл з післяумовою має такий вигляд:
repeat
<тіло циклу>
until <умова>
На мові Сі:
do
{
<тіло циклу>
}
while(<умова>)
У трактуванні умови циклу з післяумовою в різних мовах є розбіжності. У Паскалі і мовах похідних від нього умова такого циклу трактується як умова виходу (цикл завершується, коли умова істинна), а в Сі та його нащадках — як умова продовження (цикл завершується, коли умова хибна).
Цикл з виходом з середини — найзагальніший тип умовного циклу. Синтаксично такий цикл оформляється за допомогою трьох інструкцій: початок циклу, кінець циклу та інструкції (команди) виходу з циклу. Інструкція початку позначає точку програми, з якої починається тіло циклу, інструкція кінця — точку, де тіло закінчується. Всередині тіла має бути присутня команда виходу з циклу, при виконанню якої цикл завершується і керування передається на оператор, наступний після інструкції кінця циклу. Природно, щоб цикл виконався більш ніж один раз, команда виходу має викликатися не безумовно, а тільки при виконанні умови виходу.
Принциповою відмінністю такого різновиду циклу від розглянутих вище є те, що частина тіла циклу, розташована після початку циклу і до команди виходу, виконується завжди (навіть якщо умова виходу з циклу істинна при першій ітерації), а частина тіла циклу, наступна за командою виходу, не виконується при останній ітерації.
Легко побачити, що за допомогою циклу з виходом з середини легко утворити як цикл з передумовою (розташувавши команду виходу на початку тіла циклу), так і цикл з післяумовою (розташувавши команду виходу в кінці тіла циклу).
Частина мов програмування містить особливі інструкції для утворення циклу з виходом з середини. Так, у мові Ада для цього використовується конструкція LOOP … END LOOP
і команда виходу EXIT
або EXIT WHEN
:
LOOP
... Частина тіла циклу
EXIT WHEN <умова виходу>;
... Частина тіла циклу
IF <умова виходу> THEN
EXIT;
END;
... Частина тіла циклу
END LOOP:
Тут всередині циклу може бути будь-яка кількість команд виходу обох типів. Самі команди виходу принципово не відрізняються, зазвичай EXIT WHEN
застосовують, коли перевіряється тільки умова виходу, а просто EXIT
— коли вихід з циклу здійснюється в одному з варіантів складного умовного оператора.
У тих мовах, де подібних конструкцій не передбачено, цикл з виходом з середини може бути побудований за допомогою будь-якого умовного циклу та інструкції дострокового виходу з циклу (такого, як break
в Сі, exit
в Турбо Паскалі т.п.), або інструкції безумовного переходу goto
.
Цикл з лічильником — цикл, в якому деяка змінна змінює своє значення від заданого початкового значення до кінцевого значення з деяким кроком, і для кожного значення цієї змінної тіло циклу виконується один раз. В більшості процедурних мов програмування реалізується оператором for
, в якому вказується лічильник (так звана «змінна циклу»), потрібна кількість проходів (або граничне значення лічильника) і, можливо, крок, з яким змінюється лічильник. Наприклад, в мові Оберон-2 такий цикл має вигляд:
FOR v := b TO e BY s DO ... тіло циклу END
(тут v
— лічильник, b
— початкове значення лічильника, e
— межове значення лічильника, s
— крок).
Неоднозначне питання про значення змінної по завершенні циклу, в якому ця змінна використовувалась як лічильник. Наприклад, якщо в програмі на мові Паскаль зустрінеться конструкція вигляду:
i := 100;
for i := 0 to 9 do
begin
... тіло циклу
end;
k := i;
виникає питання: яке значення буде в підсумку присвоєне змінній k
: 9, 10, 100, може якесь інше? А якщо цикл завершиться достроково? Відповіді залежать від того, чи збільшується значення лічильника після ітерації і чи не змінює транслятор це значення додатково. Ще одне запитання: що буде, якщо всередині циклу лічильнику буде явно присвоєне нове значення?
Різні мови програмування мають різні підходи до цього питання. В деяких поведінку лічильника чітко регламентовано. В інших, наприклад, Паскалі, стандарт мови не визначає ані кінцевого значення лічильника, ані наслідків його явної зміни в циклі, але не радиться змінювати значення лічильника явно і використовувати його після завершення циклу без повторної ініціалізації. Програма на Паскалі, що ігнорує цю пораду, матиме невизначену поведінку, тобто може давати різні результати при виконанні на різних системах чи при використанні різних трансляторів або навіть різних режимів оптимізації одного й того ж транслятора.
Радикально вирішене питання в мові Ада: лічильник вважається описаним в заголовку циклу та поза циклом просто не існує. Навіть якщо ім’я лічильника вже використовується в програмі, всередині циклу як лічильник використовується окрема змінна. Лічильнику заборонено явно присвоювати будь-які значення, він може змінюватись лиш внутрішнім механізмом оператора циклу. В результаті конструкція
i := 100;
for i in (0..9) loop
... тіло циклу
end loop;
k := i;
зовнішньо аналогічна вищенаведеному циклу на Паскалі, трактується однозначно: змінній k
буде присвоєно значення 100, оскільки змінна i
, використовувана поза межами даного циклу, не має жодного стосунку до лічильника циклу i
, який змінюється всередині циклу. Подібне відокремлення лічильника зручне і безпечне: не потрібен окремий опис для нього та мінімізується ймовірність випадкових помилок, пов’язаних із випадковим руйнуванням зовнішніх стосовно циклу змінних. Якщо програмісту потрібно включити в готовий код цикл з лічильником, то його не турбує чи існує змінна з ім’ям, яке він обрав для лічильника, і йому не потрібно додавати опис свого лічильника. Він просто пише код зі змінною-лічильником, ім’я якої йому зручне, і може бути впевненим, що ніякої колізії імен не відбудеться.
Цикл з лічильником завжди можна написати як умовний цикл, перед початком якого лічильнику присвоюється початкове значення, а умовою виходу з циклу є досягнення лічильником кінцевого значення; в тіло циклу при цьому додається оператор зміни лічильника на потрібний крок. Однак спеціальні оператори циклу з лічильником можуть ефективніше транслюватися, бо формалізований вигляд такого циклу дозволяє використовувати спеціальні процесорні команди організації циклів.
В деяких мовах, наприклад, Сі та інших, похідних від неї, цикл for
, незважаючи на синтаксичну форму циклу з лічильником, в дійсності є циклом з передумовою. Тобто в Сі конструкція циклу:
for (i = 0; i < 10; ++i)
{
... тіло циклу
}
фактично являє собою інший варіант запису конструкції[1]:
i = 0;
while (i < 10)
{
... тіло циклу
++i;
}
Тобто в конструкції for
спочатку пишеться довільне речення ініціалізації циклу, потім — умова продовження і, насамкінець, виконувана після кожного тіла циклу деяка операція (це не обов’язково має бути зміна лічильника; це може бути модифікація вказівника або будь-яка зовсім стороння дія). Для мов такого типу вищезазначена проблема розв'язується дуже просто: змінна-лічильник поводиться цілком передбачувано і по завершені циклу зберігає своє останнє значення.
Ще одним варіантом є цикл, який пробігає елементи з деякої множини без явного задання порядку перебору цих об’єктів. Такі цикли являють собою формальний запис інструкції виду: «Виконати дію X для всіх елементів множини M». Теоретично такий цикл ніяким чином не визначає, в якому порядку буде застосовуватись дія до елементів множини, хоча певні мови програмування, звісно, можуть встановлювати конкретний порядок перебору елементів. Довільність дає можливість оптимізації виконання циклу за рахунок організації доступу в найвигіднішому порядку, а не в порядку зазначеному програмістом. За наявності можливості паралельного виконання декількох операцій можливо навіть розподілення виконання циклу по колекції, коли одна і та сама операція одночасно виконується на різних обчислювальних модулях для різних об’єктів, при тому що логічно програма залишається послідовною.
Цикли по колекції реалізовано в деяких мовах програмування (C#, Eiffel, Java, JavaScript, Perl, Python, PHP, LISP, Tcl, C++11 та ін.) — вони дають змогу цикл по всіх елементах даної колекції об'єктів. У визначенні такого циклу треба вказати лише колекцію об’єктів і змінну, якій в тілі циклу буде присвоєне значення оброблюваного в цей час об’єкта (чи посилання на нього). В різних мовах програмування синтаксис оператора різний:
C#:
foreach (type item in set)
{
//використання item
}
const char *str[] = {"one", "two", "forty-four"};
for (const char *p: str)
{
std::cout << p << std::endl;
}
Perl:
foreach (@set)
{
#використання $_
}
або
for(@set)
{
#використання $_
}
або
foreach $item(@set)
{
#використання $item
}
across set as cursor loop
-- використання cursor.item
end
Java:
for (type item : set)
{
//використання item
}
for (txtProperty in objObject)
{
/*
використання:
objObject [txtProperty]
*/
}
PHP:
foreach ($arr as $item) {
/* використання $item*/
}
For Each item As type In set
'використання item
Next item
foreach ($item in $set) { # використання $item }
або
$set | ForEach-Object { # використання $_ }
for item in iterator_instance:
# використання item
Багато-які з мов програмування, що мають в своєму синтаксисі циклічні конструкції, мають також особливі команди, які дозволяють порушити порядок роботи цих конструкцій: команду дострокового виходу з циклу та команду пропуску ітерації.
Команда дострокового виходу застосовується, коли необхідно перервати виконання циклу, в якому умови виходу ще не досягнуто. Таке буває, наприклад, коли під час виконання циклу зустрічається помилка, після якої подальше виконання циклу не має сенсу.
Команда дострокового виходу зазвичай називається EXIT
або break
, а її дія аналогічна дії команди безумовного переходу (goto
) на команду, розміщену безпосередньо за циклом, всередині якого ця команда знаходиться. Так у мові Сі два нижченаведених цикли працюють цілком однаково:
// Застосування оператора break
while(<умова>) {
... оператори
if (<помилка>) break;
... оператори
}
... продовження програми
// Аналогічний уривок без break
while(<умова>) {
... оператори
if (<помилка>) goto break_label;
... оператори
}
break_label:
... продовження програми
В обох випадках, якщо в тілі циклу виконується умова <помилка>, буде виконаний перехід на оператори, позначені як «продовження програми». Таким чином, оператор дострокового виходу з циклу, за суттю, просто маскує безумовний перехід, однак використанню break
варто віддати перевагу перед використанням goto
, оскільки поведінка break
чітко задана мовою, потенційно менш небезпечна (немає, наприклад, імовірності помилитися з розташуванням або назвою мітки переходу). Окрім того, явний достроковий вихід з циклу не порушує засад структурного програмування.
Звичайний оператор дострокового виходу перериває роботу того циклу, в якому він безпосередньо знаходиться. В багатьох мовах програмування функціональність цього оператора розширена, він дозволяє виходити з декількох вкладених циклів (див. нижче). У таких випадках цикл, з якого треба вийти, позначається міткою, а в операторі дострокового виходу вказується ця мітка.
Даний оператор застосовується, коли в поточній ітерації циклу необхідно пропустити всі команди до кінця тілу циклу. При цьому сам цикл не переривається, умови продовження або виходу обчислюються звичайним чином.
У мові Сі та її мовах-нащадках як команда пропуску ітерації використовується оператор continue
в конструкції циклу. Дія цього оператора аналогічна безумовному переходу на рядок всередині тіла циклу, наступний за останньою його командою. Наприклад, код на Сі, який знаходить суму всіх додатних елементів масиву, може мати такий вигляд:
int arr[ARRSIZE];
...
// Сумування окремо всіх і тільки додатних
// елементів масиву arr із застосуванням continue.
int sum_all = 0;
int sum_pos = 0;
for (int i = 0 ; i < ARRSIZE; ++i)
{
sum_all += arr[i];
if (arr[i] <= 0) continue;
sum_pos += arr[i];
}
// Те саме з goto
int sum_all = 0;
int sum_pos = 0;
for (int i = 0 ; i < ARRSIZE; ++i)
{
sum_all += arr[i];
if (arr[i] <= 0) goto cont_label;
sum_pos += arr[i];
cont_label:
}
З другого уривку ясно видно, як працює continue
: він просто передає виконання за останню команду циклу, із пропуском команди сумування, якщо черговий елемент масиву не задовільняє умові. Таким чином, в sum_pos
накопичується сума тільки додатних елементів масиву.
З погляду структурного програмування команди дострокового виходу з циклу і продовження ітерації є надлишковими, оскільки їх дія може бути легко змодельована чисто структурними засобами. Більш того, на думку ряда теоретиків програмування (зокрема, Едсгера Дейкстри), сам факт використання в програмі неструктурних засобів, чи це класичний безумовний перехід чи будь-яка з його спеціальних форм, на кшталт break
або continue
, є свідченням недостатньо пропрацьованого алгоритму розв’язання задачі.
Однак на практиці код програми часто є записом наявного, раніш сформульованого алгоритму, перепрацьовувати який недоцільно за чисто технічними причинами. Спроба замінити в такому коді команду дострокового виходу на структурні конструкції часто виявляється неефективною або громіздкою. Наприклад, вищеподаний уривок коду може бути записаний без використання команди break
так:
// достроковий вихід з циклу без break
bool flag = false; // прапорець дострокового завершення
while(<умова> && !flag) {
... оператори
if (<помилка>) {
flag = true;
} else {
... оператори
}
}
... продовження програми
Легко переконатись, що цей уривок працюватиме так само, як і попередній, різниця лиш в тому, що в місці перевірки на помилку замість безпосереднього виходу з циклу встановлюється прапорець дострокового виходу, який перевіряється потому в штатній умові продовження циклу. Однак для відмови від команди дострокового виходу довелось додати в програму опис прапорця і другу гілку умовного оператора, до того ж відбулося «розмиття» логіки програми (рішення про достроковий вихід приймається в одному місці, а виконується в іншому). В результаті програма не стала ані простішою, ані коротшою, ані зрозумілішою.
Дещо інакше стоїть справа з командою пропуску ітерації. Вона, як правило, дуже легко замінюється на умовний оператор. Наприклад, поданий вище уривок сумування масиву можна записати так:
int arr[ARRSIZE];
...
// Сумування окремо всіх і тільки додатніх
// елементів масиву arr із заміною continue
int sum_all = 0;
int sum_pos = 0;
for (int i = 0 ; i < ARRSIZE; ++i)
{
sum_all += arr[i];
if (arr[i] > 0) // Умова замінена на протилежну!
{
sum_pos += arr[i];
}
}
Як бачимо, достатньо було замінити умову на протилежну та помістити заключну частину тіла циклу в умовний оператор. Можна зауважити, що програма стала коротшою (за рахунок видалення команди пропуску ітерації) і одночасно більш логічною (із коду безпосередньо видно, що сумуються додатні елементи).
Незважаючи на обмежену корисність і можливість заміни на інші мовні конструкції, команди пропуску ітерації і, особливо, дострокового виходу з циклу в окремих випадках виявляються вкрай корисними, саме через це вони зберігаються в сучасних мовах програмування.
Існує можливість утворити цикл всередині тіла другого циклу. Такий цикл зветься вкладеним циклом. Вкладений цикл щодо циклу в тіло якого він вкладений буде йменуватися внутрішнім циклом, і навпаки цикл в тілі якого існує вкладений цикл буде йменуватись зовнішнім щодо вкладеного. Всередині вкладеного циклу може бути наступний вкладений цикл, утворюючи наступний рівень вкладеності і так далі. Кількість рівнів вкладеності, як правило, не обмежується.
Повна кількість виконання тіла внутрішнього циклу не перевищує добутку кількості ітерацій внутрішнього і всіх зовнішніх циклів. Наприклад взяв три вкладених один в одного цикли, кожний по 10 ітерацій, отримаємо 10 виконань тіла зовнішнього циклу, 100 для циклу другого рівня і 1000 в найбільш вкладеному циклі.
Одна з проблем, пов’язаних із вкладеними циклами — організація дострокового виходу з них. В багатьох мовах програмування є оператор дострокового завершення циклу (break
у Сі, exit
у Паскалі, last
в Perl і т. ін.), але він, як правило, забезпечує вихід лише з циклу того рівня, звідки викликаний. Виклик його у вкладеному циклі призведе до завершення лише цього вкладеного циклу, зовнішній цикл продовжить виконання. Проблема може здатися надуманою, але вона дійсно іноді виникає при програмуванні складної обробки даних, коли алгоритм вимагає негайного переривання за певних умов, наявність яких можна перевірити тільки в глибоко вкладеному циклі.
Розв’язків проблеми виходу з вкладених циклів декілька.
goto
для виходу в точку програми, наступну безпосередньо за вкладеним циклом. Цей варіант критикується прихильниками структурного програмування, як і всі конструкції, що вимагають використання goto
. Деякі мови програмування, наприклад, Модула-2, просто не мають оператора безумовного переходу, тому в них подібна ситуація неможлива.goto
.return
. Недолік — виділення уривка коду в окрему процедуру не завжди обґрунтоване, і не всі мови мають штатні засоби для дострокового завершення процедур.loop_y : FOR y IN 1..MAXY DO
BEGIN
FOR x IN 1..MAXX DO
BEGIN
...
EXIT loop_y WHEN object_found(x,y);
END
В теорії програмування відома ще одна форма циклічної конструкції, яка принципово відрізняється від «класичних», яка отримала назву «цикл Дейкстри», за ім'ям Едсгера Дейкстри, який першим її описав. В класичному дейксрівському описі вона має такий вигляд:
do P1 → S1, … Pn → Sn od
Тут do
— позначка початку конструкції циклу, od
— позначка завершення конструкції циклу, Pi — i-та варта (логічний вираз, який може мати значення «істина» або «хиба»), Si — i-а команда під вартою. Цикл складається з однієї чи декількох гілок, кожна з яких являє собою пару з варти і команди (в дійсності команда може бути складною).
При виконанні циклу Дейкстри в кожній ітерації відбувається обчислення варт. Якщо, хоча б одна з них істинна, виконується відповідна команда, після чого починається нова ітерація (якщо істинні декілька вартових умов, виконується лиш одна команда). Якщо всі варти хибні, цикл завершується. Неважко зауважити, що цикл Децкстри з однією вартою і однією командою являє собою звичайний цикл з передумовою.
Хоча цикл Дейкстри був винайдений ще в 1970-х, спеціальних конструкцій для його створення в мовах програмування не існує. Єдиним винятком став нещодавно створений Оберон-07 — перша реальна мова програмування, що явно підтримує цикл з декількома гілками з вартами. Втім, цикл Дейкстри може бути без особливих труднощів змодельований за допомогою звичайних конструкцій мов програмування. Ось один з можливих прикладів його реалізації на мові Ада:
loop
if P1 then
S1;
...
elsif Pn then
Sn;
else
exit;
end if;
end loop;
Тут P1-Pn — вартові умови (варти), а S1-Sn — відповідні команди.
Цикл Дейкстри зручний при реалізації специфічних обчислень, які повторюються і, які незручно описувати за допомогою традиційніших циклічних конструкцій. Наприклад, цим циклом природно представляється скінченний автомат — кожна гілка відповідає одному стану автомата, варти вибудовуються так, щоб в поточній ітерації обиралась гілка, відповідна поточному стану автомата, а код варти забезпечує виконання обчислень в поточному стані і перехід в наступне (тобто така зміна змінних, після якої на наступній ітерації буде істиною варта потрібної гілки).
Легко бачити, що цикл Дейкстри не містить явної умови продовження або виходу з циклу, що не всіма теоретиками програмування розглядається як благо. Тому було запропонована ускладнена конструкція циклу Дейкстри, яка отримала назву «цикл-'павук'». У тій же нотації вона має такий вигляд:
do P1→S1, … Pn→Sn out Q1→T1, … Qn→Tn else E od
Тут після позначки out
додано гілки завершення, утворені з умов виходу Qi і команд завершення Ti. Окрім того, додано гілку альтернативного завершення else
з командою E.
Цикл-'павук' виконуються таким чином:
Структура циклу-'павука' дозволяє гранично-строго описати умови виконання циклу. Згідно з теоретичними положеннями, гілка альтернативного завершення не повинна використовуватися як один із варіантів коректного припинення роботи циклу (всі такі варіанти мають бути оформлені в вигляді відповідних гілок завершення з явною умовою), вона слугує лише для того, щоб відслідкувати ситуацію, коли за якихось причин цикл почав виконуватись нештатно. Тобто команда альтернативного завершення може лише аналізувати причини помилки та подавати результати розбору.
Хоч явної підтримки на рівні синтаксису мови для цього циклу не існує в жодній мові програмування, цикл-'павук', як і цикл Дейкстри, може бути змодельованим за допомогою традиційних структурних конструкцій.
FOR
з'явився в інтересах практичної зручності використання[2].Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.